From ebea18084523c3b2ce4a05dbc517914dd7ab17a1 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 9 Jul 2025 13:27:41 +1000 Subject: [PATCH 001/244] update Pro CTA for animated profile image --- Session.xcodeproj/project.pbxproj | 12 ++++++ .../Meta/WebPImages/AnimatedProfileCTA.webp | Bin 0 -> 473102 bytes .../AnimatedProfileCTAAnimation.webp | Bin 0 -> 1131500 bytes .../Modals & Toast/ProCTAModal.swift | 39 +++++++++++++++--- 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 Session/Meta/WebPImages/AnimatedProfileCTA.webp create mode 100644 Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 463e9bc44b..64296b291c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -192,6 +192,10 @@ 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */; }; + 94AAB1442E1DF50000A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1422E1DF50000A6FA18 /* AnimatedProfileCTA.webp */; }; + 94AAB1452E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1432E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; + 94AAB1462E1DF50000A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1422E1DF50000A6FA18 /* AnimatedProfileCTA.webp */; }; + 94AAB1472E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1432E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; @@ -1513,6 +1517,8 @@ 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+URLs.swift"; sourceTree = ""; }; + 94AAB1422E1DF50000A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; + 94AAB1432E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimation.webp; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; @@ -2776,6 +2782,8 @@ 94CD963F2E1BABE90097754D /* WebPImages */ = { isa = PBXGroup; children = ( + 94AAB1422E1DF50000A6FA18 /* AnimatedProfileCTA.webp */, + 94AAB1432E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp */, 94CD96472E1CDC000097754D /* PinnedConversationsCTA.webp */, 94CD963C2E1BABE90097754D /* GenericCTA.webp */, 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */, @@ -5449,6 +5457,8 @@ 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */, B8D07406265C683A00F77E07 /* ElegantIcons.ttf in Resources */, FD86FDA42BC51C5400EC251B /* PrivacyInfo.xcprivacy in Resources */, + 94AAB1462E1DF50000A6FA18 /* AnimatedProfileCTA.webp in Resources */, + 94AAB1472E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */, 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5508,6 +5518,8 @@ C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */, B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */, 34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */, + 94AAB1442E1DF50000A6FA18 /* AnimatedProfileCTA.webp in Resources */, + 94AAB1452E1DF50000A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */, 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */, 34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */, 34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */, diff --git a/Session/Meta/WebPImages/AnimatedProfileCTA.webp b/Session/Meta/WebPImages/AnimatedProfileCTA.webp new file mode 100644 index 0000000000000000000000000000000000000000..9d2ee88e15b2448df19613fc8eb94e3c1c7bd0dc GIT binary patch literal 473102 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

@#sYXS=Rk9Ewbo|kqx>Y` z?o153^bG@!xQ~GJF+~FQ(Au#8Bt(T!2D*R&S0Zp`JJ~UDUTwv}w{vX4n5f_FDiPyZ>5L`xFgSAokfEdy$GlBv zP@2X|9fks?zstt{^!;>72`q)Gv{+SmQ)!%|j5!v+9 z&8%(1Uu0*zcqASizSs6+*Km%ve)0$e7DkUj)2Y6ufvOqO0qK*Bhy(I4`ht~@ok+$C z2FvVR@~IOX$^qrZ(xD6}FR+4)T37&|)|M!W8U;nkNS}p!9`19$?c64jJHQh~i=@>P zRLceU)N``Sn)Tbu1*?~k6~j&?%hG#3H1U~qbQcU%;_EZ)S;!-Mp~lWwjjHd3=+8p~ zj2(u!cE6KUj*3ic-^QV6u|Oic+x+N8yIcqz#*7p!5fmhwlTn8i-*8Zq#vRF)GdNzx zQ%)2r5udNrk!&if45ea7Kj$ZJf!*0*%~E{5Re+@X355Y zLe(bIS)Ka$?%1@~i8xJjZK#r70`hCL)c6!O(EB8rP}}5<`iHvIt{X)L(;~lu_a)6^qvB*4OWNnC^`sA*|^IKd)kzwDdFZP-Xy)!@>-YxsjC3J4?KPPkxjE4NvU ziNsm=vGZ^cJjKYkUlH%*VleeNdk^Q5=Q+_5_nXwH+iJGGZ!X%J#MtUP;7?K)H}>Yfcu zbeR45*tJ1^Q5(2A$oV>p_Mvys%XU%1xyY6`RcsjRe5Ih)?^4?*apVX0&Q%oaIt@hY zpRMND2}o(cx1`83-S9M7P^fnyJOpp9AEL__7=bB6tz(SRC-RT?o-3?B#RzIdnL;+W zy)z3DV(B+#)Lsx_DXl_%fDG7OE61UyGr>=c@1KKB0O{cC%18%_^s#KCzr57g0?;ho z0^Vm;9eBwRA8@VSHbhQ#stzbm_#PV{H-K8}ZXQlG$}>*^qEdzg!HRe3it6ZN>nxA< z@^;B{5lT7WurE)tZBi#d@f<0I(Ke>q1`$HxbbVUB_TP zOp2vV0I(BYEvTA>iE|&eJf020U6^dXRhRVdqQ-_|!wNp{mmsS+^QBbxSCua%BoB0) zjodOsjDljFy;K5RdcULtdQc=VN}nW6CcKlaDv) z)Vj&B&L<0>4m%d!++nv!n`y^Ty{$xMJ|t{ob>>I7zloQMJLlo+k&9sf{NZ-4&0PfJ zsHNp$woRB(Tc+u%on`H}3&smm9l(`=&f{HEYfW1sW@%vc9)IC62>M_gM)ul_U4jPiPr-%XhucqbjY_!dYBAkAnNY)oUGZ7a6izSZ-; zj@lpcU>uBUR}_6FI;IsjZ(uQ1pYHN%_+7+4t&0eE1e90UfhU%h5OqfHv;u$OQ?tt5R z#KCX~;bHJ6Le_-RD_H&FOs>+``h{Mvvh3EilzK|%8tJY#{6=NI>!Vwa2!2-oe#2rG zXWzgmUx`D0jr*(<{-v}{OM@Lky?j9WjB=s>;Gaw&;cu2|>hCFeETsiVaBOlHXs?M{1=iaew z@+*lIt144J_cB^#xp>Cv3?QLe2IW&Y-|Ke2IE@uuN+vG8Hl(b=qW1rhmEXNrG6kPB zb?O4OuIN$Tzx{gDA3M8fh?dPs%lSiGorzcHD`M0OKfbKRe$odq1cvA$$QP+apCrQq zA~(^4LUFI5C3KOW;J-q?1wh(E{#{Kv6i}nvxmfHCE0zgN$2yYFA0H!sWNR5nxR$iQ z3QZ6A&B8x+s<24C5^rMxXIQzemuF05PsQC=vsp*XCCAh+kFOnIF6?&$WGoF?%txe< zTIg#O2%*Qgryg58$_a#8QH6B@VA*ZF6@avZUCF#5QsAUfN{6Ogy&)7)GFMo#JVz)M z8JTaa7L8XgMX6zbe@syP{PE0dUGotyN7{2a-4ur0aNNjDLxZGYO_xUYS)ZvtHXM6Ukd46ty3ZLRI=3$vN#9l|_C|69yQ2zm zDHshyD4h%~mE5^E)dtx{rCkxbVk7!cc}_fCq|wQju}FQlsU)h=3`tW%E!{gfS%v1o zKJ?uo+BTVuM};AsmJ2n*K&YzwEFbx3G7OiH;FO!P0q3n#fo0@2_ zsRm#w1?C++=4{>cVpX%eM~Q%$Kk8Q{D;BS?V+C=yjdtl14)g^y*6S7a)1>>u{;57VzeNB3UY`NIRtSDKJnkm@_!HR#t}F=#A#x!>K`&m{Zg}0EI5GC) z{9-@};voj7CC$ANG0v{aAe?F%DzR8p!6A5HXA~lt`jG72g%Oy$~@{ z-mI-p-;8x%)nxNPI84j71`RlH%YY zq;2FG3pE1*E^j9_bpSd7g><486WqxHuE%i%OCcxaqMMK`fN7D8sp9pgoTAq0)gwGx z?sJv3;D}Fq88E)?^AiDO8DLE3)X*_$2BQJd2Xu$V!f;)IUtaMn&LpD$-iQ*HqE_8A zglv@W{@A9+`3V~%M;DhSC#G-_^LlV;zt$0noGd))_2zFP*;B+d?9<3a44qOE$d?bH zmpK?=Gs}GNuRzlR`5Xq>cqg$I?^k;DN&jr+Y>vM$Wh=209C%epcv8?w+Uz3aYN|3; zemWuKc_>!Xh&ee}`#mdp5IkvCjd8+aHNuNuXqc^=Xvg25YY1P88|eQTV1;End8gUh zK)t()-wC>Dg{C;k%`#2}8K`0n%MtckQFbSncsAwW-tfJy@F*=o78f`n2&z@gTv!Mz zp-Xq-LG~J~Pm4vOw5Xw|!r0j%mA^*gNR|xL zUl%^_e64fgo|-1el2!<0YksfmKy2e_L$$8DA}v zwe_75Dpd6L|50?F;cT#PAO82Lwq}i@M%79XqqTw>@rWRTB6eui7PBp-)Oc#|5uvt< zy;rEJR_(p_C{@I+5i3?+@ArI4avb-4|E}M8UP&RDJN#}ni3-`31s(QszqpRHdN$Vc zo-zrKzCxOW-lRcbBr3@xZGKnoNAb+Zb=`pbK>}C=>ATQj3MPOt5_1|KL_PTcw#ZDX zFQN?U2*&F}d>xb>I$HxC@$)lEpWCrtG$rn3Gn1aEi6%Xg1wT76b=-_Fv!fV&cghIw z2>ayh(ez@Bt)iyhVX-)SNdt~G(AOLjw(ZiyjP{ze!Th`yT6f?@IY+gk;=7r%Lz>FFhv##(?2h;3~4)Usm_^CUN<>e{cCSu7iQ~!_G;MS2s|hS zT;=zQv>lAR#4nTr1`>v31qe>}PagdDTCKYQZBtvG1{XKG@y;>LimOyrp+(*6ai*?j z=2{wR^zk>_M@_~_ka{jqj;8L22q0o2X*mBqR1=<`&m%+?iUa89uU15q?w)0L22`dk z)}9Jr=WFcU>Ux9`mNTC=k_Fw-^-hBn=D}qz5{+@W-=Ec_;%}lv?fs7KPcB&LFBR&= z{u?T0N6n+V3`HS9Otlm~#_ARlF3H_{Eju5HvD=WXnvX{}xeDZ52(H3>#V2$mgoQ>P z63P&VkiI;>(}>SM+5-ZjK8ZXjiyf4?*X7k7H@Kb+nF?BD#Npy5YU?NYl8sFBRluH& z;F|2O3p@OxBVium8LmC{?QXP50eZyx0iW>C>XC&hHIoy11bBtH`Rr%nLB>;g`XP(c zu~QLEkt1{6M8A|hz)jY%!|#*BbI6__?Lf5Q#8(yfx1AwM9+2d zj}u^LLQnqtR( za5gS3`;XFNndkzamw)-0U;oAEph(ztHA8RJ<|ciBSZKaqm{0*c3WgNEyj4n}+@1|-f7#M4B^74x zu`sZ=U4y-!4Rir-I_CN!8AQK1EK;hikG*BccsJm1g84_L`AHw4nv;fe_jwu|+Vg?3 z#8GR|O)Ep7=*yPhi`2vMV1<$qIMCX()L}}H9xUnv2EwXDD7PJdbY9xOPVbemt}E1uG#U|~ zPAQ?1*u|E!n^ZJdU?U|HE5_8-=hVflUyAnp_~)bUw7Ol=lc zxER>Pxi+Nr_DNUta;J#ytUw>JsIlr7Ti6U1u+A6Q`5XbXEhfA#Tt%(i3E5| zWuai2Z1CV?_wV0t0rZJ!-XT_pX7M*EE({92330JPf2ep5i%7JIved0r0ruagZ?qsN z8)!=Bk#Nf+s$O`Q@i%$q6YL(+whdI5g*Ny7XEzWf2;j)y}-&(|Y*JfN!BcE3kHS7)Zm<^SuN ztUYPiPmnYY!NgM{#zaC7Kj;MVK$Wj(mUNz-hD}eV`dwq86@BZqH!Dh!6d5JzaagCO zRiaZ-I$lCvP=Av#gMIru1t;aBZauraITvRGo8Eb;5ZXj_liigt5qS&elILr?So#Xs z9BLr|xBL7;(K+znlL#bHKwISq5G8CFj#v!g^TqjTlp4a)=%JeUBmIz$oaKw3+eI|o z4sSf4=-a8PNQvR?g}_ly)cFi+`skHez;P)JTZm2gc;TCjkj*IwUtRF+rv(x=nF>M4 z^HkpxWXr2qoC|51a3id|X_DA;gaO9(@t5p!m#NIa=d2$SFkkdwLLtR4R75^u+A;(J za8HD$11S6f$w8|Mp=Tm1z@hJ7Z3dSA4y&nl5=AL zyt|m=fzC;Ybq=SgcbjN!d$CJ{k;Gv3a0p_f=w?jRj81|KZA8?vr%8`W6XI3`Iqtcr zXMWjx3FqnO3{jko$Zn}w*Rce_`nc7ZEfN_>E{AEnhh_|a9tPSsd#}9H8yA&1r%kS~ z1DEsSeO63K1eT_DVfH{_wO)33VJBsbTld*$MIimwS(e!I3}K2T$%% zss!jTKmi5kzU@iWH#??B-vV;fkkYo*mjws^vcBx@>JsnrX>whhs*5XOs(&7{p|4pX z`KH3&Em?DO6V7%!!{L7FvMu*A^XXFuSH2{?N(IQclul=?o6}1bwf-f7)e~VnPEV%> z&`C06)s;3L_*B2x^XcES8>cwVK~t&3S5f5$fOMav1kTYvqAj3n(P!%O-tabAr&^EX zws+%4%<9YHL*kchbwVQa(=v=b7rVgn_^nYy9U2#^w$t1+qXGXnI6eotdMz_2LzPU} zJ^mg6H+-fZ+r0i zDEG!`jw97y-rqKF{Ly;h?8rXgsyfw8n_?N1a7tYg%#okNY^yIkoe0Bwxwt(UG#gp* zr?~YYn0{EANJ>r{@6uCalyv{#7$WVATCFR>3W1+Xq({s>&LZ!T_nyujL?*>c*8Keu zPXq^=sswd>f^tMLK_MdXJeyjfMMy=egJ?jn`SCva?B3ol@;+B(L$#e;@48LeaBr1} z9jfeCcQHl(-sb98LB6bh@h>_APLYhV-0dCyOmSJc;r7oNtqJX}1?-Z25vx54=#sPt zTbT&{lssle<}8f{x%cZiXVJ<(AU{Zv%KJ}8?iic|gl8V``?E8WXi1@PLC6M+qB{UK z$`tiEUg$9F@-gg;w0=48;U9W>HiFx0@wqHnQFj`esD$Rx^oXE~1hri~v+hnuJ8B(z z!K@U~UH-}6m$W(Azm|mBIm%?TGG)`DJ>Hu$=|&N`Y4EZMs!1NBqM=vi;GPXLs z7wy$|$SuS`Udr6~Y(5fLc!`RG`5eA>%5aAl#>C5uP(y5yG{`hFw57-smW{$U}S{d7V6lbFhl0HkyS&CkJM551tl%ld!=}`H+*HeI=M9gHk+UL z1Pi#R3L=8_?ENLhc)8GB7I_kGQq3OgFxlY5tEGm(D@!=S;GG>%6^Ofesxz*|7V!xC z!XjZl#1HXBKAdyJ(`!32nyAxxyz<4ugptvVT?Cb4)EneyJNajB^x0Z&N{Iz-z|78g z*3AGzBJ}xYABlgUQrQ}kGo8z?2<|LD$QN3oKO8E~HQabnDkn52ovf$RubqG>wHb*l z^C5S)oRU`u`Q}qaaVbbRgkObEhzg?)&p$jd+Tss<$?I4^CMb{k4Gwtw`PsG0UegyvzEtd!oc!t(BB|o zWK4pynoh4zw4}pG0>1XEPkZw--miDh1TSf>b)$rGMBFJz-y;Bh+(l5nACU1VY9C_C z1}Hb%@j8**(~?9oXbO1W{f@9d>tbpu+75~OT+eXF46s?!hPCVa&X`RGHLSEZKFFzBT-S%mzPTve6!{c&@kn08m#3{=nTa2u9KuRXMt z?}QLR3qA09g1FH*cqV)d{vLAY!P&m&tGajYA-2-xPz)<9NL*cHvk+)T3ZN@J>dE*KP(`7S3!ovdlXqGs$W==l ze!jnjz1RN@p84+N$gPIn+`KI@3e9&k(cES_Z~?MqQ4u%?>Z=}cnu<%cc?teM8(B+X zen0ne%51J6><*|_`oN;ZOE-Viwk%QlKC73T1(D!|!3;uLhUWL| zu^<)^{vn9O5ksm2Zj$Z=nN&29aS!sNORt^ULsnbex{R(Cb5HA{fS35pjE_&B zzod|+i;4dOrA9dD%riB2Ax_7&SRo>ew`Jg|mVVuDfqqUC&FSG7o{&PWxddqznzs=z z>Ce2cl44gC0%yXjdN`GCfX^S}CWq`WJ?6EGG`$o{GMZc;_=mhlFK4-Q*uEv z(3YwcOm)>YC7*KncdC;Z#*W$pET1&=#nkexj~)H9?px`5iIjGT&tWD1eeF;{mNq@9|GdatitT`nMKdO=f6B19EQPKgh@}O-k7GktVjxfhQnwfrbFkEr{ofHIeDoCU49d2-wTVC`0|U$nsigmX846z zt1~2Ck2K{u8KJZrf+oCV!^>W5_d<~WKFE|ym~T*cBWW*DYKeJF8bGZP@0Y=ag@X@e zKJn-zs*4NOehn3W+6Dd?NLF6jBi>iT#QSMns;o^vMgV|7jPlc^NX+m=cgso33H>Gc z7UtD3y`L|aErHnjBgiPAg2^y4013J9#j{NkIybMok zrLOu`WzrIvz7B-YX5d>|`*gm~Ncy|EeTt*M3i_-U1-VP^3A|d`By&67qR;#m3>5Qz zm@m}FeG;jzj2GsJ%EbesJD}qYS{NCatoQSs=JXTBAGc46+GiI27RKI58+EEyZ|U{^ zvhi3`Ii1)MM_GN@`N&_9=lx{)>0pna^h>`VsdI|X6J(fVQlroj zsG1Evm}Y?ltH=_`IgSZcGDVoGc}Z89azUw^nh|lK5->a$uTo(C)i><$Yx7MYAc`!V z9HTaxZhD4P!C;w)#R9~`h{2A{9r-s`K;e4+QJLAA#{hTe&7>uvSw$sRT8Me@p&+K> zP~_TZI)wT)aryZ5w}l2?y0l^3+!`hOKKF(#x}_67rLsIHSa4o$t6`%NZI>s*eOy zgPdXo+=X*U7o7PD!3i(`FT?V8a@;ml@RA_XoXO_t5lEcI$x+ZML(#1B#O203lfo2SD_E1!!DG4T8hX>SH51oc74)2r!9I+lIE-^XlL_QoSk z8Vpn1$3idM=z)#Z{Tm=NnrWOg1tILQ=-`XDawm*6t<0I^9NBcs@QazxDC5obL=aQ# zT1XSKNnAfe>_P?JqVlFB;-Vnjq-gKo*8>h7eCAsEi`omu{UK~e6ByZ7HC!d;%}JrN zJLFCr3S8&V-(MCRnhTq<{zeLpYxXY(BG_2!jZO5+BHr=}=g-?INZcg^C%dJbV4wX@ z<-%E}CjQmD-&09|v@m^v1t$yA3PW?~^+>RN7Rjt>p2peh=a(_~s=nE6zpH>pb501$ zdRRCX1qi{zHOIyo7=OW#+b=^XBRR>D&M?lQs@dpXQ>k7^lBrB;l39M(_?uD3G+A@3 zzp0W2vxZu9Lx#Bq;fbj|_nVxce&sV0iRe(XEe$$U<&Uvi*o?SszgI33z^GKH99i}t zrZh?#RBtv8q#v&2vdz`6;K6F5d-v#Wm|O}?KmnQq)#J(>IkBR9{cY`C+ZBDhpWhE# zmkP-TG&axT{2o#PW9--v1w#4snvr7*>d-Nc&9`IVC*R`r?%ldjX7Yln7VciYl!{E> zvwU8PXZB2vXK+rlc~L_N0^I;M>*?`-tIcXtE4f7xCHQLstDMya@4mfvPRsJCafz4>z?1~AFefRY6}c*r+-u$pt^J7YBw9ehTTri1ng zCJdCyZJ+9GR^8ypfQx^8XZ-CLfRY81Y08CARrM4p@Zb;j`TFN5;p;R**WHgJPJQNi zJ~u9E;F3OcQRs^fB`-eOF!M6L@}YTnH_cwoeb(}tU38ti#^1GfxCD)oMr)s~#n{P>0LVv7P#Y9DngJT-QLFbw98Q0IR zU=qDe>FqxM9L3lYmi^dBoly-Gfxw{L_%XvWytTKVUU65ld$f&*PHz*4W`7$*jXQ+P z?)_K@^q_$$&&WAXkfeZHP<)MBHPVj+J4TyLYc zxEZ82?}$_RbT8)hn(yJ({|XqDdkcBNbU}Q8mg`%>qa#pAPSnixNZ_9){h;;WqG_#x z*L@Dt#vyi{2%&;+wKX0lBN`oESrM8cPGht@Uv8ZjYn{5* z!^WGvp{%S=7fX$kRtKBKNY7cQLz&j-&DEX%hW9?a8@Q+Z`&_~!n0tj(&<4ZvhON~_ zF}_er;n4(x;+>Nt%3K~(6pS(m6A%9zDxW?{ybJx^?DLSdHO9x}henGwf{z20W${+6 zF*{kvov$Q|A5=hK=~FCprdfHoz}uncs07FL_~rYhz_C4iO(4uuy1$p}%H#EGx1UbG z?pqkz?ZLKDd>M&Q9CV;zR|6zNEBl&kPsR0^hzC>xGU4M8=s%oK*ymkbRDblZ^HC=& z59~9Bad31#z}Biv6qd1u0IiY=CNjup__tGa82 z>C|||7o6c=6$pRd(;kaLouCFgwm^SahqQi$H{RNjQJ*-<*x6hYkN-?HJgx_HIq7du zLX}u$2W;C|X~0U19_a6&swntF0lcoNo=qQDtcDtg_J8bs&qZJpidD+4!;Z9q0!AN( z5E@qhE-xL$f9vx5QEe6Vc0Oz2h4}DZS&7PSa}8?J_>E8heT@z*A0ls{>~=_hqEg7- ze?F5=1I^$ZsnAGi_IwKeUhG8VSMVJ>c@LV-CS*jhFvTuOEaGKWK$9I+q&Wtm#%Sg58C(W5N%CK1uJe9t8&E#UrrJNu3UK9g zmcYvLanxevLDS)-Mo+*9eIq(oDUL)oErMO9Wz$LXfk{9&nS-Uz0dliMP=7i2(kgb2 zj^rX6ctHO1;f4E9_zegJkdd((bPdX-t5rGt)pXOeUhQro$iK~k7UXeG+TPcG@Sunp1rc%bK?KEn60s9PPAPYN|Qe82f5y zH06G>9!REJ_UP@+PGSD$XeEy40&E$rVW#YZAwc&vokdkf>k!R6gn@klVv8?p)V30r z6_-jM<~$ZSro0w-u6(9pR3=!j%oD}@n3)L^!2oMpTDBy`ZtHyH{!{O2yXqw~^|!ay zdT!8Lr;!J}fl1LvqDQl?yr^x=p z6T=d|&G1ZMY%fP6hOSB$T z*B?eI|7xDJ&}Dg$b3}jAtxUHvPow+EI#ob{Wxe*53;y$rS#5%T@o$TY{w5mZZ3L1Z zhfVOQnJ5El^na&#^(t`NnoZfshyMxKol!D`Sva}0U#F{7ZCvc3u%>JfUlv#;2=IVt zS1~;;aJhYLM!q030^piafXHSgxMWlceE$>K;qv?Co3T%__ol=AyyRUv`f$AmF%fbNAwpTE>;Sk z?%n@(-o_H+Dil=C);Uo1?u`-*oBB$#mFwBJ64oG@MAAkCBjzS1yz90eFybOHYOhUD z$j!ABh#KjMjDn~I^*C7zuTB7xQvjVVt?jLoYwxH7uSBj%1+&)f2U8|B;yUdj-u1zlOLh8xTJHem z_XzvWY{AXS0a=ATIWiP1oucyfDFHN^$(oPF(1_sllFGlepyeHuW+3Wu6T=D@= zpap$y!qLGLRMwKLMwTG2x?jB+Uuccb6!iF_5>)XkslwrqFpYXXURWZC|5Mer)5led ztiUJI&MZU>!A}@XRH!9=SsNN2OF6S4cmvt$pF%xR4e)#{X0o=!OcxEqGX#mZ8C99@ z>yo})eY*}NcL$zv2MHf1tSVq-(F^;7mPhUf@#piTmC}Ed_7BF@TnLRf1*nWDN+XKi z4;}Rb<_RI(u?Q5GNuDXyP?8X+ke0BBn}&!T1y`~U$OrwQ{K3e!rg zEM*;sp-M|1Jem8~BPs!bZYDg_vkIDO&`Ehuu66&0otj zlY7(PjU4jcW&CCMwGFu|@SMCtChXZb=S^Ru=QJ!H0N~dL$*3R%3)2Pkjz1%6 zR8z;E1kPnh(%wRjTck^~>~1{D`lJ;nFz6X8W%WVLYG+V(N$iJ#c?H!moy^?(Jl8K< z3YG4312j1huJKK`8g|y?AmUeREQ5nfka4V_>Rf(7a%6IOat!Tmv!ea|ZS>RJ-@m5M zx}!yNNPdnlv@xKN*Q+0(pGG1;pwXF-~IDt@Z(ZnpScaXJ-diGGAs1UJasz z4_0FUa3rO->*I%Loi0?VW;;%(R63nXM8@PK@pi)gkTOqBg@z*#y(pYdeZ00=n8>KEGrd@r4_8Xe1( zXUa-fnp(cln7Z%l;C?lWxbLvVcZ$dH7q?XmgC(_NXE>l?k~EzR&hWDFs8hLzO+dL5BR8LXdWVjk=> zrv+?!xLe)LKtrsfkHq08%(o(J<7oqMbjtY}Ip z89`kL#MqK7zLrMQA`2oti+x}=O}+NufsCST3fE}*htRx)`P9TOxox;9dgyt7WH0n7 zN+Kw=`sJ<@3!lvh(BqUz2_D(}1`^DBF^AHvnS#Zbaw~lmY^*aHXR;Dar8{=H1CF<6 zQ;9JjSO>G}DgiISOZ&KpncgnKH6bgsVI%?|EyZid_m(tU!%%zugYd&~M`{Uer3u1N$Nq19Sc|)#;_Dn&gbgI0Dq;5s#di1|7l(GX@ifBSZ?5aRZE$c&nZK7m{!f6 z+^`#=7*&pyEg&`5Ry;{}BzQ`rGa^%bt|koHsubTU;mu@MuVE6k>;<8CGl-V7K~ucw zX}^v>237&W6gJ6Q8%$+6tTZ+Lr9#-vz~?Ni=F zD&lcv$mcmq#;_V>A{HEpe`}1i5u@lowfltO6O4fyt@G-5M2E}5vwfZRCFPVK4+`fl zxTd=;;`&gjvM3>G>4ML;M325&kn`3;94tRkTPtc7@|M!2XPz}5?>F_pgEI8<)xZ6F zy8{!tVwx67Dw;`r^SUOr`N@T8!d$B%UriN{r9TdB7rhc|XYFJ?YW67K;fH76i4;cm z@x?N|k$d1|keo^%m)0hI!o)lFnuR;vQ?$Pwp$_LBbKe{_##oreVMWrg-n{sd&n$0z zAiA49u>Q&O62aE{RZs8rqis@M4LFYyIqK|6fI#N;AxWHCy%(M}xdCUM8`{7nWXmlf zYt^bVvNF9U6>T^g?hVz|4;6XH?xTvcnCtR{IiI6qkHGI^X;oyt~Ll%?3p zsgNNj6nQ_j$?AL**>;ZwJ&hU8M^sM?pzS(a;p(-j1pcNHpX{5Xy5out+StdL40kR= zCKV_yNY9lt=sg*H3v$UAw0K8K8|a4J8FIg@3)~#^t~?JWcfOKnJjB<@RKNErEv$*1 z9KsA*FBa7FIZ0aQ=Ow=lrn~SoG)0&ilzYAW|v*FH-^|oB~i8f79qq7og&-H;4 z8|+bP31x}*N+M_Dt+|j~8$Et|OQej(!JO;8rL`U8!|0-{le_Dy$1Ar~V@B=6Hlktu zujMHE;T7s-j;B7}4rW#LGc$v^- zzRNTq(h!8kW(RCMoV()HR=t>b5Ek&8WIzZ=T>G1N=-gMum1a2o*n|lQc!a3i5d}{I zatnI@ggmQxS$V=pExs>iMtYLe=Ljvw*^W}KO|J^#Xwsty9%AE&TwH@I+T-|-RJ@Oi ztp65OBZY#_K!aU*MvNpreZ~fyqw+jmT~e@VCI)|l3kdNBZ5-aWw{SUOBLiccYf%P-batwl1GpT~}Hy>sQ8nju*eytR>3B{a4*G1?9}6V#dFv zJQEJ-t1_biZl?(w$RYM}V~)hQxT1{PrL%2i%DLZDpK}OiD?gf_8Ke;GjYDfs+4@WC zuYK1`&rjT#i)wLcRA)zGG5@j-@K2e1h+KT_>8Boq>O&!V6%ZjnO)>PrN9{<&gh1-H z!xZkbI~4hA!BnZ2;Ar;Wv!44RvMJMG27{!jfh$D3C0gwqezo9%SSXg>i&kYrO(wUZ{=5uYTOC6$`vyM=pq z28$IB&YD5gHm`YrjqqFQ5A{1?LEIGq3(vvp4x?sX96AnzL4%TA_)iI`o_-&BXh6o? z<2;AYkO6d7b^7e$b)!Wl@XgAJPFE=89%^0O^ zB@vWA>XGa`E}A5Ce^zt7aIEsLKSbT(7(_27T4nt^ z>|w)xr~K%V%0fpQhK+^S)?d!QsWdN?2I)$p?2IcZJpMKN>c;_Wia4%ErMxE?CzIn2 z;cluE-@C`}V~$5pCJV>6qfm?em=Pv<>o^~uq~n+!;Qs{AVw9liXC+^xu$tR!$- zWKfOcwK#N9t288);A@}0g^SuSM2rk|%Vs-{#-Y;a9M=SRw7lteOv)0Wn__LbGNKDDPO% zYL0YQ(J_iTjY{!L^P{80Q7-@$4nG`9WSD4gop@Pnaruer{yd5J4t=m=@BB@|H^Beq zv)$&bRcu*T!QWk1MGD zkzy(BX3aOjoE?IV8Gl5=C+Ii)VE*V}Ttw-p(HtOm|4X#WqjFdAjy}xZT;l7uVls`) zy<*3~<)Q!$^$pPqp!xvOnwyc1`So+kT2Hi!8tG+}DfYBEG2tfgN!ylQa@7wrhEwnm z#@7oZcuIz3GQD@UnuEXo%GBdrQ^1QNI_^DLX@m;PuYV9x_!hY%I%wp&dcV@1C~y&x z$Z+dHjRp;G)IhhS-G+@}UX{PL7%f5wh+IV=;5~BH^xXyHw;N3asm^bopk&zxe|42# zEfS>&CQJ&v$}LyBg|@ADd~V3LbH>CiNtEiwC#Mookeh3TFJwP6qUoT^6YZW9vx!| zk%=e3iYdvB`k*^HTFE_t#-dO{kg=w-A#PmC;tA_iZG6JMVq2&6$W2EU&uSecGx6D1 zaq3S)e!CqAwLFw<-Bmz2AK~-f^uW!pKrwCpIO1lrlLpwV$xKR#=5e78n{d(!E>%!? ztj`gkRc{nM@JCXOo0AF0zytuUs3&k*J zzVY?hN@?UsCaN~YkqeYQQH-dX5e++@e2Mcezpo&d^`21qCLgZ88|9Swp2+ixW2*0}7WAI2<+}jIdmjLjRin<|O291~+D%R)niXF`WJ_9|{Q3&!!-Hw#7e_&}PpZ z5!jQgF{PiRM3*&=zh9(@4()0y_Hhi`B}A0<{;0j@6!fVBd&J{h@9+631;i$vklR~3{>KP1N_hI&m3$UX(Nptrk+iWDKJ<5{4zV8ZEn98 z>RQbk;${m+vEp&4#jTk$c7ud5T~yr~@e$aN!*!jGs_;AP!Sml@cA?ogU?*fM3Js|1 zaWun5In^+WG8#R9k>7xI@3p?bdy>=>e>8(Fz%^<#1erPtvtX98(T*g>ii{FC&=KI^ zQ1Iu^7=`37yp+w;#E*14RD5El=*1{=^dhZhlu#j-xpqtqv93c0#CjF{?;f$bA?s)N zq3t;L?L8_RDUNq5+8e5O#I)e&7&U$;mT1iZrzmwXcW|udLfgM5jKO!q(t0t=TkDPTT+UszTEmyn?_xmIu8~h+MB{eBH8f2 zixVuUbe*YsXLYbJmnz5E@>9k-FavG(ivp#b79FjAbF&^+6<_r7=Pgn96TozBdV^LX zo8Zt2aqQ%OzQVe6GoN7st*>P5(~k!(M3cZyh)+UxVo9=gqbM_hdejuYfL(U>732oK z1;1?r-&#}9#Hdpb-!uebf5b#5ChtW7N`r}EQ)(=f36TXp!OQ1>zr&}?F>8PQuKzO@ z1wxtcyb~}`0UO^6+yOU!_t<%l0p{Y8MQRY zhxfm3BZO5C5$`{|tW_DF+zgw!w*yL%oJ}J%xORZy@s_q)I^6a7lt1bA9EgR&3cZ_ilb)-cv zYO(2ZIToebf8*QMHmK*iKyZpyU)|R>mLhAG<~n1QPGn?bhr-_(F-4pL#E16S!!=R%bUpU3P&oDS=;8 zk&Svi?`o*Q$&V?9#;21N727oLI6cl@E_j~I7PL7vn_xW}9Q7lmdSJMg+Mv{qFM2Mq zUvpYf5WVJ7m`r-hoy&82J5!^%sOWAe?uLyg408Hf=gFt+nO3EEk7gUT*TU4}8vgux z{q7ypLcsmA*C`Ey4+TV8ykztC3ce6sXbn4nJ*D@EVH%6fs7T&XsW(Q}n1sdX1Kz2F{d zBUFMM`y|Gx*vo^Dq*VGWe@@=N|41O+pve>H)AnawT+#2Ks+}* z$!MaOMTU1n%S{}lYyUbZetmR?Zab{K_N&7AIc;nvhmmxDzQ8wP9?6Z5Ukl2?=sMhJ zF&^y?qo8>G&E`Omjqk(64JF~=&pZJDj#-qjcxy3vU&8r#Tn<0l-5#H$nsKM!w^z;6~>48BE<9~QtXd{80Gi)CErk=bckvW`x+~qeZEBd_%Rla z3;R9^{$otCAlA8oglu@%#~#Ov7Z35)X8qf!%X9f3Yz~B z*8RC4d!}I~qt8(RfS#16{D!$w&e_{Ucj@wP2QxVg!s}A?7>Tko77|VIGq?Bt?Wq$Z43of$CkM-({j1H#^%HaxZ_f0 z44JM(1)L=fD4oC7-K7)8ZvZf1Bt{M1lZ06g(`qdS{=MchTiz(`aUpkKK{{VZ~iJ@Fgwh3ohvaN=V&i^cJNYH%|5E!yjOs>j#`yLRqjPiYP699 z%iyKFtc;1|9>UPb-7Hab)qKb1f_ld)ognmO-b%&{01CGa(G(eKB))t{wT|zgfd9 ziUZ0^erp?vVA{Ow-85E10Iq9pN}J8LP#bi3m1L-6=JIK9{!vAh_DMp?-JorqyJEXG z@0py^Emvr?BJ>*GE|>mBE`BTL6Y@B`t4b-hMn}k(D+=XfD;}6m8^j9{b;h(= zSL9e*71ys;^QC=L$9H@NW;~UcXx;J){HPKGw3d&5xeA^XkBn zEH3!I4RYI@`$H|9%lUAd(z$?v>K#t>IvoeR*`k>uRErg*lWPD{9S(9oc%YF;BA=2n?IJop)33is^^THivrj8*wv%fz)X7r{|4f5k-~SynIM6t! z47>eKZjTb#$cXGW_t!3?A1fvmcQFlR2zMYw&n+Z?+6g~*iKyU(BgW+l@~ZcBjh30H zyRNs1EUT>hfOmZM*Cv0@_wW5Yd^{d4HP&mN*vSlVokgq7>e|;r&C5wjlHFqJq~7$d zCjrm_YqNT^^Z3xt-yl4TWL7{nmCQxB^sCedoQkAzp?>kn+QI_3QXMX=zI<(!4z_t3!2bgbW<*V z-}xB8AEhQ7W6Idax2mB3x-_9s`fs{M{aYQp*H^#}<*BmRi3S() zof^n-Qe3c+Zr8GBwZ)Sng)cY$8hW>1&l>A}um7x_{tc$}oBxmeiMo#XjM_=}4sj~X zC}~H~-b%eObSLB}DXv2pscMuMJ>BU4LuQwk@SoBvz`s$9j8t2}=X$h>*`X=h+Wyil z&vnpheaZ5~aP*(Xj@uT;2fx2u1{WEEK-kjH`QOGWxX41Y1g_kAP>7KXrvDDL=mCq)&tzazG2jzI9082RYpRH-Xa;Z|T3B!C+vO%Nky+0ls|ezlKHk;$7hmG*Z^qfG`24R%gz z%BAX_-oGo!lXUS|8YKNa3a-x%i)<@^;d9Gr5;5C$-AWY>y)tp(EZzK>bK$|{lT)op z8-w&hX%4uvH#IsK;hm3bkR`8Ko^e{v?NRLaT-i(eKdM^}M-6P)44)Tr%r{P!hp@(a z4zAT9N~N{XxUeeAT9D16!h)r&+#c^|>#c*7ax342oY8IvF@rqBa&0<7csr(Gi^;sV zlR@B|+NNzkJXl%VV7|n#w4}K7=ErtTKtvZy8&RXT?p;S&20B*--}DTYFXQcbRWsVK z`i_Z=7^)^7h$Qv9@WLfguyv`*1GL(TN1B7#>`pHO-;m3eSR$?XJrV@ zWjS>}vq!w#~4xW~u1Wc>FK%S;;O! zl}eE#lbtk@F|l(BPZ;uRaBq}-y;u_wk^+pAYY$UC%QxwQo--`Hfz4b^#+h_>wGCiW z;oeL&uc=~d5@_{%fGv}=uNkgK9?nsCtSkP+*|W~Q+UkGPygdG!!Mwq093236z3Fnk zQHAigk|}_)=MoW5C{?197jzS@9ryQdP3lc}>8Ga%qmAZ-(5QLf{{fUhYroah&a_cK z-0X1zGLP{3k7U78ESk^4$j-ACW{AXIdq(9I29~j=u0!)2&D7zM*Y! z2)GRip^8K-30grtCelNb1`WzvmL|6@<*mXwrAZPP;uEXB0Oq6N`I()m<%bU*on7|) ze73tDUr13@Q_)J3D%G?!`sItmJq}|2++fZijipI34CcG}%p_|#Gqxn$c({IN^I(1P@X_Ypm;U$K&@8qw5q%YBYb-Z~^g zTjtGbFtheGjXktN>Bok(pZp+O3?qz|VpS_DZnIVk!zY<+X!i`+L4xGBGfK$l7t}H+ zZ$y!z6wY?H5U$e6e9m{NrBAkGJum42X8&+-eCgV%qQozY+qg~*@gy*-pO{T52Tv#Vs zrZ5#3LL_?g(_eFN<4tZ|>=zsz5;(_Q&&nTfUll|z6@vCKttsjU8Hu_|3}KZ=A|Z^( zC0`&X9}O=MrJmc`tTT)RANdFVt{(eIKKnQSuYdl-)VKdzzv70+zxtp2o9k@tEYD4Q z9Mi~az-=;?&t}_svx5XF zv@k7AoMH$mF@k(IX9)E{>n4W%XmD+kcfl2QS_V~=3=&!v=Rd%~l0~~$csGqF_hb2G(GsfUdn%u` z5?uwUR2jl56Xj55+)3Ti2UynQnjwcj8l00VJh%1PAGN*uZ~q!@y!43!Z2s~8?(W^2 z^X(7)qd%SeGG6*Gz6<8J&fU2I-}P&L)t9zH%FTU_4S9{b+xju{E>lmG=5L&=73=9| z{gfRV(Fj{*h%qIUZFEReLQj6^k>kbNa`WN-p^M8+cOuL5dk@+QPPJN~CBM zIsW6XNgr_h_I|;@Ft=+1WT6tbSYIoR2|K+IG0U4biM-i@n{ulY`B95d5%>F&kC3}R z8r%~as%B7FR0$EwNPk)yJB%$p8eFMpaC_?) zc+&sv4R^8p7v8)$9S_c*{aN^-|H{w%HhlhHAvNRU`R|4gB2Ck}z3;g^0hVUaegRCyOP zhG@I^lW#F!aCEg#AYe3cJ~t#jmhz?+rJ`hyjY+>KnS550AV#GcVs4f7N-qPBuS;Wx z^oN6uon_qII?t48$B+D7pK7$4laqEu5i_c=Da9e_EBP5Ri! z+q~1`V;N-mUNB^8&#;hPp6?;HTeTvgl5{-!ojh}O<-o%|f(P81*oSOeGlEGN#8`@_ zqeveqp~@SSUM9#^B-TDnb=82a_c>QNz@yx-+FaNhc%y<5ZU;E0Z z{zCj4|Hl9MMJ&JfUw;|>G!0T#`<#}0!JLL}7R(?v(=f7UXuiW{CW~h@^XpksSX3Ko z+9e@WT3Mg|d^!#^xAq4%IG{BfOj>LSR>^m@L=mMRYIQQSZYm^8!I{cBlMlz?38Sw| zqr>yzqrt^H8f}zG7jT+Yz==m?LPo!N`oMpRr|XnjsA(ZmC)|#T40&kp_vY8#f)jIb}7k z_Y2c-xr7Xv?=52(wlva0zE4X;Y*Le!RD$GEBWRkIyG1U5(6*ETzdnC7IDbGRn1A?p z9^M%jq`S)T^O!@F16;n&5B^>M(-GeLmwWtg`4@@D-Z|Lk%;mucIJ-QUDR;~5YB)(7 z%)(;jPdD9`WwU6^vYF<61<}-hCgJPgF z{-3|(U;J5~_&5F4rzoWsW1o}g`2k~~WkVxows0`(9_zz0Z0VskZ8-005IL$0XFAna zB#qc^lRkWDI5<6YJdeGBCk$Zh!qUti4qIb0hDu^Wh&xIWf5)-)=OM79>mT1JQb=09&s7sH`_G}tbs z(iCZm!hB{sO{KrX6TbDI``FL@z{|JZdG#B==W`xk6(@V0?e*YlK5&w`Epl7FFmX(* zh4Smn*iAj}h!9FKH=5?Oq>>>SkY;`UQJEes9iNVU!A&p>BLK`|FlmdK{rphUo|WVe zYTo@YL8Sz-f@IlB9Ar{JsIhzi&d(nWRyU-nl%}ajJvNP|8Z>=el%M!tJz-j;sXAKj zbBZiDy$pwK2WDd#wgWxGek{ZCxqAAEl^+{h+q1NFVjU}=2PKlIq?M4ddh@+^jq^Kl zb+FhI(4hsqy~Dgy6EZBAK}Ah)gh8gba%nCoHOj1|T~i`wM@S<4xh-7`ijM{_e4mnv z-BF~W(3CW`qogX8rjH3h)0Afp_Bz%3VMI2vXoaC^*z=uft1(;K)(_e=G^nl2lC5GW z=AA?ngpnkYOs8+ZY}!1E8 zkuOiEtx!|VSWSi{W3^>L^g>BiBZ)*9gE7d_kH6=bM<=pbj=cahkl+&XJ50il;0M`O z?PFph(Q;I)=8#fQGE3>yh$il(F* zDorU~I@;s33w^s>ZaUpgZ4|q~Hu@+=G;OObGc?1P%-d*|iq)E2w27i(_s~QWIeYfo zta>&q&z5_E%nnUpK=1>>18F_5*xZ1#XdIO;78t-cU$_&jHYN7 z(~@McEOnJXO-7)09)96jrprT_?FUmZ4cFj4vLQCiXJ$xUzadRaL{ORvmqDwNHXsU?z_TOoroA7A4;Pk37RshLBGP6g;_{6bEY2;tNX!H@K@+z(329| zttBV9U=b=q=9i_hLx%W>`0;=J^-sR>;B0>Q;NElSb0d*iF#bG%0nKUJO-~*d1kx&Sn1E%W>n; zus9FY>7NJ8DVzeqV3|bCkA{q~P+U``iB;Y*QxZe7vNbu(+mPCk1Ict&^CWJ3RDAwd zc>Lz_baOU8%EjsC{PL~MTUTfI?w?)VyZ>PG&iTa^)Xq|hQVMtelfT+tw==ylIJ;~o z6O#-lY6$z;My=t5jHLznv9dHw2#Y#H%y(=j$=rr9J^IGW#^tnh7-Xk^2*wud zmte6u{4g!cn^8$3H1xbjMA1chb#&J#;k>1CsFoo7=cU;oKP)`)bo8vB^m;vCuGjO` ze7S!3=t~NnAH8*XZ+`I3+n3pN?req|fB8T7-S)qajwY^}^YuyYPsrTO{m}Ae zK8G2Ctu+kJiV@_j4vqVYBHKeE?T|4g^V`pJe1B=w^w{(-VG`uv-JUTMjQQL!6pB*D z6ZPSmrll>U)z)O;*7F&n@@_2oDEdc;|6dn}{(qb|-IzDC(|hlnuHW3eae8^<{lDa8 z``!#3;ibRm@A$XEj*NSQY}U*77llNL6LG zEZ%LWvak%XS8?bL*_(x*7=ltXK0mlj7Y8Sr6g z?tCa6OMB~w!msw%`cm1{IR&%VoLM(PGm*CuA22kVR~bh{iDAOH!7e&Q4Heg+?Wr?<*}~QCg(-msUf7 zzbxe@+6R(-b<3l};juse`~Hb9HC1eegw1Cxzphqfw52sOO*QkbMQccxTue&dh%pjr zWF*n${ja=eEbp(NX{gh@dnfE?VR4t$@WU9!Hi#CHXyRicQLmQo1_gA=8TY#O52Pazv9rr z<_!E!awY)SuMKQ|n6@NDR{k^z&0BrNna^B{j-gm0kqoN&Y^5_OAD89>M0v`fC!RD* zElhT!5qa0x&$7nGwE4`mO#P6s!fZ0ZCA~_546`OO5(!NC#0dwR9JB*-`Zw>+0^v5_ z^Dg9b`|2s*qaw}XM1Z~^nD{;4+Qf(x&8CDY^f`UaVl#&vN0WnwFMCIz07jfJK zv_*V6H#;Qo7I%uRp2SAjWST0aRWy?COO}XGdAr(U)SOL+L!lHw{&ZZLu29N6p5*nE znS5a}L=SBkL2Zo8XGjzDlQ&J7vZ1-GRSN_{n$ZZw^o=hcqGraxQN%QEZ(!ge-KEd4 zsv$!8JrjLug^JXe>k%om>RKqfXjhU@$|3E!^r^${>eRvE>WS8*wiDf)Xe@imXTMOy zRtEFgR@iSnlB>;S9aJ)eRH8{lLS?K!zXG+i0`;_*zO@c5;H>1`p3j-g&)Sw@PpKs= zLQ+T_wxWopmZ{Q&jV#Ve+sU3k9hbfhW^Bc&^MZTpC;9M-xjlS-CTwOl!iF*LnE7eK zg2sGjoH_5VG?6zAA|oj!3CT#7Z-3$@xPA&;MVq$y*1$l3Ez3SOQ{Ga|cLjwMq*%L# zr6i*i`AmMOtkX!hh$5ViZK(@y)ky}c)sxI;GwhBIj4LT)%7Q&@NoK}u@|jUdF&sfm znj#rNg`|;$vijWTvQWc-hG90T;+a~6nH!~u$dB<)nTOVOQ^Cq#aX_88UwuuZG zqtdh@nGQa^H(;B9FQ#n*nqcs*ZESffHVYe;(lW9{D{rJ=$WWe0H|vA_F3bw}{5@VXa+OTHC7-2ohXPWw;RYWDW6C}tEtIc-1K+rNF zaLwu3?RBo7x)XwA`AnnIB&I|y1XZI+5tr{>A6wN*d&&xtfP#m>n_F?=k1+6BnD+w`MPMUJus~-67X%g*Dc& zB#7Q+G#cbH!{lHUDq87#KfNR?QIK7PrTe3^e%fV9Yd*79kBA#(wv|uPYG|)R-UhFH%_%6i3~>QD9WYPN(elk)*=|U@|5uF)>l5f8cl}D=Odp@Nijh!yFz0+ zlvI9Pq4F%DOHrz4jUGt+AQY;Xx9Ssx!@q>TCg z=3A=k109^t4a3qRi&QDZQmF_1!BD9aLJmnKCQ}qm)M?Q;*q=^Ic9q(5>55@&_rmG! zpbA$731w%k6;-D;+FDdis_ZF|cj{^-Ev7B`TuG!oIK~?0%lTWXS9y8m>Ef7zD4$VT zu!l^38&Tdz7DZ_2$oHjkuC7i$Fa?I*mU_U*>f{0HE7#YELWsGaE||yi!}?`Gg(_>@ zQ{E9XDOsPAPJ^v2Xv<2Pwkx_hc>#*J?6*+oh2!AGma!n8lg~f!{Eb0d7(r-UO-2Z{ z4rQ&Rq(VoGuTM)7WUEf^ww0HwyJK!Qw~c1Y`%7Bs`ONxp!z{XjD_Sa|Qd;cfZB16% zMLKDi?L@e7f;42c`WEW>$T^<0Y4S(PVjn(ASxuCD_NiCPx=qO3ap}sElxWeMEJibz zT0k7iQ-|%ia-K(RQr$>c?oQN)AhqZ{Mc46HrPgxhxW*lt3rXIJex=}IM(ZN7W7=#6 zmh%-3Gk?o;yrF|1DP!wTnm9ftXf!M2M~k4JbQ3Cx(~{mbD!MhBMwB3p=ZB@+AhTDe z8Sw44^6|N;X71MOrvQv~gjiEgB-yq#fAI!E}1| ztJ)}jjX87j1^Z8t1arHYY8*TRrH#G$TmJ{*>|h85Mx4Q5+;dM%6) z&4^%V(XzB5pEX8VH7m7P6p@WY3DdWyjEIRP@8e{QOsh-uV1xDX>RY4}8$XKKysZ^P zl0hi5-=YYWEs+vYD2K@0nJC$bQkKm8{J69yB*obixa8X5{x)NY%@?1gm7%XFoa(Zw z6-p?RLlM=K3tG?0lvpcM$Y^2^bb0y;7ULAh``leE`F^OWB3Mu<<>LntP1zwiAqXO)tSh0E z5@3s^3Eo|tY+#;No?erUxj%oarSh4zP!hXTxw3__$|RK_CgSdV4_aA}bR;Ck z;%trO*ld_`wh5*`i-!j;LnNgymRezzg@O!ih#s~4bdaT`#L~3U0=clFC z$i5XP2_TkUjz$T!^_dYd>UkqE`nV(Sb*)=IvvgvKrf}qZHnnTBNrP?CkdTN(GSd4P zM-R?$Bem(zdGeBdhz&y{EW1W~G!YZW7MJ!yfTM44c=8R_RW=IW|q@s|}jQgKnW4V00 zOjf%+-I3QlIzO^7<+CMkM5!Q#@}VY@H;1f}NFt8LZsS}{B8futQ(qa;kg5}cmwsHi zZYwcWGD{1FltgKhp7p$y&g$l^)TM>gk;s%vVm`N23ma54WF(9<($(q1D;(u=jWA4a znlr9F*au7`3T|HuX7X!AilHwNY7#M3YPFKwNJ43)JviK0&8=fsCp8$y!^+F+#Tdef zIQ;<^rm52ED>p5a#ga}d!kAoTl@!sWN#$tX$skRX3?k{Y`oiaNc3>G>x>u|SxkCl+} z1Xov(ytUGTtCo*-s^qh2BzB}|qxEfPBNf}zYVn0vF-E)!K-#D`rz_{&?rd{6jg5Zt zMvKWbDhy^H)Na=5qNM~`#+@HRWu1*#bXjo+o7!#BZ(16kW8mv_yox5Vm}yv;UaON6e$J&&35MOoohkE3SgK5YC8>U>VtN{j z>ex`b$tqGCnc*j>!bMghktngVv7PbhPrL(+q+bEVK%zAsOhc~6!GY_Fh?w8|#bC6o zd{$B=LX=aUifv7EoT!km}< ztRS)w!jrD~o~Y2OOJQ|Mj#%C_i2NN0)T)ycuDSH_U~{({MjBc@^)WQ2hG!C7hGo)q z!gZY@if~CxI&BjAs6h#mA&I0(hJ@0C!&jcD0r9+1==cy%rD~O6n(=ZR#D~?vZV=Cc z7QUuSOSM?&Nwrj~(1@$) z6+Pao+C+qvu$C^xl`Krw6@)}0(L|C6lE`%2eEJOuB0KE8(*n#~s@(La6=yk5K4C=Y z7q_U1p?Q3+wJe_t#-v0ggMP`>wc`rL*9%Q9$I3Vz~||11YD}oK8IC;SDe6 zO=FL>vMAKLRMIaR3MKDV-9nb8m1=QP;%w#GQg?zM%CA{ihT^?sz~ZbR3zF~OQz-|)Tl|-&?XDzTG@_aR>RgZg=zWD z@A)0Y@_jeXzIKj0bK`Qn{qS3Z<6z^%7|XQkW~^7MObgXU3nfY)i%F_j?ntFBl^7|? zVG1EXeOkh>XLU;8I#-^a4m683R7^0c=5q;JJ(5B!hU%16M>x9l3YU;X-YAU`+i1#0 zdB+L~nj2?-@^}3J9KGkxy-%-^<2$_bjrF$x*Ku69E}>gRWKFA~%A-k&WVDYZCCaF> zHO9iyscX^|Nf9yoaj6C7P@S6K@U(KCO;B3FFn60)=@FzTgft^5pIeD#HA|L7XPPp0 z)t(v4`S{L~3>G09Bhtm4i$C#OzYNE>Uq1T$>+7<(apTtJ>$gra@=cxG{c)T)x6php zh}h&U5urpuDXnGca_Lvyl%m<4khiEuKCW5Vy*kuKfV$HV=NjJa(_m6{&KicAS5 zrLvT$E-ty&O-M;6S6CuUs;I#*G1SKzHZ=l?&CBK$PEo!O!%tkh)8H#*{4I#!FW``O_%lp6e|NYzsi#y*v z-uU`g?qj0w+&>tLX>mBdIoo)Nk47d;mX$Zss|uMnMg3A+ba@l3sHpTV;@YV>)U?9K z(~57#H>&eAXFshxJzbjHB{O4Wd!$v!l=&>2cBL$~7IZHO}O53#13%w*oyWHbaNoo;;X*Z!8@^oH#Ze#c4Q{`B+jS=oK| zXzn~Gi|Jsw`X=l3!uzYan*>Su!>LkXG^MaHEXigP(zLXot5UiXSuI0e&y0mUouEDi zjN`*vGZ9-ynpP^^25a81!EDwhv6WV; ztX^FIzW?uk{j}}lkH37JdslZpe6+Q_IlCB{FW2+bd5q0+tj1W4#WyvV!+Ck+g@z)s z{8~>dn9o#^(knsGSSm>g(r$~+XAZXY3G-91=-t%lxAV60{1lnw?oI@CQK4rEqNNY1 zg=RGtN1|N~lcic^<onrZfl)fufqNzAtN^iqiV?19hy-OWZ!71@WR|?k)rXIKM&5)Vk%WYfqr>$#e*1s$pZ@XJo?Y14 z&H25rt*>q^P7XKI!BM8wj&Fj;dBxKe>^P{k7j8PLrZ81xg<>5UBZ-VMXVAkN~I#d{!jnrKl`Ph zuK&e<7_?rZ#P9tSo4a+AgOg>kh!cX)qEytxnvQvc>lBMC>Swh|TpC4$DV4N^4axHB zm|=D%R02gMtwgLZ9^82Oh4;Pxec$%dc+5}sYO`5)b9Q<)FE*>wX)?~``oi-yUsx>5 zil~vbQ1ulw5>gT=J@~jaXKA6ltzc4#@Lz8K+r&CNfdr;J4>NM<>4IuBF>5)c&|*H< zZ}K+Da}th(D5*@LO(Yx_q&!uVZ7is=CRy?pmLTz@4MOdXWWKuC9N)Tg!i$Vw?&_nY1eFAn%AQsTEIBX>D{QDOxCXm2i#= zOEpB3iH)I*yhAf)7%4%~gd|0TNW>r!nN>$J4I@1|J~?!IaWdUl9j+EPrW=bzmW#=F zy&O7tJ+tmnG>H(KuUGl(w`yrfsWi#TluR{}t5&B52}K|~0_Lap=2o5d5a*Yb^BU$b zm>MkiXUlw+60L=(kT=y|^jxy(N2Ux_YKm6V>TAo&8-pMOgT%aB-4Rhyv0Vt+HAqBY zq|riCjix!Cj!$M|?8kkn^VHf+oya6X{yku&Xks{YpRt+B5Rkp$w+L^kjjrRk` z6p1K0(-b2eB9U}zLOK#jV-QV}CLvMtJQ=rL_;BqGq6+#UTHXaMI-g~uv~rvtU8st%N6^DwbEXfAXhYcbiqdK*Khc7D~Gjy|?QD$yK5oB3Z8MVqg)1*=) z)zl9ZEE1d=R9U6kYThuB?_@oREQsttNo~jWR_qvJD}k{ikwzjS$Tu5D?mnGtcNURY zk}X+czfC_M6t#SI%*PrLl#G-&wM<4tfo%u!slyZus&g8Cd0u&be=v5_+$M>|67y?I z7OSMGX-YLqtUZ#lBTdU^&7AL=yo=VJd`wsrSq#!tqS9<5lBP)5id_+jhzOB%5Q!ii zlX2T4J1vTQ%^OtI@=S(`9tx#Wb>WIiN#eRcxm>D5rGy}yk{>$g2)R`!0eM<^eL3bf zmfM&SE%~fx3564_vc9C#Tz-v5T}YWYRo+tfb!O!|-w~O}%4anM4XtPqTZn`thIAkj z36UTXiPWgcJjctyr?bXn5c;KLLK9U&=qXle!CI~*6jv%FYuRe%kR@A@63B87NK>HG zT;HBnp5GseO-`)M-RZG(8dj{xNFk|q%nTOnc)LZp+i+4*o|O-_RRy0x0DDKw)fKG29Om%OLc@+L#-TtQxZ&GJAZKXki4Aq$5odl19=*BGMsg#5W%u&X-3%oF$vvXnhq?t0<|&3i3DC zDAgX}F1Q|TkyR=AtSnoyWJ$~zFRI!DU#1+rulsuwY)RGCgxqCp-S5Zl?FvcJ0n~qahRY8Ql>gL_~sge6!KP5$|8D-KkUyCM;1XhEWlA(biNWLR76O_lVl#GFwfA zju~Zhi1`ejfXb8M^7gp$<6(Bky2;&PZB@VhVHuhbQAP&EL9%X0N@3fywl&j2BQkAR z8U_<$@dH!a3DJ_al2+Sjk`7x*hZs#WwwjP25h2pa@W2(7k^8NYN@#h@BnnF<60(Y> za%a{lSCssqc35t!L^1)C!r}l5z~w27I;&SlHF7(4ucg!^t3N2MQw=>)=%?z#?wTqq zZKFn{W|i!kh!t5)jFDhwSVZ%ui^(xu8PRB(wI4=Xlcd$W&EGXo*@>59%9O#p<7veP zRca_Cf+Vh0Q`2c$%%wObHia`$trYJX=MlyQW|pU^nC>ednk`M4K|_Ol&L0nrl*%Go zEy-IW-(8VMR#y(yhAk6XqzbJO$&8$7U!VWh!m-iWH6!wf=P9D1GMxc&Fyk`b2sKaI^S_7 zs5Qm%&Sde!mNGqRaV~KQ<|9{r#8_uBLEovvgyO6GD+zqt!Pz5laH1XAm|O+0Ze%cVYG1lcpPin zO@f^_TZ&eYH_Oy|P(qpWMkUhLnkpqanGTZqG0KRF=DQ*L7~^^8Gkpz0^yJqh8S*9} zwL{D3b4p6=a0w_tZir1XC6f6UsUZVObiV9>Ip$Db7Yr+QME9 zLEBK>V9OHL%KCU~J>4XPWa*ZxLWoMJcIESD6WOuEMo|l6)+R$UVbDWUv(^~dv~ADY zW+-N?jjYxNt*BOLlwO+eTkjzf#WZ~@*$F+>^m!vCEFo|RqC--h z8!-RO;`vI8ps-~`MG!$#dQBKY6^&%Di|Z0uTm^@+DjiA?mS3B<<$LpPWlKF~)9(_bKrSwxHWymV>hN-RjECe^MU27Rl@yuH$ zD3K!QW6dU}^${6GEY#?U6`Z-;xIS_nHnQ-%zr~{(N+JDHPY^AUpcSo-LKYfST2U#m zghe#q0VdU1Kps|h9h%X!NxpQon7+!2RuPq!xXNd)H7=4Sg)xz;wI(6SI|>nc>SM%e z5Yb#BLW>we@|m7L%$w54-!*UI{qw3>kC5xc z)svbO5fvB^^Yatw2k5F!4S3EKb;BuRC~P}!kToQqjVAs?T0SeL6;&*qGIiRO^eCaC zrptG1o`g(nGA2kIHbV({6B!J{FpFTyo93c;KKS^O&0%gg*798oRZNlyAvh(AkBDV8 z{m7|#W=-B!^l<|Lg6sjF6zC+z{^ynJCu1Bfwb@RnlgV2JBP;RbvyT;VZOyR8*s1A? z*)*7FDVfwY)3hhcplLl4ERu}c2_hVkTRHnfad2Eq@s6?-J~L(F0^T7G_}Ibj#PQhEv>j^V0TDYrzFPw%gXD=%Wm3+DR&oE z>m!3HONypinfj$s7e%o|K7&kDV^}E6)GnC(J}o2*G83X?QX+3HpUvAC(j({#`Mc)a z{rFnJgk<8&d=4S2QB;D^26Ifiq$d(mTD5AKl2a(RhkxT=j{j5V|LFM$DF{_3Amgxd zzDW7fX`?~37I~`$nWA}5i_j89aZAB0JC_v~*DA$?)M%DBB5#<{R%1z_L8tCi2RSY!}7i>d9AM6MHSX+>QNTHX3b`hW1RbN?Uz zk5@7qOjoA>x6{fsv#~J^6RYWwOj!7kf~cT~iL^DLBG+|!FK_9`+p4h-)=jiuM4De~ zxEchtQW7nQ<#UoGkwmma+TS^j3lH&pJv4WNs2u0r@?Ayr(h72I+~qBjM?<4U)8yd|)`8hPHeypTqF640KQFQBBZ=QW=@q*R?;kJqH163=QFFBKr2s^mg8aNy>&anHrojy#M%ax3L#IS zl&{rlwRUrox<$o^Byn??H+jo?*w83#7-P1Aq~&K2N*ZLw%sw&9^6PxY=Hh&C_fyU7 zY>!0C#PU#5Qz(>Qt5KTNrNlH@T2We0nQJADd0diUDymaJdl)Mh*0#ge5FKc3iv=a0 zp%VEl$(vfKsB|$It5hU~t=z0wJIUyW$QxwdA+5<<5_0tCTRlhq@FJ9JDN_w;HRViPW3*7q+aQQYhGYauLP^upEb=y=jc6tk@|nL|Y2|Xbx-$#qGYmbE z&!9aaAzfM|lBO@5ShcKFcvg_l{NBZF`(kUfLZKlK|8L@ol4Mvn8vV}g1Lf*U)dB+4R zmYL4yBd@PPzA##XmUj_UrY6+K^+UJ#5kn#>YeDS@DkW%I=C8-4(I8cw1dOMp7Z!56 z!FJQ;%YVyO$;Te(1P!XDA5|{BcURY0T@~!+kY>dwA%sc@`XvbYY(mh=nh};Elh~6# zJVJlR7{l{R%WWitSQ2_FAweN}u#zfH?2(naq`{9`1%1fwvVYu9OM5~@bxMNk*QKXv z1raQ_gN3w+5y_wQG5n&6MHzA-$x^kf)!3SDAxc4Cf<_RT&qNUkt}SBFBx=bPwy!s7 z)lB=l#jd_2lG^wymw1Ai)k%O3 zD;7?j?gne-ehSW;Vm-cuNc$Q!u~f~p1QR*sOqJMHi!oD}AV%~tCL&2hbj@263FdE& zh?e#+@^{Q0I1fI4I@ZMC5fLrI)VeeTDM2Z1Br{5BJWT3G7iC%3qb1ru@U)aWnCfig z>t9yR4@+$^_j6j5+kI@U(jJkrAZkvOx02ux#^$f1YeiAMWK$~N(;6+=4x{C5RvNX! zOe=4M)<>frA|H|6YN4sP-6O}bg^9H6V|n|iW?0b28?9I!&DAkWVM>-bZD)!|D<%E> z8GX2NK! z`7HTNBU?lvLtDny%A}z!V_`Cv;~Aem9-YYNgApq%=3VHUT{dc{W^S`6nPF6aU5>Un z7prE*X{b7^I1iAnPB!`S$m03Foi+~GkT@~Q{2Ci{A5^JiVpI!tY2+0YiF_a?Zxkj?(1_4Nq9yi$_IC}OH!p|#k+tOcOz6vpFe7{X{t3Uqtkk)YS4=AA=}S`D|INUHvp6o+CY}&XR;i z%=dgo;%T)-Gkpy51__Zw%S^r_@-`KVOE=x*c4)PyilxSa^x-!&y-0+#+FZUfF(s#F ztX3=JZTr|llr#-Z6WdJNnh7yt zRT`Zv#PYzZH^DxulLWg~K20fVWH7{OQ{Gv9Ov0#;AV^d`FN2g4RjNp6qA1ffXjK%A zXkjL@Mk_?dMi?<@CFD&$vk4(0Y$_JwV?JLGeW_s$)>DZ^DWw^?n6^CG4AmGZ+nTm& zqHR;J(SVi02=@?9yZH1&(pm*=#qr62voG9wh7hd-P9`~Sb6 zb2`l0sZm9cTB#M37Dq(Ps2PNon28u|wN({*j#Yb0iCVEqlt}1MPVFF#8LQ>AHA<;A zMjKRr`TYa;)C*Wxk|jvQL#p0ipOED)QV%Ylb|8dKzOu&flct0 zLjb(>Z{JjS_j+*fmJ4PHS>o%Or3I#DzEdw~oL6c1d>89tkj>y%4~QLi`3oKs3V*bz zd7gpXO%`BpjhEVa7jP;Hg+V}FFwDZ<;@H)lmc>YXjX7rY*(VfkJ?K5y8@AB_w zRhEW(pK3d!%C7+ir;Uq$+>w4Yc&|&vTSyi72)7bT&d1uO*Ij>WsGE7~pn8yO8F2|5 zv=qwlbH8-y=K?a`CpGl$%2#UX$=_X9gPk;zx@?m?{cjr+G}U~HJ~w3yLpkyZ;b0{rVk~oKR1VuG(~-7z4P;e_Zghis?O_JrLv~j( z0IRZlfH$lVoYYCc=34MUDUDZdm96J+w^Fo9r4Buv^q|QjhqbQ3M;rP4`O|mZ*RGJ~ z*mYY^H=H#ci+Qr;F4O%Yk@dhk3&djPrjA#Phku}Z^E9U{-Qg);r`u%4;g=>lvPPk< z;Eiq%n-p3rMiB+|%bN1FC&QEYL6O%p96C-S#4d`wVyCF(71r~Lq?ESdU| zDXaCwUq8*fEJsJwOI*qpY1EDz@sDkq+D~;3ZXOk2;mE~1j%$ALH?+u=a253$--i6I zUBE2B&_id^S4vadF(0&isc&3Q`Bc_gN#}ef4gkvqsK7GDm%)xLqq{?Mz4!w~puJ&1 zau?#+edyS?_S?R4#wqD@ex(wIffsr@K^Th3YSL&T@}?K_eVFIttSpr8O5TqKLd>I* zx*KTFMkpPwr@okL75}*b*r*x#P}(^aR1lpY8p^9ZYwZqvN)<|vhBDVVoQ+$r-XvWV z0NM0xm2jsT+-7S696VLZ;Cj{tDtgvio;YB;iB+P{Rczw?oktNOS`S^iJuLFY_Z-t< z&fsM6rvuv$<(&~S1SQZy1CphsQlXFxQ%-@4E30U6;Gdt+f~)Os^6vfR#4fUia~ble zcyZ&p&|(fNywFJ5VeI3z9034z3;!xG(XA}(C%1L)Q-UPYC__3h0i=bJ1iOwbs;Q~h zSfEsbnjBo{PMd3PS#XSK({Z`Qa772&Y&}a<$#Ow~ek{LNyQS5EUMb@bepr(-qgOJ% zmwU66Oqfq;-IhHoSf9ld04!@~32$xQH6Pe~H-Bb6tKCmO-)dttRVuGiMFp5p^X+_S zOh^IcEg1-2iOO*|>l^O|1yE{b-niJ0!xg51eE=qSw}aW=;v-USe$h>z{m@Z}-ZhGf z2a^;-&mpZ+WX2$NUbVES!j8JVvvZeDFYrGz2fsG&EPUl1oh_r8W`q0SOi5kQS1CFS zk||d#YSwc`)U+d$P3#-INXdRwAVv-%stJx`h0j@ZDd*fl}^{S0Ylkc-RyK7 zgap89^+us^9`{_bM)O$kn1=PJOGeu>0_Xo+?z#MRk#O^U#QP6jQ4!8$T`$)yYyy7p zAv8I)<}?06yl1n7b&;KGOw@+A%1R`H+Lgu7=?`Ag!5G4h{~eagV)lPgmhZxw2!)SZ zXB?xsH(4qfW}azTu2@B_CV*J zpNbPa#C%$#bGNonxb2&U#}uxCFlmE-YNagN9w-B|ECD*Pspy*W z$PXIcXcVAoqUk{nsR9BANA%~d3-@)wB)TvAq9HlYsORktm}If^c}J2J`QXaW5C3yD znvBGO9bvqrUl!NkOgP}c1~BSb|7Tg^0x10X$w#IOPnGBDnA$JiFkq45~szXDllBLdAlKaA$#=8i!(J%1q(YR`ctW^hMnPn z4xNf*A^Dh4r6v^~MoitlK{EWRD*41tH|v&Rt;L~DZrd8A1oH+Dj*LeLa_`NxvdKNJ zPj5Zoet46%W*-YPFpYz@i;he<46C-~QC==2wCdh3Hy9%!Z$3H+-UtP~zGD36fv0g# zi>J)?TLESJeIJE>H(P}~H*O!qZsxSEBdx4)@#LX~187paeiXQsShzH<~r3s}&u#SPnyhA={^Jy!0t^bQzEx>Aw&T0e-Xrk4VjJT41e zK@gxxickewP05u9@%V3?(mw$U>+HzYC-gP>P(GzL^lrq~lwos6&f5K~qLK?iN1JH% ze>XlBx_%qk9K{JHuAIc^S42W0!Jri|i8|&%41q39i>-mZCYwpokM6B1-CvMw#=|WG zEo*&S_l3mLy25i=0I$;2*~Bkzc150)WUj`lM$(E4Hl4>6BtPT}SB(Uvz8tM{ln=EV zO??~vL^Z9Eaz*{`PaXMFqWYgfP#9D+J=IhxEA_J(UySmA%S#h?KHdJ+c7xB=irmai zC2169IGN;WmWTQddF>#!_4FuS{2OWn{$a5gqlDN#v0)> znWJ6JQNdRiLoZRopPC<$!g@(puA)82nGBF)piV`ZZUyCejJNFWV}4cj1SNE_e^`Y0 z>p{mfBtgp+zz%G7S}-JN}D$U29GBfOxZA$6jSiXls!dVV z?Rgfc&N(G@Rvn{ZnbQ-N_o|t)nriUz_+(T_Y=$AxQeXQeXL)^gY+VE!AiA#NC?$?LRL`>c-T9B3`mX(yL&)6?27zI+H? zEblTU&Se7hEE18a$whwr`LKf8QHKkNcJ!OOH@nHF_r3m*r}cg=|7AFRN_Ll)TR?D#bf}dFehxbcIw)Pi96JbeTQnW>f^USiVJoNL6G9bq&yc9FY(-Z+WG6Hj(7c`TjWJiaE44y&@= z{bit;zhe0c!-s_!F!4Vmm^=f4I<~p*yzsTfoNj4jJW*ZQ&-M7!Gtq7}5n0F>cgifeGM#)Nf{+h*A z{fflVksk#t=AwAa?{l|}k#A=^e@=q_X@KD`$DD7j^q5qP#L!?Lr6rV}G)+v4b_jPE zEWR4jCf5CB|Lx22?1)Wng}fBvj4Lv&#<_%bCGuJ%MR~2Pt2soMMWjsnP*jYS82QlV z${7cZxzHF(pkR^HLui0ff4n#DQS^f-lh*R@o0*e!Ws6DIMgt)QJAlcg;08YV625MXTCgzn8HjDTIy1$O+Ix!y&)mjv)0-->CA1C8%@L6 zhU~WRb>;pfca39(@OyHp5A1Q^m58qk51eb7iTGX{0xnvmxYeW%M|b0(X6Jj;^jb!! z4a*ITkkb|jg?`1zc#3bG6{+qaJE34;S>9pHzWes+yrx6jC)|5xHl%GGVrYRp-MKV;}ZZ3Q&zzQPOrRj_uSqQI=`FOd=$-{3?`N18`)A1lG+6;DBz*phpBDP8t-A?TTETM9ud@)HgVSmQ_k z%F?hx!9Os?$n2Y}{-U}LQO{>7f z!oLLwWYk%a34u!~P^LAe$TviD!EtYb8qEKtt5KzJGz51(&~0fg+;pr9Ju1go4>U9b zaNjOVtuqmhk-H-s3F#Q`O?}wu*v&*8t`NU)qgwn0P6-yfaPIclJLb8@FRboEpqys9 zgv@{8t0q*POdhQD+^!j8M+v%=>#JSEc}v&`At^=Pxf|#98FliUYrwR&02uH_^nfgX zrot~Kzrf+E-sZ+-z^HdkJxOsi6F=qk?=n20?uVetu)z;TeU>u{fMmX;Hi{G z?R3R<_xk@sdE3fm>TPm;j?gQy1~{*cC*C}>3zQbqCu|?UnsYR(*3y{R)0!9}J*2YL zaDl(5l@26ZCCWYonrp*8dE06@+&_E0zAiL`{>#E@beZLl6zEP{f2HcNYHB5G7ktk( zxwj&gMZP#KE^Ns*O` z3Fc;c74X;5YJ4`*>n6GBI5W+PjDTT1v6PE?;&t4Fc{l0d!`|4~zOu`2kV zq&FYMVgiVrieUz$E=ygMK<3QDv+jipbx(trf0CA*3hVi~;~sUp#;#X(boK&6Q*GJ) z4O%2sxKdV|!;)&G%}9AkgM*t!A=U?->r%YwyhYs%5Q~wh-L5w67hoUDf80v49nGkH158B5PXwBD_L?E)cfHs}as)rd*A!CD*~i=IUpe6=rFj-|l~> zmrQQ{JUpLG2fL#*3OfyYPV~U=3v@G{)o?~52lejh?a8@7&^dOC`EQ_{(XS>{GP72+2ZWT&S`#l4biQ@cPQ|B-$kk^UC2EF<9>2YU~*a;Ww!*I>z1?~_goT3 z#WRqk;oQ3ZI!)14z!P{_epNVEYRG)whTOdyVH$v zR2-Q~irjwAxr1X9S!jyF-BRCxB(d6}+DTNBN-Ci<$R81KMP^D-kLiq(5%!{rPF_zQ z&;x@cMuiLv!8V}DLO%pGPaS`x`c=NAYoAeVSlfYHPtc96Ig^O&6%P$ozeB*jmFm?9 z?2xZ35npjy{g4r1k^EKXp|>t-_Z)XZd~vfg@1jc$D#*ooz$58^%t-M`G+(?>Y#{$UNw60kyPNA_@7p-$$xiJXtn(($IazzoeB6SHedU1>*41>XrhkJI3P zz2Xvj8R1FKRI6KFW6w$d z|6YjzhRdwoumN-!gP-=<#2$Ukc=#vv+W%q#4bW#s1qhvDM}J8fY6<20j;II*VS`y1 znzU9ispZ0>#{PH{J>bwH8qVq;6>a-7&7kvx&*H7%LJRhO(ek93?BgXI!C9?R=n1?zQtdu#xBm>;j6GAN{yab_|4O~->d{S3!NwO5@>&_p~ zE}vg|8J%JNZmuMsFo`kn=Waa-D@Y!*ERtFA>j+I(0^f-}hYQFsad`4+^zY2gmA@IA z(nLNSd1!&kb@a8^ffj6VBc!U`HT65XGAL-S(D@>N4{MjqE~%h>8(Z8G3Zaq=io%ugl)+Dk`fNIR-_&R(^71lf7dp_XO* zuGN{4XO8-QnAJO~g4nSX2b);DLp!jburSUlM!P7~38$Op9sj*$oK)UTD$(1NT_`uKd!A3&-EMg&nn$?WvTUK@PWl z`09>OdYvfu7qP}soHROipXm3+^Zu0^J-@$>o@#Ti=ij>d!Y3#o_=+-`*Bx^EGVz8= zE4H*Rmuh5F9L!JFwjJ%nZrFHyFXVU;0#jjM=oYRc%f&y5_qk*E`hnwso};7XqXqAi zW9?auI=dIb+qOXNJ^mO$h4x%6huxz>qhgt)ymPK)R}>U__`YRss#us)=6sU*Mxve2 zuzd%o>vmU7`Mz4``3_Zbw{TV<&v;*E$msGr?bWOiZ_~*xdxg^$ONzR4=SvShyPV`4 z9_DoKzW+3oxXG8UoVOf1xLIvuP_^NFKTVXtz9=)HF`1Pob2oqM{VQ#|fKJY-Enno) zi1h zpqf+0qF9G?#IyQ_zMo$hif}Fo_)6Z-D3qj5|!fEcrXGJ&mlCUOa% zceT995xdo$&rmNcW<79W)fs@vK(RE`DJ4cJTw_k(Au}xBlXH33RQ17H3HvZrNrO5J z+|@1W%wwrg+C+}jM3&r|k2$j<*^d=2+6>*6XcQ~Ag@>B$#@eu)`kp(`4aAr=^zr*T z1>+-Y!!0{Yaf~o|hkxSVi)cQ<9fg(`e4tLK7#77+-ig*GW%E-+L&YGrvYZs@YOMazkV$|SQQ_vrW9;(p2XCtKkEA#SO6&TOtD39?Srx_t9jt!a}o9n-G3Oax9JeGEAl$|-g zm{GRVKPkH!8QGGCHH?&$1&lTXv_2(HHfxv3NPZSQLWlp96hH_)uhdhcP6iQAjH+%FsmW!E3X>SIj$ssd-gnOX^TvNPU#pFgv!8u^dNm6b zpVNgShd>y1E8toVE=g#?uY;T$$ohc+?@O_1Hfkym3QWDuR4q=+3T{6xI}k`PDm|F1 znf`geHF&i0$r5R$M5zvw^g^OlL@d{ssXh&cTptO4l+4CKYw7%gy71d*UkLRLk8`(J z`|Mo{Ua@NRx+LxtPdK7yCn(!qQxBd~><|MQZeF*+o#^a7_f5}RPV8z{jI)B28};FiZz5q!g}N<)yo?Y)K3aXSZ6;XTJ>} zeYza)Y&Whk9e<@|OW%{5nY1Uk5{2?_D~HWIBmsZi4wNJdq&syW$f?|$*9$tmJk5Te z5*tC8xpqq^Q=S>^9>o`PU-{KbDgW9l_dS_-oGv{CwMOo@tqTj@P}><6pV%xM=#V1J zz;H_hdg(B#nPSuI#jQia7m`5GT|vDh8^l5 zjLg>Yl^;M6>aZi*Rm<%acDP*nG~EofM0KjAJ{q&15jy>2*8Io8_Oo*V%`W*;9phe8 zI*8I_84bwl?4ZGwQtZ`89L_>WjEo3>-hFhE@lnDm5On(%S^uL`7XN} zQ@iu}dwCsQgJ0G6CCAXzbL)F%UXtNjlpveU!MTLJ5N_^xI`!5lvS;kS!$Oc(_nGOB zRh%0WE_`QVlyfe|NMu@%J}dG4q3G5omUnu$vr|NiQluEym|#N*t8(`=D^2hd8Zl=N z9pAa|y88S2@BJ^%PS0P&I!NV_gFFdEwYoC4D_2)Di*0c%64pV-du$G~_{qb!=1jyY zKjv{Q%@ro-n{2Io^jVj)#-*zYq#+q*px8R!QvuWPRQhIQDz){RzZX?2ut;EG6CX_R z1e$4eJc|y!aQCwTlKDGHU0G~#<#wGpI;2g|aK;nVsDC-K zTkTS2gQI@`u+(yb&0X%F?n{1g_!jX=Z1Tz{;XHGN6g(hUf8vU=8t?hoB&oKKE+*H< zWhtBz0tV89kj>{0z1VT{-o1|jw~x7xj_;~BDmMb5+k#BerR6q=iyln;ZSVErk}HT& znO<4kP)A4l6JFT6mB6LLh0j%5vUkRGYhjSDjmsX_XH-4sxe z9+-PnhJo56Ed%)T@18Z~gHkCe4N3f#zl&>CJcOY$h7a&30XY%+(l1I}YJlm%Z}dPt zM&it{z*d7d97p?MLwe@@z=aNz%f3l|!S{#>owG&1fS>pqt4wTH;y9NhfqAQCfi~){ zb>Rf*j8rB5L`hV2Pwlig)&i!%pEx0^MJ)%pc(ux`$p^=}wWaV|j{al5(Kl5kTC~`Z zfX-ai2RkRg_}0Mk*d70V0+A+MjxFQTDRHF=ynfibcQH z*eBk(TvB7%)Mryx1%lfb{`hot_rPL_>*KJvjyG^ z`pgHoG+R8SZGY}>-cbN#HzcTAor`SGJ^nz;-(ehN%v?B6NLH10nI7AEn#GG^jXByr zKOhEgcKpF@)qDIvc@GarQuNg7#Pjs!wkWqRVnXh$F5%rczSSF0Rl^KMt;nxjB zOw)GXpO4{#{bSa1`Nqw}erE-xp0y}5%L8|3dP45ZWMy~!#Un4hURQ+@lktL|u)vsd33b zh8j%v+vV!kPQ3f-a6jfazeJ8EFk=>}Nhu}qb6&^#ugoenlP4flx2&7CBj^2fWI)Ti z^LjFb&%c#(t)yZKd>2kepZ>b#Q*oMB!v&NlKccZIGuXezLgp}1fO1i37 zuPavs=_7A={_^8%wpxoSLFi-p#CTu6%6ncATFNdLy?V(0riv-iGu^*bbW98s%mQI; zt5vbmx><9)WVaF{`e_RNw@70ZeNP|Eue$om&rRm|IV1SFmfWH;$0O|TO5)jYRe^-B z7`Ga?l_=VN5>=X6d9i^+@rsu(NFJ{N(OnU5LwP2@rT8?l(+j~N*mwPfG?$oWc>S^2 z3v|_E>jvQ}nAx>9v~-bg=i$#VzQ*4oHt{)|d+v>EE$ItIK_<;OI;Txxc98S;zMYFQ zvR%?8i~4u%FkBWWEQZty?1k0fG1Rt!;b-Qgnd6u4R}+z5U*C>j#BYoV`;3RjV3;AorDfkKPAIBM}}m#zu<<=z9>ZPUt3edQW=rRtpvK8n@tITjg7O{6t6|tCrqfT9#)$T$RVEX9uD>3R1YK;lRw`NZ z^ujt8DL~^IK=r-mgBPyh`x*L?A3qt*+Uh_p<9WjaFVVH@0oB~FtTf_AbFzy}oOZrO z>Ai?6LCoxc4UScbj@-b!_+D*kP3{OvARXH?%rQ1QCfHiF6&9+{PcyF;>sH|CYuBev zx;?uEJNON$mzKf&&g1I#wvtel?HB5+gYVC-{3ogX?!666Am&k~KY9t2=^pAu=rmw1 z=D_U3!w|LZwt=;$lJoyGczLUQ8^4>O?UER#;i#_LVjm)-qVj0LMa#3iQcA2SRfl0c zKD;suE-dsM@heefqaG2Qge8Pl5e@ikLecr_}n!yx9 z80)C(sk)&$kxP&Y#(Isq)T+Y;H`+I{-KcTY0tzSb={epRB;@+EzJL2(&?DTNLxGq3 zdG6+z8eKxNN|vbCp^bG@%6z0%m<|t#V$nBwjLtKdxkyDwjDEx)Jau7<8>1)`)d(Cy za{-a>P`l$zSZ6KupKUROrs@VC4G%6j><(s@XSSJgBO(lUwEVL_SQ`qc@*5kLN&^l~G_I&KsIBOY zxYA^Ep(JlDWs~7=M^js{g%F`FU(57NqeXViUkF%?KDIIJ725qWytCM)ZyDx!tG0vK zVF-;WFj}0fs~$!o%e>#p`v&?onAqxEB%HY~kk?sd-58u`8w220FTP|>6}6xOS#cPJT9H$Fe@>^UYK&GVS7+|iLMX9eP{mZ+Z}SHU{_U54@2 zN-lQlBOzL*Z2aKK|M7yzYm@|XLO=<8{00*V-5U0S39)37=2cqgh^Kkf-yzT5dM(|s zI6cEhk~u{YL!GqZ9iXG*pMKbTxuhtl`zP9!p1Wd=lfST@z0Enu2!8zk8LQotG5i0v z@={2csx$`zq@r;~f;Th)kM2~*lqSU}Q+x5+Ue5llrlqWXbUY>R^KG;EhcKMd%4~to zjPmc|l4q~eHh>r6Y560_VjEUhUQ7TtJH|3*7mkXFYIjg-fjiehz?$LV=O(F7CoV7J z)87BJs?Pkm|2*GFbd86OVXf@y>1rvDdhpp|Qv2~O=(H@evEnzy-6uM(4kv5KP4(tu z**ro`(F$S-L%Amq7oL9Qd5l7|Hm?Lf4^OCRN{g`+%s;J=q4?;^R!qG!ftCnJ-jp;c zs=177T7BnW{O&?z&lcb1>*9xdt%bdf)q4eq%Il19 zz?yu@_PW=WvRZn-TGV71T7kOH09v#%Dh;PzT+Z>mYhc6Ug+9Bkxr@l%aZ{(EdeS@- zM?#iJ#VnZ0@@O$DMOU|fub}eUY&Ox|wXa%NxHH+x=1DOPnUOoh?UPEg>9+qB(0|8^ z7esNc{8@j1X5f6~n!e1G79X{Z^7!~WlCP9bNZE_gMQcpl8MiG04{5D`iuRp0;%jPW zzEeOx{Ufwd(TzZR78i4#OcR%ZkZR)!@hqUZ=NUo$H5x!GM0fa^u#(;QluHgQeGQt! z0nNH{;M~hUhf>MfUN_D986T{a@+EPO57KC7@BDWO4Iah@g64Z2dnk_WxU=l0C zT21luTdlAD+V0sa`enu2V!5Q+Xsw9!Jb>8bV0eEUVaRImN*SIN-M(9ojgbR{oKn}X z{!XuyQnxj*%GW?t!Jh4XRghSE;c+eY#fKFxoln(LF+~(^$pgDsrVKBV@@%nfb>%^8#l@Hsrk23##&)}?6rVs!0&nXn$>PvS2_RkrKtxqCG zgdkdd`G*Xb)=+n7gQ|wQz!SB$g|q&2l|0`9tKXhF%RRjoA@7VYPfDigXap{eg*2Ch z#le^kES^>&b*>UA8inc5(zt%nwLNi6XZz_yYxq! zJYp@j@Pl%P(vBZ|>gv9&<5(B ze8l7KoPo1~s@E)|zm&LZOu<7y@;qya4VwD;P7q_2% z98jfe5vIVe>+)WFQbJNZ2-=z|EL>^?gQaT-*S@VItlcOXcGHaNG&Cj~8^;}4o#NRM z#6LasG)^3A4>et`n&HfMy~5HlZf5Ny0QZ)-5Fj8fi7$AY$Eq<}kp4n*BDyt`q{!2* zl8u-bFtI|&?R+Ho*Q3`fTGA!o--TAYU;kULnkh}#QFs(32Tx!UyqJomBjKcmLj4z- zG`ooV7mWI1zGC_|kF=@()NKY9R8zXpyWd}=npRZBVdymk17+^M_{fo@0fL(sZcEgP zHB5ydsq8|r)!nfqG^ZqaJhbg+E6~=ZS;e-@PS94Veh}giF!&Mkz7mnJHRm{XmGi~0 z#6i$Rz$F(E7Z1*jE~&SLx- z^?ntxZnMM`y0mcn__WrCY2Z3yWOJe>_ywpjOr7H;zS23~E1@K4-@TiY1wJ*Ve<3?- zTg4w2`>@8+aDDdioty>1t zt^wqhA<3USJ^tHcH~i0U3ev?{mHR)%(u$)kiDDGPJR$B|eA-K<_Y%PwJkzR{P}Tfw z#`to$)XM#>FWOXe`p2iIJ_T)D`1p&U#?2S9$c=`^)>#C(H5M5`@>9H)9Q=z*Gc7bT zENU5(O&i>DFO6DJaLsgiKvC*lstsv!Q|-fcjl};IJ^u5FiK41nNht;o2(wu{HnHpg z=%b|3v|zy9p!_fu53{w4iTQa^Vbxy9YsXs|(~911m;M(wpf9FggGnOiu}-_%;2`PE zb&?5$?tY!LLA{=5Qmc=7vu^_~8I1H+@N#TMHG8s>?FbjdhfV)&U4WT!Eki?zuHCvJIJChh34_2?H)IcguTX1~(Kbj_&(PAsshUp%DNmRtjCYTpimDWn z+#4HJBPYKX7HqmSqUbMw@6?uI?q&)Ld!y0ankjaoU z(gvz~4kxK<+!1MpU4TM?q1oKmiJI#iV-ed)08&yt(mtmK@1eHZ2+L0=H##+83Fivs z1#i(C>%wiew~zwc>T5YexkiE|@I(;e=ZGaYPouY1J(000n|ZFlwWCbadVTdrH%3HM z>hQkLOB*#*!OpSlDV#2^evpT<*wv~+h~T}kVpizqOPV;;yBY!;dIDjUPUm;7{Kz$W zb6CtF0Jx4zpT!4PA3C1)nsxoZkT0qTcqu#yrX-2BmqxI5xM3XJ38ZZueAT`EzUhp* zm8TwX>*?YZKZ6c!qILS+SWml5L4_!y`PehDeR8@KKWfB!Rh==7iAR|3DSHWm^*B@M zdt05;ts^1Fr1-|J{XJ)wb9r?829oy3Z;#6|eSfPxyj?jp?;s8y$V_z`3x_1?_6~}g zk`1K!^=i*z!Iq06=1v$PZ;hi-`Mt19dPB6p_4z&KlLvpe+@ms{gl~}l21S}#0zHdZ zK@48VTb|Cs@N{-?x`G&3*An#Q()zEq$A_^Kg&<%8B|VRoyHG$HcM+>`oi?dW$rrFq zgVsZ2?f-BWKjY-By7)pAT?nXBZq$=w%AF~ZNr@R68&u33Qvp;1(Trj>wO=#eVe<>~ zh8wNlzwK)Oqg=eH_2Gb)A0m@BT~JJqTlrxHf#i*QR0}Xdz#NCMXKkQTqH_ir>3m>C zxp2>LquSHfB?F~xTvt<*8G5hT1&1)Pe*1e?Uhs_Dzh7%kjW-?rGdB|onFmgvo0x<= zA5v*kTuz$^>n>pBPSx1_dj9q98~L#uktst-YE;zRHGEp+-#5zsJ~n1QFq%|ZmY_8{ z^pf)6F5Qr7U3UTXfM1m|s|fFd>Nj_smJI4`0s(Fb2@-ilz;Hp~%3iBeBC_9#{xkUVpEn(sf+5p1{AGpOOG@dr2y})?d#IPa_WR2Y zo(SaKmi=E({{0`y?UEc1DsXAg5{7$-Cs!5~pE9jtMAxd%z$LwgF$NV3FXOq74*iu$ zW`@}UdIMcpaUPZ;k)U$Vo``I6aS8tAPT}?>@HWUS_aVNniMx+j|f2X+QqHdoO>u4H6`iY^FH#7{9KLrJb^T zm0Smbr>YDBKGo-7%nLdo=dTK9thVFtmv}Bw@4`-t0rOMS>u8$lVw5Bdy-qad+kW#8 z%C~P>pOoMIUYOTt`!zk@wJ6)0%kspO!t|KKa!P?qSW;TJ&5};rFi{6ypyn=~61) zVe?sN3FeI}$DlUKLNhj^)`KnO9kZq^eI@_Bn|%1~Uy+KhJnoy|v;6d9)Uq*JmiD7M>^9 zc0YdcP`LT=!+PY>o3$^#ix~H+nZYnnM@I=ZC4BcSi_9g00;+Wq0TR10rF7X+mlTF> zA<{Z%PI8;Jdo7xFXY1&bh-8yv>F%E=(ZAOo{dlxx&aL>JbF6uyEu!0=W0tfwOHfc_ ztq4>2>`0Gm_XPpHY&GJ-80FLp_;!ppg6(gFrU-Gvu&1K$OT$LPB>nW6vOC?EMAD}v_%WYNp=FLf`8Y4xA;=uCT^`Dms7j+|tAK z0|t@2_mk3xzSxOaEV^D3Mns6d+EN8+@GW2RR@m18qg=Gcb-ZK(K_0KQj z9~33Hk0PkU3h;jM>ORpN73WZ+?q|HBuC4{Bo*RB?r$Bpy$l5%$w0f?{b z?Qy|b_osO8-|^K$iGhKzO6pH z@aWsAUj^oO9u|iVyajGI>-y`+!oDYl9tQ?WpHbS45XLU`tI4KkLwsBUy-!9K6NlT1 z%IMCg+p90I*F~){+TB`*|h z^KNPCz9NkmiIJBKKw#QdBJ3lgwL5+FyaI8S6c>$fY8C%n^6FP>U+D+C*W)70q?0Ba z4BOh1OS-ada4t?X)Gh*SKA#8U&Ck9sDB|#Bzb<(Xc-E<3CI{vovyZ+enw*{{N ze(8x%2=mj}Tl%80W$Anp-cUPWXAQQ)X~Dk8)sE(F{UNC z26+c!1{wcJuf)^F?=1GBbP+FlhAZSerZed8Q~uZWW5|0kQ>g32`q6(Ap-mmspjWhF zNWhA`R*W^q60V%J_G1trYpP^HOaH=P-2b(TFhym|oh1ryJUvIesIb+wR$t+FwuS?V z-#sJ8+-}U}%&*NJq>cn)lSMUOUOUZ*?m^2M+2~`dR`tiSb6EtdcxH4nXfz!6xDopz zHk0^G=0$iFnMi$lkjF?reP*}Go&v^ z0kBA9DLneA6vgdZ6A3VklOi$X;5Y~j^E!RoZO%U`JF3)Pe!|c%af~DR`V;;|>!0dj zxxk3awN#xzO@;^K)#)}EeXpnuB{tBJXk~V!8__b6n^2hypjEyfzAzfDS+Mv{#EVap z#=g@jp9qL@Xo`j#sM22u_-RWU%`S@jos3od?7ia-Zd!mMJ?IurBT@d4e!8}<*KzE;?!hWhPF?vpRMUxR& zT!L~=HX(LY)ZKLstDD+lW7(l$`XUv67b>k{(npQD%IDveEgtCh7Yd*r`pZZQ?O1+9j^Y^n|WY_M{!T>8wUnO1@-RgwA*ZXe;D``(y$s(ArurJzUW?#D&j>b|~t zt@@AhqZ7L*q@mz=`R}>O_UehILOWjUXeJfX8!TQu=pc~^>N}%Ux~i|+!%_fPG%4`! zTBJQyae*ZFf$HegAT5P*bUpNZkIB!yr@iEeKw8%XzMd?k5ZPTVYCBpe%NR;00j%Z; zd=bde%an53_+%0&%ab&KaL64)gNH9jWaZL{>`*8!>*RvdkJ9RY3^l_;|8JkoS@`#% zjC|$K$pXz9>K56}nNru}A3L}5ql8r<#tebLmBA2_Qbil#b9G4XLZFrS|D)(!9GTw# zKmPrGPZ!tRHB@rVZH3Jg;ke)LteJ`#W42K#QsNx99f|?9x6HwE-vCk>~iPeqO^xoAMG?dI@ynDU^w+W zW=lYCKZ*$lE;T&hF#H}YRR|YyLtD_=2;!vD8prY<)@PsYgtc|n^27?*RYrDgZas=n zQ7z{#p_07e25+ua-tv)8qzRRH`>548GA{w;fmV3AWcf zpstHSyYwqeFhO5q(;<_~p6{fe#bPLM8BK|DmA}(JQ*WPnI`^q|p)`!gup&QFH>e~5 z6xOXL!^1P+ju6<9%YqfrYTvubHX5eWJapus3GctQ&5ckk$9awqOu?Jb!($7lFd@}L z)#dG8QCN4;WSV8R@DtgT?Z;S$iS^c3-DKiazJvbeb*H6q*9Hj76^Ma=7`dUm1IR+^ ziHb)oj!aTYpZ*tQdufoiMo6l#(W~pzohmrQym}MsfB$>B{^`fN{<@8)6!xnE8+av5 z9>v;dck&RDc;!^`EWdJ&DjlE{JdB2ka!>;6%H;%$I|5VOdwsPiN91 zmJj<@oQ7i(|MAKBld0q~e{S=uEf+b^qmhj- z7Y@Q!`DN_XNgM0=l=SX=>}cSR!dn@%=U*U{Ntmjy{brb~*V5hj_cCNs_RXi!=3iyq zThppLl`5uW!_8u4wkE@sRYnDtBp=s4a)_!-(VJ<>J}IKQ`n1~RhrwuLmG;-|aI0>tTha7n4xBxjcGD$#4U<{T4`w7|KX8 zmbm%~jMD38VkK?4X0ti%DJ{A2*^cVzDY9Oo=ypjJQT8<(rmMW_ z;Hl}mY9C-tXg?s;uIY@Bt>@i}jQ`mB5?*iKFY^ZevbHuDki6T$xat4(`G+t?uk-nT z*=+kX%2gI?;h1Ctik%)xA6czD5|kb{ocN4vugeN$z>FUFYH8v< z`h|b=&klXMD)D>17*mfC>L0zc(Qpcc3f63X)%CMDCM8kC zArFPyXX2vLLE01uSLT8epcM` z5LpCcYf_BTE`6_X#}1T{Of<6;%UIALDQcHsYJjA$E8}e~@(ZcIg_WVn07zJK z|0NU8-#Czo|5o?t->jY0xQG1T`SsUBXOC}mYl~TGnqKLkxw+Wq6!tIlB$6CF zp9Sj-f z9PJmZTpq`J@>}%C_TR*WU*+HH;&^W-GA4H3Bz&HmQ=U*RvoL$r^Y6C-{s(66&Z~cN zyR1%pe6Z{VJNhvtr+cmF8tv}%f7Jh{eca)^q;%eSGYF_US?DR)i z;F1_SI;=V;Jq{8pLko(3FePURuCZ+EvueL$N}Cotaou14$1eDH^Ja?srIQ@!7c~Dv zaE5%X2vFvBhOCs8r@N3pzJj0q>-H(Mq5Pz6MmWvfMkw4}$TTKC?q}2Q;@|BF85Uda zabGvMH!RF>4goa>t5fd}*6q3!KEBlXkG51nnY4BMp;Z-ZPGGhHZ+wO! zcCet{PfvYx*y0Frb|y{)Rse^#uOk+rb~70cSI#bBlWP@X=5+V-QwWunO9fi4x%L_($bjg1epEN-@o7fy?MZPLfhi}2BOUD!!t#M z)X)DnBhNv_QbgTBCB)e_W@cPo&h=qQM@Yb7GiR5GgBbcTYjfMs0PXCO6c_A_xP zJjwAxS~jY99k4<7RqA{HAMZ*#* z#zn0qy6t*m-ZRZ)sEqXI_2J(mJ4buJe~dC~oNjgBm}X)Gn&kW4XEp*Ql;0%0 z=#DiBxaj^cJ}zp7Ik41v{o)gfvHcuV(6`UiDZ6xIB65lL;FaM1_<@y^is!NaW&9^2 z@2_Lkd3T*v9-befCO7x=&5NkKXT?$#w!0YKmA*s{{C%P-d^OW^yDom}`qE>L zpyC++^3}$fddm*X4{!LbJdvAF(fj@lKk{cw*M$3xgrE1jV}J6msr9b#u^YLq7H8>)HUw}X-{9w!-9RQK(hw6YhD zrJX3EGy?^eZx1R+&P?N^ywumir%IWgC#8ySz;7}`D}fV5{oY; zUqTe#g*?CxffFuIo5ME8<_%o{B7Swj&C$^D5Ql${by{N9KfP_*k!V$x(2FwmTC}wR zjNY?Cn+$f%a0M;JEAOkW29bikALgg znPc@Ln4K^ng}~`)*03W3QbQuzQH*SK#FL*Y4Au@})m##uyo*{jNOw`qCg`v>24;n( zCBqH5Vkn{H%MOspk2T|Ap zG{tEK%9ufzT%1oASL7`a`cm2FbE>fm$rW$VtLa9Nf@pKyJKB!$m!NQ*>_57Ih5V~lr%-8)lQ|5XPpK&<3l>BwxY8nrLb2#LO z6h9E;0tf@7+>07txs60h0Lw{_k_8U@YYGUGB{-Yy@|&7#JZ3_3^q$TuXFju?8eJa$ z40g32G+7GO&+{xzb-&QC%wgUlh z)M8#nI+pf;ez=gZcVN)_EzI2Sr*`c0;NfaBe-#KyQb0D(W0kVoatuf~2pdH=*ukQd z7?2*i0k^bIT}lrqE5mS6%L%G6h5L_4g=fQ0-pL!7M`zH?`dy~{@0lpyH@n=*Mm1TP zi|%sln{0FU40c9HyW2ti`OUSe_;UJ;Nz3EwOXSA#u|ajF70FHs^@e90Cv@(5OIhdN znI4(zbNsj<*6H}v&W=I@R)k9`aU#;mCC| zV7?fUl^;lFgwaQZ$wr30Pa)S&m6bn+GP%iiBO8G>+=mL2HllJwb^c>LaU`w{zGw@G zCKdN$zl2hyIU=6P1_7)55DQ%Y!on!X_ z`hE~Bl%-QyahhTAOti2pm2EDpIc-8#DZT?Ea!o%D55~6ro)IDz= zjC!&vM86-kxEwLhhU@ufA&@A69e{+;WPwpXT&%r#50kY(L`Gh=Z2klxM6TiL4BP?K z&w!Guo({3RrZ1egdFflX)cR(3S2}6TqiRo|Nj^lF`wK({%~tZU%*5^t6*K^t04$s+ z%5N!IAh)Elt_@p!Sb96T-ZD!*Yhg42uc8eL826lcENf(g#R`mIgbH!HEXDfmKIno# z)-DQ)hKmIkcJdC-J($^Z9F_83jOvi2%&Pmdw@~84rvpn^(lqBc9dWp|9RA?pf zeoVNOV(&=duNN-{2ipZuSl-#naON9t#ltWj#?f0z`4_0H@6)d}{raB`58^*>WOH1y zhBW>4Us4tb(L3OnCreR;QWTa>#`Tp{u)(wtETt_3vLKCSb`kZQvOk@6o@^JkFp^cz z@%{MteXyTt?szyV!t_68grbd~-*$~Alq1_mpQ&q>qb*xPX?A?C5{s7ew`r@lE6*07 z@hP~h=EO4z$O9Vgq@O1ztQymAR1LKwi11sq#t5binpeqlK#Ghulq*giO*5k z^m$kA7Sc>rwIm)k2nSPNCp%Q23)QCr!(D;knu?}gF@(~nkP4}A=KwH1jvuDx3&xuA z51Hn^!Npj`sC|&QU^@OLC_z|wGX<<2oec)D;v!TCIJ4cGL$f-&_)Ibu^i8A9H%Fv`>QH{2hG9;?710+i z(8ByJNT^2rV0kR(QrxAd<4w=r*L_#+6|9TBxlEURfYQDmCMhRp+U07h0#A^5U{*c= zh~>RfPVF8Q0l8OPB6hgd>j%-i+-_7(Hdld1h+X&Qg>R)ih=1|*6B&olL|a+qK57H- z#}lWd&G8em7A$i2?MZ0KGtGhnKfnH&)9{cj;ywaLvl?>eW5f+hP!r=Y27nY2Q)?hk z@#FsHoE^x16pR9JbEvuJT_tcr3_`#LWms)QXDU-u_JVB~j?SII>o44~Xt5Zep4XX~ zUjO@=R#G-p!aFbSxp3Bc*N{VtRK{x`oqHzj#0&NE-g}*#10`(o+ zY@nn2tRT`;z$t8jeuv#-k-gpf?}n4;HER)Bhp)ZjmA|j6m3JmO!B>OMSRIcl-1&H6 z=aj0uiCoAzwX#Pio;=px^ZwTz=Kp$8AjBV`%5%S^fbf><-HC< zn|t>KWjuNoOsLNDoGKozX#j3jCaX<@(-&%8aU{vkmlX0Vb=PhsIy!i9JTQljs@;+` z#re-aGNia(P$IQWm(ct`KPWBbeW-_63IvFS7!07Y`||7nRf+9qn6%Fk#LinYdbN5H zY&s%bq|?;YyiX{--tFv{rjzDy7x`FkPbt$A)>B?Cn%@_zihY7zZR^=eSqB-M%;%pP z(=%+LC`!V2256XD<{+IfBxV?8t$5=Cd9(!e_bnw^zkbn7>W-BXS9Iva&H;D2d*^j$m z6q_{5Q^-`;cB=GjkwU@o+-Oz5g%{CdwcS;phi}?kz_@?Xr12ItjtuE_sAf-Z^|nm>`Nttwl+yYMI+DUQ3tKm~7y>VZoaDUf;cm zQ55u&NESkv+()rG`YdBQ9APDoJ~DI;$dU96_R6s-@M+FW4Z1VG%U5HUXbvD~ZC2I_ zD-w6eBBh%v7pfk8p9TYnou=4rx8{r$78}duK8>L)hG7|D0rXI71yCP2!MvOj$Fhf+ zJ5K|=t)92|!aYN#!a=A|s_)uQVfY^pVl!hRue2%@vSn2t09rQoW2>q?bA^P%?HrlK z&Cyg_67^SSEgG#vOpp7KyJYcMoT0jEwm6{aH=t;U+=v?oh>^CiNq$JpeMa--j{#PC zZn<52q*2(<5Yg#tBm0o1(AD}74cd74WXaQ?--vh|=!#Ayn}a2uI!M-YzwqN9=5SW6 zp8~i5acrU=-VTsIKH39QK5MsJG&514p`d!QNo71Ydm(r9@Wb!cf9-ptAID6V2a^w% z+vlmBf<0?`kOSa!B4UtB^DIS5+kzgFx=^Bw>FGFXKAMMw>nr%vC-KAz&zy`=A@5pL zLoqdf3e+((X;R?mls|E^C-Hv|b=_~j-_xGwZa=HsV8{6J4&t6n6vbw}t(&F>rzqH2 zTz*8$ym6yr3~z3X{&yN-*NWQd$MqVw;e%V3{J-Q!Wo=DeH2HbSc+b~UK;)NlOQ)@;Et)4*zvvZ ze^=~P)Es-rdhpTNy7!LWELauyo={q@tVc}+rX~anK5iXhPZygmj4 zn!nSfSJ$TxN^Qli5{UuB-Gbc&Q;U*Yu#)so`0KpyYPeML;cR!`;s|)-;~53U&sQg= zgU>Zh@AdPFANM78vPCSjACydtkSc7$u6kx>72GCM&RfgV21e?+M}~&~zW&DBH-Enp z^UE>yaIj=&p%kSgR=8w10AVZ+&X=$^J|q1J$0!$Aty$T@`i++T>WYO{3+D9h5J)zy0P#lS4ogkR_XT=9kH5Tyu&sFMpWZB!}8A>W*szx?o?qIuj9f6V2Mg;a%r z7BAJ?d~#BKO`tS2u}BA(TeRCo#ap#3qg{Utps2zIM>#{@Utnws!pE0cn-Gp?5pG8#0ii#tHV8ReDTf~N-j zo_Jfo`p8%b)P-1DoQJqAJVhE8@=(U8)rCw02!Xv%=+j3bv%CGFYp!b98pqVQloMGW zk$#YHRfPDWK{wN+wt$X#APozuW<(@h8iVR~kGY1Z#74J99qbcNwZfYjc?? zG@;NhFiu<8V!V$N!$Cu(dU~iM;q>Wix)%@rFu7nrqLeB@*b${&n2vcA13NJPV}Rb; zhGTpIDJ@}1eJI}ZhV>P)6hw1C7UTT`D~T=f)%7xQ!n}4IdmZFo2dSGfRD|?M*e#tO z^M0Bu5qEW28JU%=C(nLI6j^-cOQm+ZVm>v->s)m5BB;Pm;;uU_atMj}>a|%L@fR2L z)JSoEQWo?VAjT|iGzvgxAF;Q@kPOB=N}n}9L005tX@i&y05_E5V%2lHW(~wJ51Il$ z5+~_gF97C0*R4|nCVAE^ak~Ohflunb{o>|t*t9P^%XX3s37R@z0l(VA3|%j+&A(hz zPAe)wqB%i2j-FDkcjCWh$wNX<${CC%+P^kTw|7AnysiK19wtG_0 zKk;=@=H2GIvXdKDc~D2SO$qZR4`?Qs{m@n=BU6BHNdF?1<~0CtR5PeV9_2?q={y;n zY2Qs+UV*Goa4fg&FRL6DtTYQfAdy4_a!XZ^$NXiur8^4wX2(Ih56wZ%yPLay(b9Uw z4tk?YurqhZ1Kmi7%JCfp47UDhgcq{T|(Y1s0OZ$eZ=L?(E6Gmpecy zR&k5?g#shHwHtcjtDOWx$6aXS3ShFmL77e|2+Tr@LM5qtLdO5y6W*AM{7EReLTt!4 zHqxrd9SM@2u`4+_hpzE5^6U$TBfP2_GNkesORt;#oF99CC$qCKW}|R{fkKfMP{PrL zIBqmZi9~#aJj_O5=NB1zbgUtUQ8v0^CngUpmK&7zwN8P#D5FrF-D%srAAjCIGwwc27iNap78nm0dXJEHNU|Z4G8eq+ zM{A*&l9%O~lTXVD`73#Coj+|qT5%LsYhpq-`J+{wODwus90FQ5L=6R)|Nq%__l|j|#T_N9rm5+w zepvG-KMkH;3;2v{6Z64PV30i!rYmil6&xE$qh5EPcnibf;KTf%Ht2)*nU#S z(}|U=m6@>*xMXm0w*2uHc)m+!maoNJv}?${)%E7?kmnf=aKi$`)ffC%ciQ~O#O{Y| zR7bKMilM(h%-Uivj}J@-E<$z!*@60~)l>kAjB}D7dmT=0mXBrVMUr3pOv7b-$5yT` z+is!z$cC3uq@GlwzW=?K3|{f8nfcf35oL5`8}r~pS6{VUwP&U-X7x%{{Idno zO)8Fmu)lXoh)w$j7j~0qkeIYSfnH%H8&S71uzfT?{PmB*@?luKJW`jwp2pto?61}z zGwd@IqC3bpYY8&DXY6vQZGczcKX+y4ex2W-_YOR&z$w6{SJ}=B}`1&WKGS5Hokt}xLo%8bD#vKrLD-3NiETJ)HfdGFDd6= z2j;N*yM8v~Sg+wp3oc87`Sp}j(xo(jLYhNT7qBcEF$Sg3e`r!pVQ0^u*V_S?J1%jo zH2TgMAk8k#HKRgw>1XAjAj7`JORIk+^E^uB%gx^rP~2#1ap4nw1W6m7OSoT?&kVjE zJy%g221V5oJ>2wP|MPjzKgRVeD{7TZAhk@g2nuZsx&jKKL?N0X6_Ip?d2@LSj@-@M z=gy(U*G=om1u=t-F~0Q0X{KtpXi?(ui9r`JQg^(sQR_O-Mv>-KV~_a$ z1epozdy6mPnfx_g+wv)cFqs1YK14DomSS!L&qMM%-#WD0Nx_|D(L&5?a~*o;nzOUG zE4ZjG;-wXcm|^pZA`**)VJIkZ0|wb>hg=I`%FD;5sSqY4gVsP}mFYEu`ji3T#t{wR zPRR^-av4?4!s5vH2#szbkB0XKJZG@KTwb3vQ)x4=E=sl8OqY6IJ@dX%&D%fIa2%~| zc;b&N=3KA!%g3yNuItf-mzEiEsBOebn-Ua?H6PILjqD>##F7F)sNGZC2@A%EPNW`! zmX>%iI@EJo%WI)zjoaS2g(_t0HS$O&3F0oXWdc?2WYo}jqanjgYO1Lsb#$F=<#jJ0fW2%A>mT}umgleR5M%gAp^zy zwSXd;eH#GtB61B-wAQv%gGi%?K5QouOqOo&W9+n@3~o z0+>)GE{DAwx%2Y>bfHWIbsUg|HW|CJ?wsw~N zKJ@W8=Jvhqm2p7o3PK5ka#g~RQG$~Z#Rx;fVU*|l3Ak0kpUon*0SBasnne8sxRrD! zpC;RZc5o^VG`KBD?v1cJqr^Sj&hM+RI(xM|GPXA;;^Z`ZjI?t;a`op|PrKZ)8mM(s zd6hJdEgKOoT&YpEO5+3mSs+AO1TTh`-SN1K$J$6j_B!H3m#s zPgHN1&hDlLSLy~8N5G3;C|lIi;9iY{b3MPalVa(&)>q=QiE-}~QNRBpt+0Y{1Lm~| ze8@y=RqSb$O6qwnq@%Du_Pb?u;_gAPzNeHlD&Nh)9{bS>{>823OeXJiB*6Rfn0L3i za(83f+2Fbw^;l4E_vF>mBg>9pRqeMH2A`tw4+o> zM9(dz<#WAL+Yk47>s8|1XvUY?6+bLy(L8Brk&I|lM&baF!kBEoRFox7aTg;WV)+Z7 zy^F7Yp6WI^o2hVJWv-;S)Z9WGG7$gxj6io;$HTbMhg<>QzhC%_%!|5MCY4!vCykw- z+@!@{&Zmykt7fdE)5^Q9azDR*zxsJ)2Tbv&bI1!V2c&EqWB_om3|X3oJOqlhg{J1@ zAo$n6Qf%0O0WFsx11!p`I_25a<-&~HQD~gDD$&~*H?lIt{-g4b=N=oRljhhqNBZ(a z>*i=o0C&IXGC%uz$a6N_&B66LPHSLUyH;KLLz;x%>Cij7ocAk{EKN%1StNX;bQB=Q z-0F9sG$T+7s5Y(va`KocGdTs=i`w1vvP%B=Fd47U_5r-N($?NN{By%kKI_Wq#YMJ1 z>}|bA`Ra$y5G>LCj4d1r4Ijnrlv!Y7y%o?UhA|{3bZ25)-mOAtW;N~5 zdxPQ>H)9cy$q%$Ct|t9%fxZM9bAxB;DxhG}9;-*WqdYu;lz&Hx>Epbh`Fq zQcEQ*yRzn>^Hxi;`iIYH$b8+$+k2$H5R7JG$kLszY=j?bdbzv{L(XpFbn_mq1pl|`;=Rd-L zV1K<60C1V)5K*P+2`gAQb>UogxYiOyF9;`E)$jcSChR1vA?w{ovjnQq{k9n@KW_b* zXnC(^j0w6~YlJlT{mkAj1pKBYBhKz%zJ`;1T{ErhRF08o$yE4eaJ66DdnV(Zw(`$@ zwKvCpV;^#(P$W7+05o7{zYA5OliL>JjIl~6B{f76f*_^2RM@6JD(jQAi;8zGG%DXj z)9zJQcNfvAHFIn~FC%_{eNbFo zeFN1zGFA*66WU61aJ_WQA)#>VnM~3X!#_O#!sWJFlu98-0NDo24Sj&$nt^<3SZo45 zedv&l&>M?G<@HWj9zR?Cb5oKU=>2`L+jTp%@7d-=?}^k$O>RTE0q>j)JCfi$dalJy zL0c?+F>?KuhQ=;b%#1XXsoB1Km)MmU+2G+Obv)EyVbLB#)+-Kza)QKLG|;KPwv0tvisY{8)NHGj~tLQ5r+E`^Jo74%kpn?i}+#wO$ve= ze?vV%`5@Nrg{|TPu|)@+>#}F6ZE2Eq)vB$gmrp9bv}!5NIA#H0wQHhQ`-1uuQ~8f% zKotnk8)j(!jv~T*z{-fQqoEfDMXi3vos&4Zo8I*7WAkYXYY!i; z8ldP{YzSZtG+)vOLE(+Uut1I>kkO4az#P-vQEmR{S5h#M3G)G>Y9^s!6kPL+-H29` zFI{^hX2<2RWTTt?j~{1u+}owDfP`lQg#s=oJXLWNb+Y}@T1b3seg z>mO-r!Dp)D!&s0-#f4NBWD4@@+p8AtNnBfepx$JAtv(Xd){@g~CSclV*-ACVUaJhz z;@&9B53Ihpgvubk2_4(g1bH&{mO8qXZ0?zFtRMN0FW__fJZ7HTs_4iZNy{`2otT|3 z$ObtEd%RJLb3UK{zQ9`y9*yoP%a-Si36V)i*LNh~Pp8M;2(*RHZJ2#irU3N*mz%bzIB3rJF6 z1zIK{M(2mcjCTFVVuEW=MJag~-eW7>-P;!w)}O0gOSDUc1ExW=4Op$5haT$}cA3j-Kf_iO0zyr7dcSG5njPm{o*- z&yxx9LNv14Z!bxbW4`LZ+a$a_shT+wcWNMgj1&_Obk!-pJ!Yy=?mu{begKfPNbcJU z4H)gT^uA|uYGX5{!g2dFI_#WialcyI)h4l^CYkL<%dQg1e2r-J>FDd9^ZI@%c=*Dw z!Bzi-^9ROui94>m&NN$n26En`5J8s6YI`7rA)h>e2d!4Jc6^04RqS}3H=vjXG{v>Z z0K{MBMz1VSAT|#_=bU+~<$nLsQHjKcSSkdMSIalHE?l3ZJxiH!s)wdS5gFlr9IMr$ z3{`~bS$~6Pcy+~XL<#4xWfK9!0BCwidh$qvq;90209fDbxH<)c>7Wp8MH((Vj$uQA zqw@-(LAAkjW0<2oOZ9Ns-YH@Sw_0TJ9)Iq>V)hlC5)2iwvfI;2And(MgRNtVvKJlq{=M<Sh< z?9T<=0@sCTjE3@;6Zr!i3Ut)LIC3O?GNlTuL8TT$Z)?1b6c&PY#_8+)3sh@SD; za6Z>FAW>&XG_a}KJ6sgNZAX;Tpi?HN_ix+Y%c-G0g|1O&;8Lr0SMn5Psc|-eKp94| z(GGZCPzw)H1I618O|r>{CIUJ*TK|h*aGueYa1&zIqw*JlY=swZNGl2{hyihL6rPYO z{PYWdhU`3qp6iYuDRE%*3yi$B6G_xu{&Hc!N#_1V4UAz>a-hzeB7GcWN}!!j-TVJq+>ElonS@yp-t6D=BftmBOlH<@+-U3$d6l$PZyWI< z=Oyc1L^$H$cCfaR%Ho{)Dvc{~f!`eWJkWqZ?kmYqz`H`{2fF2)X$7CuL{&#N8gmQJ z%Bu>z&K(ShXbD6$JH%mu)t)%T|9Z;kMM~!M0;2WuDZLP#LK83f1EIE-L3Czp>g!pR z(Jok$tIj%SG4oyKdgV_g#>e|2Ta0MWzv-!mi}ne!OKA$R7j}e@iiVnnHNv~weuaos z#Vqph98AMYzDYB(sJqF_%iGaXExjAai>Mx1$ywN2ioE^L{AZaBQS&k~eSwkbJEimH z;JpG+*w$*bcsgHpD&JX;SRAzXrP1L;(z8!*shnI93-8(xzohPIhcQ#Br84@-hpG}; zKt-i&T3@oF^CcwLF07IXBFJtMm%?UcfC^UF3eZw>#fYl05cyM(q!!5EduVXg7_*K(R}@ zF%!!BWUOoA@hlD9r1O!=i`}AT{8zF2hX`4jWG9=(Lvt*$K#?>tzJl)O?-}nuZ!uYk z|0Un5$v)~V4-0-|&_=fJQ3I6=u=Jn9KfiOqbcPCBsghNm{Ye2y%cVjxtXpzu6g_zb zhJ4$}=`u5tnb82(bysti+>H>&7aL2++wzTiw?Et{9#n;{`I&jHnUbV}nhYAAqK5x_ zw%~L3x5KmMrIz3h5-KyS&0NQ3Z9m;d+S+e#*2(Fyddg+IZnVt*gDLEJ>*IZ7_Nab_0~aH!p_XKl;ens0>@SafY?!2!Cqz)Crhr} z>UlVP)#Djb`62-2QqpBCD`Bl*^L=vIylA4Rr2b+H+y|pAGev;k^1rhy{O5l!S;0_+ zoLm!baF#5T$J}+Bo!ba*2KO?oL^;0zg|KGr!AyOi&Kr!T-u=q<&wQ7Qqw(2+mBif} zhE%WS68e{KlkJv87jDM}hh_jl9W_%A08;$b=Jh!zDGjO*I>QfEMJm>#illw$XAb@e z`bQ~S%;IVge>r!73{+z(YN9-GxJ+$FLIy3q+xnsT9A~YizS^$QL)#_a%H7Ypq~56c zirYYATem`m6wsPr2HSPKNr>E{)=l4z2?`|5tQ3_s05m1xwo$P4&#toxWanOZ0 zMzff%o^GD>F9s1yXa9!BwNM)I@wlp-}bs`-xpQzAzgEJnpgSGf; z%=_ex`&W>ujOCV3k-wDevnAha4+i($qrHDwGM+vr;+hq4F-)YV&N@`f@HITt6@+jM z)^xxWx1>P1rjYVtqk$56)&{(&W7jRepeA=rne*&)c*az>xAPTnEXb@6(#tiC@lG${ zoKqgH_aO0p8M)V~xZd^t?#~(il_cWWW-tBxbrnPn8|ImMxWuL;J zPaidhX*;uRWlNNdKBNIkienIM&Y4D8DME!Xk64f_km=<<(wZgcTbY&4rnzk?>_)iv zzm^{h-S|ysJ%5w18DVoTlXs+4)+!c_1zjjx->TZu#{UR z=e2CyHA(g2k9jc^%?lO4>gM1?WstnMtM24ysK?Ozm+Eb65@~Wh=1($x7(pePYM!gL zh_=e=mLX~Ij@Ag4i0$eK63U!I1P0$L*qpu=cR!^T#J(f&HK#X+J<9)Xka>pvF|^H! zHsX2BxhW&UO)7=wt?3zMzuMj8*oYfSDQ4+t+q?I%6rl}D8ryd_BSdhe+ApE~Zy)6Yej0w23g7XPNX#R*>g_qMmE zMk6j-dWNMWCx$mLG+z4@0UVq&#OU$(sivHA{P86ioEc{fra`e~fgw|ud9VEOL+JGz z_&h8Ahh%-Aq775gYerfZ~8Bo9HVhLZ4o#Zh!vf&Ng{pV6Q?=oSZW< zn6FhTx0OX`T&=cc`X3+CWVGXVDpiV8io(&K&MJ9WZeAs~WCWi)d#{N+1aH~Bn4Lnq zE(&!$Lt%A+H@ibz!fWb(gscX$yTE47VvGI38FyN8E-GY8<`CKsTRx>KEC!v`Y;+s0 z7*5)64QYamE$8fhQ&=1hq8$VJ;}yH&$JfGE+w>@^x>C;qGlz3v&sfVY)JJ1gzsfb^U zJP&yM{%%?4@B7~tP&J?2R4g8p$MD7SZQar7>Z0;gUrchdoM`^}*tBvh+4TPFn##-~ zpYSobt)0IyZ~%2N-pO^rNH}8;GqhSejd=FK9t(?M(Y%DSqN zVT~!B8tO(F@5O!I()HyyYRncF)6qg0Vwa>JR1?qg!dC*XDc~aF)x$Hd&nb@ix3>}2ySM{!(HFM&bmF@Qhjs0`DsYgbGFDd|f1uWu zwc1j0CK7P825i-%2Cd*Pqq#H=C#s0dO3bPos5g3!T-Fv! zDckX*cDG$}cNYCM8;`aW)plBQ^%Nm?siodF`FOSAZ|#Y1n~`7RxDoRHUO6UNe@|S5g7YLa zkL+8SR~7H%_WY>UcLiIRavY#t=y0;yJ0B26)F#P9##lKC|Nn74zZ}9)RETS*kEE`& zDvbteG8fXAoVTymZ&)|gU(A$poplk|^2@x-8q|*8IzvsklQnt^QMTq5e zy2k0T-V8h7CxQyH5#M;r92m6vID2XDuG8~h51)ujOUiTxUtjQqa~5Soo}>c!>V9E3RqqoJQPD;%{I{3LYS%Y<)WZ1V-Dlg3%0vmvWr_ye;na1C~+XIz7ICT0ouqfnaHZ$M*8Wql}?L*bQ*DXlb4u$TFTmWuDTtNJun*kGQa zN(ZRQ<<`yBTjvBT_UHa@a+fA>O$&Bor{$d=eLp-`5hQe@^>u1UjvUlq{M90Rbn58< z-`+nZ{~LSPgC>|?_V@X$j%N0vyY0wz_!K8FWtKKMBPdl?;w)~ zbxh=DImv1JL6S$I+_NOYk~cK!1w{}KxHWcQeBGV@g}}#pQuB7=i4%4Os2~o+u_VzJ zZ#)=2S?|j^ka%^qs@>AXW*5_F--Zn+3xq)}Oqgg55X@e*4Nx$ZsKzmwlm3*kmDjO> zi@)I)Rx#%Ps}bi{#9kZ;wQUZb&eYES^fbEX2)}VkYPKOs_vQrB$WhYCVIXe*WLBB% z_cdUoW>iR+3Ug^%og)Q&r`+?@BjQiiKyHYUcIhV{mFkc=P7bsYD0O7gUfaE*@*}VI zNf@69u<#3^))WNjCeIex8)_sV@9B~(+tm}0m*G9&j*uVQ{?Gr6{C~JbOxyzmmJ`uj zUjdMR5DpY%Zsy6AhV!<}yu#fi`R0#z?%Kh~&+r!cITrf{G$ zUhi-wwyTc%Xz!`NYm=I_u_cRe$nq~FjVcOHCsVg~u4NLWkaoe`E+fAZ@fx?;+wUyj z?oz*f(!JTrS}w;`O~P5%q6a$_j@ESNP>v!GPCARzw%0d%*(d)?LHUwEVTPRCz%#8b68>%ekCz5M^`wZqSy<@ zDf@Mh@Abna+=Da$K}>|-G1UNFo_DpmcjqhjZ}$mWt>1utjTN;X=8#xBx_%H2u22D> zF;IV;lc|qij@{>ff84b)9w7+N>V3M!H7S+lcOurlxU_Vqh`>SH+?;S?we4h$*7%4c z^7NB-x)Cw{ySG~UdEWk&YITODj+`Y6+zDhkef}la)jtj`b?au-&2g|-mT#SMRZHle z`C21c>s5(j=lWHD^;UDXc!D1bkoVl$);%wuPOSf!`M62>)dgu?S1^1<)^PIfBWdVe z4)x-rn@}U2APv9Ah5e4JvDe_eWC^DP3wUd2vKKcut>t)uV2an?8?l!8)vQ25yaU-Ku$nx0$#MxR(AGRx7=n_P;twnZAs&8cHk zxhiKeZP${$?lhdRcs?MLgWLMAcQcXr`V#F`hw8+Ro zk5)I+`~7Y)!_m_@4we50gFt-0UzWF_L3vvfN|%krB224jNP?4yl2s|{|8{~{q0B^f zZS#3d%aAQ~eliUCZo2HyJtc{ydE12*rK^NFmc?3IbxFt) z{ohs?12hvt)w)op)D=Y=IxRm@G|iNz<}Di=S>tL-YH4}4ilC3y8oB0euA>=Q&3fMb z{pwLIsE9IEsIgWZ@3Ot+tTcmILM!D&|0;$Gs#Ld72L43 z=!e|0*2ck=w4;p5xpw-$Jp;>ROzm=%#U7)LZu@~ z<>#y=<}FeYcEfJ?e?zr2iPaD>g%##q7K~xJyEi{$huBSV?%?(D;Nwe^t(e>{dbIp$ z{;;6Jh?pp%6+hFGwc4tQHBG|QS`z2~wm=SunWz$?k}eIqq=lxqrk36P#M%#0s#c?+ zBk8KmoegyxM*f6_0ACRA`f%TEnUv{_7@Rzv9YNvo9nz5mxUq?pT{3#-E}`lkHUcBHWu zZCOjIg+tUSg*%&QF0ISg+tJeum)gSRN=aFbCbqma^!Fu_APH3y1Yw4;ipFBA$@%6N ze0mI|>Qp>+a6JTrv140nQDHu_{Bb(fgG5Kz`FTDQjgwlIsHQn{)S$KhSA*ClL#te* zBrL1v5=#!HHL(aO6OPsHCPpPLvzj8;Rg$2_qRZLD-KrKvQk)+1_dAN;gtm!^`5cvo zEyCR2c=(EIU{|Nx-o%T>SQe)AA+3ZI`bZdBEL|c_&T95yX~9fOR*NXs^e*~G|8ZX- z%WPn{<5WuDB2yUq_~B@o#oB49oQXb_lBlRs*~v4OR7vyJ>PIpca~Az17XAHdC7Ogi zO$7DzZWb~dwG|w$kH6$K5v@Ayi!81Qk?C69imcsy4la@w1(o+M%LtO=x}z)R zPm3j%`ujpN1ZlNmlTB=*YQ_j=9h=p(S6&{)KB`j(&+iUC9OP)tXi!WF%I~f0FiBXe zLNXGY#yss_g7adDKxFaI-_=l zx>9ZEszykPMujtBjg-Yi)>=YQe?;ZTM@$o%ytOcrup2W7_U7m807Z4$l^=@B7@A`4 zU`t|5!Ws(-$%NBXqLrc$6)hQr zW+Zi{)GFQKE_A(I%Btqd-K>gBEhVke6w2G0m{#(~g`gDATV&>)%$iy5hsAbt`G%)Q zK-H-S-aPX1Ddn_bv>_`z>f|%l$FEIBOw&n5Xt!c8skN-L6fL+jnd<-bPg~#`D9S{f ztw>fkso{ASEtG4lCZn~QQ*(ZlrmU@5w3sw+s4cA2niH2A)X<=o`E^O5AoW|DLcvrK z6HAykB#mzF=l;Vxo==FVPP_7($Gp5=+^!5s2~kMs%w}9G9ZS|FUCT2kl;P}bXXj}8X&kMz+C;TxcWY7PEwZkbKOf3FOe)4y)Dz7TQa9M#*5R(H&Ktje;PpkKk=cen zc=Kx}9Xp@JM`GS#lU%YZs8AdxvvAVd=r~%=Z(gB*n91z4uu2V$%z|(fRFcS>X{)9|TNW)%bF?y);L9T?2G|Xp%5NYxF z9aEvtxW?0`V>)3ib6b+OMScnGBa`H9D^+URx=F-{&T^;9Sf`ay){a>1{N@!Rn9EL$ zikR%C)-)MwtwyC#sZqm%s^!;^-JrwKw9=2%+)Y`{GHa}4N@yv9NplGbga60&(e}CY%q3g zR)t_TmQ~)Yo0~!>)@o{{LV{Kuk0!sakZ7e#Tepc0no)~>Xu~v%*s@>5`Vz}$CT&yb zW%CwczGH~;J#EcKK{CP-b3}2P3zrK|PmL^DHqNQ`_~Sw|VR`dTG%6}nZm3#UNGjIa zDTrwct&Tlz@I5C(yB6ySan0FMiA#}TC2`wrtxAIhrzw71>PM5@q@mT4IkG0@%^#k| zu!Jf6LMRb!nF_HQgEVhVh9pa=Y%Iv}H7+r7D^7+NY+=zfw?T+Ldd%;wNx$`>I)jUp zyZqE*Q8lUNFf!txTh8-cz&X%lsG?jYZ=v%h=Uq!R4OxGYG~D7UmMBRA`)RTl(5rdfmuSMQsVIhB*&kBbhp~clo-NiX+(@oiJGRj(xN06A!+Liou zE1`sBRkCy9r<=5r+Wf`G!g1CuRxEBEO|pc@>L*0%BS8|erZrze^n^;0GIe!@W-2SP zPlBmZcBYA$mXt?Ik^W=HJI?DGxou`D-{o(9L=={?gjOrP>K4l?loBGAS;v8`nYNve zKZC4+rlPb|+i~Zzqq3E?R4gYYYRz%Wk~f)}&XE#PIboI|)|e%-XRt;V)#!2FP()Ls zvO2`a=O&0MBx1%!5>#&2cKM#C#|Kb_x-MQboD7z1vAq33Pt0fXCM!fmEY(iaMMSwf zwIzq?(zaoh7H_@<6^O~0E0fNMldKM>r6tkPxT!{NF+a|taMd1KElod4s^q&!EsLx# z{f2HWti_T+NU>rSTSf@^obP7RLd@r9JBX-I&yN>A9*i)nA-_ans1m~B7lIJP ztrm+g^y-W@v+Sr-bxa1woDVl*Iy=kJYUw)DBCD1w2@$n2G54dKj?>3?C~sQMux5WS zSv(Vj41#G|^M;gKMWGN=sTekI^I6E-d}qciyRmlp1GL%alPW)s@?}OlFRTQ3t3(%tf=@;(lC9lv*nsEm~1Om(kU5ttMyG(w@nh zC5YS#atN!*cUm7PK~a9~!3OirJE{2_lf|^PE#%$TXJhQ9^uT#J$gEJ$4+pQ2B|<|& zMV5m4Y7t`Qmx>m*k2?Lx4{9oEv6kDjwzG0d;`-)0$UA5%QcSZo|c9d$ecYcQQ7qopmZ z#I0`EF2jX#b1SDwnzSy7M-5fDCgiiU+Db}|Rk4v)Lf?>g5E0xLW#)20 zEINvqQl@5FR*rU5?6jHl@n^9AzcLl=vMPy+img-Zmg+=StGNcbaFl5!Xg-Fsn$MJk zA{av$MJ!08yfK9}q3DuPQk~DLnW9PzX=6V!MmJ-siQ|`CPqgiiZ{_nZKDV;au-gD} zHLk=>EG^EUm9X?&CZ(j)IX#{N&t+M<7Hszc>yn}0!#7tby5s4J(NR~n(&gbRaqFOB%X*Xqy-VwxVDGtT7<`0uMD4(%s z`jH5+ClP73l^VA!L*l7YkzcbLGr>sfLP7CIHult?|1Kf3{+>H+xxv7&1PYCDv(RX@ znkBE`*V^@>bb=1H>q+`3&Q5$kp#IzMP!ANIjFK|-k26u zBvD9VDJ8_JL4EmksbZ29Sz3Ldw8~JJ66V)2W40`BvXL=t=JE?Vkom_o{@1x6w%qN| zBR1u~+O|jaa=qj^m0DRHcPpK?ckFSK4oyO6{Rnlx5#HLt^Be` zM2k?NN=sIy{N7NIjaCfKml-MPloegf6^kgP5J#U7)-iRTMKKBI4_8A`G@3O_5$iTA0y;tU^msK(-fVKXWK=$ z?6q30qF44ugK9}g`e>R;+_Y3fH!U+`3BFR;^pa3BMWVkDikW8 zlQ+MIYBHmy?Pz_|)U8mYS=q*M6eFBF@b(wbH65yyC541CdQp0*#0nMZm6R2Q2q}vs zEy(XHw6c6Jw3fHyqY%-wsFf)}SCO8r!b}J&Z$1ms#5i~Oy$i=7IsFm6|0&q^N~Yw% z4&1=Ta~v-yHVWogyV0ptoRU|zOLh7xOkt|JZYr@{CZ*<$e8RF4gU*nZNC-tJ2=xO$ ziz`aR=61r8J(}O=D`7p$=TmLvP#vSfQof7a9*#L#M^qh#hp(Wv@4KC2Ot-J;>%_IW?QkYw>I*ZsfHg zT@{9kq3A+WK0xE(;eqG($L2Oh3=b8O$y?0lv>L5QP*E(Z7E4x7BAMIPW+$;}v&#hr z)1mi^Ryd_}qRNS)v{Fm0QHe%D+9OJeRz#$f@1-JGzQZp}3i2jWic)e`*=ECCHA35y zHou2eq^;S?v2PEGwf*tT{UaM*PDHNRjh0vP3U;GYt*YbAF|Fma1GnwB1Dk%T+HgwD z(sNqgN?2MhlU58frs#$iSK^BDL6tME-*PT(*w)P4FFdqr3Rxc&LHu&HXDf{m*X6S) z)E%iws41_&T4qCqLKP}1N~LHeN<_LQsgy}jC2uLMq)08V*`-<2cNnKir?b$gV#p%9mxk;#SmL!>b@t(MQC zTB~LJHh;iZRM_J(rkR0a*PAu1 z(y6G`w%zVjyqe$av}$g{DfmA9jHQw>HI+z;gpo)amXY@|iI`<(wX9-@4_w5r&hI#1 zF$S{@ilK$F22G5-v9yK~92Lseh7xy-USeq*ZPnJ%v0Z+StbuYWW{T2FYLb4{r^Zo& zP$?EdFG{0wWRc}0HCd2I9xLa3u0cgdLz)lzeum0t#tPT9R7S8udYskRa<{h5?+;JC z`SYN;Z?MM?nGQ?WZh9TRuCzK$->KBgTEVS4mAYMVOCEhxRMXUxs-+rnOO0nTaXqIF z#FAz@=MEX_2Ob{Y#fKwzhf-6PNJze;CTM9vNk@w6=0Zj%e^n4!+Gw3M#$el7$0H~~ zl)3nO`}s%z(ZBce^Rq%qdN@UhBz;w>QbTefCd`6VN}PyVO>RsJGKs|Cqh(|*$S+I2 z3sToZt=i_iLmC?@6B)#4n_A!JdL9&iFpu8KKHp%vF73c}eMj1|<2Bm#qA}hqsMT7r z`sUIL1Kkh9rZQ3$u%f=u5+Ovd0I3g_Q zgo-Nqi&~UQWbUs+I}4}M;XJ;>D8Xd@LvKEK_~5k%o6W`L<@r2~7AwI~s$^7#iFOe}wzcdASTrAbTr2(paxeWRxA zE;SorKQR&<#7N3m2d^#yFI0lBle&VL(^9jb)GAKZE_p?#Rqu=kouG}=v0YyRa*4OVLqIjSP>PLsN5W6TVEw#Dbxvd0QvCB8e0n$|bB>VPj zf9j1_zWC}_-Z;H=vWnW>P@1aLcI{5cIOMi9Dwd*2y0oXLhf0qYkN|3@VU)(ez;yP zTp<+MR;q}q$q)oVE?t%qmr7Ywb2wxxWNVJ^p|ub(6&ssrdfe{yD?k3# zAN%o7eYq#cORCb;7OfOTql+Sk)|RYPrcJPo!9<&{yM(+@O0r*wl&yE(Pn~_ zRv&KpJ)GrZH5}0jiYzUE+Ct}KhqlmY*7@)=+!Tn(oW~~G8mnD9?|6sIbV zhg~R{)mFKhYq6}!&WB(5!@vJiZ=B^|ksS@1qA_ndq#cTo9JyOdw2F^ZzLSizry!~2 z*N1{+?O_p?Jf(`3l(dCS^81?aNX+dFz9FE7NBc%jVg@>~)ki*&X5m9`hgYYzaN(k{ z6_pn0w3gTNsdzj&;0{}ZLQyrLXlO#2*~6y;r&$~_`D}>PvYODaJ`j}>wN)$6#F`yR z3RC4gnUeWz#CG`>tuVdC)n=X-%Z|y;ul}Y#_L;A|v$`>rrlgIgDiT6SBuw(muQkjc zKluz+RvM9f_RG9|{b9&%4>`9SoB53PG2%iBR};^)mo0ke+Y3+5F= z$y@cxqt8EW({C#^Xz9ahULP>iF(aoBYKCI!#I#rtQ)kv(xIP!tOzd7Q9vdNVkzb3+ z?;UoxRH@EI-YpWdx|tSgxt&A3K|sXhJpOXsj5x6K3%~mZ{=|>EbJ*H0(o&A-CdX)$ zyS0q`>5Bg7jrv6+%h%{xnzxS&M(K#=6tef-Wfp>sfY%`$$+T%1B9alrF18j3Zlv`O;l}i3p@g z!hE^um=_1J{bRrH_x!=vpF2R)G&7}Rq}=T&9aKV6OC*V;LM8MR!?MPF7N*J91XF7v zQ{lLECg?TA>P%`_n>7d%W0v!KglC4ve>C$;%wX@i9TdE9S#bLUz`!26?zqjygM!uV zD6NWD^U7Y$?bPcXzussC4X4p`TXuzhDk7CMb6Q3jP|Q@uA|20oIWulrDJ`YU1TDJU z94oV!5F=kUY8Wk8iDwq1)Pzcnwmv1qR>x8>&1yGmOBakJ$~Q+BFBt5&t0TD9S{ z=x0oi8}X!;P~v(b9#5EqZZ45iVwp@b$cdSjYrLN9)j>uz7>Xn;-@D~W)I?v!$J7rs z=FKL*cO$ty4mdJv*kOD4Irf2auIN?g%W<@T^&5Zi|M-)e#T2wDm75|$Arg_AAt}q7 zrDuwBWQEn_$@1$Ws}BnjrOzpGb8X9NGeIjG%jWisSz=V3-#>7kY+IGa6@Hchfnm#kX>d2gQagkhQRvMcT=Qf?;&`}qx zjfmMo=lOJVv>XGcpZy*Pyunw9L7M3yuESTt;vV#)qKsduH(G5s+VxyN9S$#l76vXloAC90&+|R$ zdai6#D|V;q6ugo@=GdcN!SMq6YU-F;+#N^{=&>O!XAI~$)65z}LJ_JEs-jN+I(UA= z^{Fh)?b>8(tupxz`Rt=;4yjm7T->RfNQU#^lty&Hm5==0O7PLMvk`YS5d`7H=SO$^rL}`1vR)Wi8v-H7;^LJSV}b%Q)sGY3S)Y8 zj=p0rXvQR%Y7!;#$NAG@F->lHs7a0}mFH@b2+Hl6Z5-wTLBxDDc3wW1dUynH|Bt`^ zhsWK_)IFsdl8QyF+%z01luC6OK0*X3DfcuT1<_{6fu3lxo>!Vf0;!- zn;F%Dd6u=0&xO&Hw~G2mEmkdy<(-n5x!sPVGWVx<;hsqIwX?$d?d4)~D|qeK{y%@@ z=53|cK{cyUbK~CI&aNzFtk53o8D;SinkHLvl0W&qL>3D=D}~EX)n12TB9Ko)Ax)RV;eSQ zvdlXVMHtB>Ji}^RQ>ILjp!H~tMw5+GY*X3xv#3_4y=ma=-tzbu-u(6d-48!=sHQYo z?9iY|EpCM@jS-SU6!QLp2$?iPYb0T)W_?XmR8H+K+-KFLJr^AqvRt%kAA$rh@Ey>yWadP>eX2ycDvElbltjN^=fXlMbWFe zl}@wfx%4v`OG_(}GL*EIQnO~t)U#?{Pit9ArlQ!9%NsnI$^B+zLu={D8(*qLpY&92 zNeFj;u!5x5YQq>)bujAqE?O#4CT+VyFXl&!yYSYp`R~4X96BmFa@#1i!)?x+5<;fX z*kdf~!BQwB$lF5mo*J|!j%cYAd;F|ztm{c_Gl>mi!QLG9I;ml?fPL&paO0=x9SaF`&Hp;BxGu;`JRH#HgCk!!W!IHcC?!im%oIyyqE7_4KxMZ)QKr-u-#}7J-Z~t2j zD1N`I;3FjZbbx=6JZvp`@lrOvmT79>NGIsv+tTq1ZprPGt1YimEqj$tr9xkYrox%0bY)`yl3u*RoST_c}D(ld4z_%1+B|p5-xe` zk(AH2!HBGr7=skV)ajPa_s>s`miYev_W%CSgF6v~+$pC_%QBxSNm!H5G%`NQyJ*o^ z$mo~VXpI)Ke9Sh3ni@-f50b4}yYrZH7s8_2W;hc3=>i`i&3_+A_kYeP3&{(20FFco z2BrloALHnq)|wZ8&d5znqb0r{5Cnm2*6mTF*l4v{MX%`AYo$)$IIeU8DOqEIDl$1U zuVrJIOe|#%I|He_E(~3hhN3H)GUbequU~PtDWufq%k!2$U(eh6O*&;YIJ8ufL`@$^ zac5yM=ka?<1rx;m`bE|8K5e<$x%JqG&OnQBo|HKV04~rJ)&ue6F>o zEEOKo*G<-{OFlP6YVBj#f+Zx&`HM&Ak*Oh3YO`@9`1a)p|!+O@pTV{Ua z4RZN`sUwcZcv+dd=a{~vPm8`+)sP<93eUYc2UQ{Dc`zyX9BwHLqQ- zI(F49`E|eU`BlH+H=K^o>Swp#W%k}X=~II|Umy9{<~FOzY($#RG9-iH(OTp!3~4o) zB$L6C(5W4^(xEz5W7^NS$3m6^pa_rNiMPD{`H%mPUw;)^L9I-!WHr>vpb;85)*mdZ z5vu&2l-BfcYAT}o>A0I}Y~9s1w#nM0F}F<&HkAE>%Oe?KLClB*U%nE7yx^BwuSGvy z=^nJ%MnHw`IFEkow4;yzA4>d`nN}&>ieK}qZqxBFuV;HgqTTe7Uf_9mKm9YH3Id$Vj=AD*|GDqx^Y*@f zbzZym<&LeTx7_ErcvRvR4nJURSoTMb-(tq4zp_ysExh&olkdRr?2n%re(~w)fe(#n z&62fAg=12A^EqKwtCUcxtw~9UQj{vRIc#iXX6>*=KSv44VQldD>H6>xKmXPL>W9k# z#Y#zGf1og7g+d_-8}lZ!vWMxRwh;4~VO1lGc2eF=K2y-3??PthJ+Fj4O@pk zi;!^I_qOi^=~-Do0ZHPMm$pCvZ=U$T1kZo>*)l!VT|Rey5BmqtCI-ZwpLFj1zoqMc zWHrVX7d;sfZhAa_?b`5dlHX+p*{v+Q`}gm{uKx%& zMC;iOAO+z5xZ2~7hgX@=SnY5A^V!{=^=$ci(!4<8P`&0_So* zzRl#jr^Z#xFiZP^pFZonbj5!{uJpk9J`}?ypD(IM^4apIElG>06x~{CxuBrXYB9!P zY8BMZ>}yIK3o0&O|JcKYpZ)58_QTtfq>O4x^ZD~EX7(W(7Q$dXBrGfQxwM#H^JbMq zP_y;AYn!fQKV?F;TF1mpx-)1Yq5AD05efLwcL5=RIp-d^KWh&wVG%QeI!#>t=47a<|<~8cJW8=r?&IQ^;95C~vaT>4enS7R$ugamn#D z5ggI<^5f+lZ;$`+|N8646%`IXNFu0KB)?Czn2CI>#`1m*G13$5W6ohNrMR;3qf0-! zWRoz8NI3>O-{RmUEOL|`BO36FuT^_N{Bxh(V1gn-!l{qndBeTW?+geY>$P5)!L+i7sqB4QUaU``PM3?}tR%Z}!!Nul_gx3-8=O ztt^t#q_Ug0r-;>{w6w{ZM9P#Erdav&#BPP8w!asX#%w>PECz>Q!ix5I;d=5aLLwC( zD-J{iUisl&K0?yH#kI%u0*T!Enh{|0uNfeD*N2Dy3CQj4-3Z2xnFlPq>rC*=AS6AN z_27jQ#t}jwB_R09?;=3;<9z$TNml$b7cfIDjy@~RJCDoIn&moNNoaF7vlV`cNGJ&M zJ!7G)d&1RBAGQ)AVN*>lQ(@s}?&jer!H-|R;obB9^v8#TQc;wGS!;0u&FBW4WOi6`5h#s^rDofHTqj5o0qFj}$ zOko*pld=usfm)#)4n{6J&#%*m|IPpXI6}GG5|)(j@~#F)NzWg5H9VvssrQJfT|&Ta zSO4O7o;2u=(cJ_B8-bL^UAB3$Q%iCGTqF$LdgEumcIS@(7GFR0al>u9{qH2Zmi_ga zE7;~hShg~x953%UKe8<`Y=S}lFrRzeA3(&=pdy7IPb*R3YS}1Lw7Z3K^Gk*}rV)L3 z`R3s3|JDC&j}fV?J%y+blhnxMop&ts3rkz9^g$cOBB8VfH5r+eqdgxF%ytD7gI0|f4)mp+b=aM_bU$w5fsYLDf1S5^ZMLdU5I7-;s;0l$ufZExHN zV#Dwk5b%>j0X}m8z@ri1nJ;E<9-LIKmpPt@>9$;$W z(EM>9ef;6Ge5Yua{94O|u0={JhP6zTG1VhcAdU!u#cF<3ZhhCugLx}k8*UH^!f_)D z=iMlnL0=(KnK${YkQPK0$?l?q3p#8Uu30qQ$%;)eybzYZx*~hS)aMH!Cg{HD8#SPD z?OX^XQpkkXL&`2w>EgZn=fAu(P?X_vk2K&0Amr}r({tei2+4jI(w#l=E5Op?z?3(u zwz9s_r21QZ!_y;$T=?LlZ?Di2+1dN=FQ*55?l<~^a~Fov!9KoY&%$nUe>VyGD!=!Y zMHzkNX(?I-WlBQ#@jaQv!pf2Jy9){AG1;BNtIOhjhqXIIQK4=M+JcG3YK5lB`;7Ts z!#;*$H2eX16QvTDL*z6SrgmpUb^Y!w$At$G7EgVd`!|{L-n(XW*z$HdIo6B6oCEO8 z%JpdbAc?6xT0z&K3IxBtY#FN`2MPf|Lg_WW3cz~5Wf>wF&jE}t*>lQEZLnZ$cv3yi zoS`UD_}P^n6Q;hhY|IV56UOIXd*!(}6PcU}OJcO*ibuc9tAxVoK zWeIg1$xlkKlSNyzq?E|4 zQq(T~s+q}zW+Tq;-?@Gok0LDi>ccyIV#=%UH(yA2YM%)jr{6C?(ZLM>vh=L@AjttY zUkMW9)ML_;-~R&%S)ddEIP_kHkVvn>>I!A*OAaqtH8Po=T9G~~LM%RPnLtVa34$eO z{(XAD$FAfLmxs{F^UK#Ncz_U@vTxp1x9MsP`adoN|IL+7~`nmT{> z;EW17EZf2I!5MCR=QA7P_P{FTwjrcJM)rY)yeXa@PpDy*H-vVSBZ?h0-ASz}L!_pr zES1&ut4G)K9S5O#?{)mnn(8?2J`nP&Kb>IFoFbsbq2eNi^m{dZghU7T|3FCd#)|-s z0Wi{l2SPG5fe#4D>PloV^$D}nj!&lN*eMeo7N5FI0OWT}u3TCILHqObrw4rT%Y6Hu z@G-kQ^8QqYU>nTcNUeN_Sfo}pVPYw$6|Peqmh|QKEZat{rEncRp&S~;>c&}F-o3gC zlu5H=RVcSb4awx!20=_~EY=Vc8~#^! zo$~(p5eFgp?$@oZ_Sdfi$ZQKv0Fr@T2&9k}%zqdG`&~r$0)#|Q08NP$GBJSwq`KW^ zg-Y2LzV8d)*0iY)pH6HtEoMJ0G6a{s3=;jj7o`-p^Y`zTfaRA>6Zpm)zVvSR@ci)| zugf$TN7>E#WugA$v-#75uB&(sp(J>h2{N1cM#Tlvgiks~$t$HAC8Up=~h!a;bxa1$5Dr@WPCKZ1}YM z2YNLDAuUirLb*rjDF8DIR6t0A2{r+cFJb^}Mr2fi6q9(_3bu?6SG5uFg-_Fa*DoTW zcK0-aJ-@|Ip9!0I`S{3(GqO9P73_-8@_Vd>7L?Xk1(&PMRkuM;5-|;fWt~RL96Nvc z;I*vi=!jzV-~!J*TUAGHS2njtC5|8OdPc@*)H2l!1&NS17Fie}TUxh_wDiqgrAjmu zM8fOO=)xXFcyL*5=g&@g)X%#MArVqY1R%4&m;;pQmc0rfBsmxwEC|UzFa4TPf8yFF z1rjX=*mS2R*kaSALh|4%J9hy~Myx0xU5v@JVx09OL+~_6xPXZd$ROaY)mZuXt-rhD zkwR4CD|5I)hRoQTcYN9!?3~*%jztjkM3|D?BWXcoTBe$))`R5j(l{!eIJT^6<8_{P zP(3=2J-?W7bn+jk`I{E{;=ICbTAvd>)0= zVA9pyeUc$E?_SO_r#w10{$vagQb>TUZ$1RT0VMHW21rm~hf{8m0{4>LHeUXsFQfou zFA2E`Haqp_Qn2d$f6waikI(<;f+*m&NKK}4kTWC0y(XgWG^M_dw9a=Y5pPOaJ`KYw&~7&=H+M++R;Lank&$2JvfV&0WvY(dBfR->r| z`HZlXT1g4|%RwbAM6bwNHWnk!pW}hO5h3FL+}rW|sSlRl{jXP+(w#Z<^rNzLH|W-T zt)e>yd~~2F5%NUxEr6J||?WKM?j zTaE_75l+jE!zCjVVI1CJtFz8AyEn!(K)mV6`fO=yG?t` zIX1#{{{0-jUIc)4b^M*O7#bWNj?;V3lSP4Ite1xpfCB9P+Y5_Gy0;p6Ttamz;DV53 zC))mpACUoK>G!ghQ@5e~(I+;X`dS4BCsPAMM27bOAva<*wU6F%?eA{AWlNxb=A|}* z-uCrp*L##J?XmaYn|tg}Um7dD^U7~N^eaF-eZ?4Dt;9vZl%Jb?NFVwr8z+IAlR;Y_yfFsdDb;1e7>JPu@-{(P-3? zwLLU%qnU_gro}K%OIoyq8s@Xy6mu;)Rs=h;rD3u)M8>XPv6DTDFyVo!b$TS|=LLcE z3Cv#KeKV-GoFRbYU%TX`Mc9HcaQ;QKJ zGK>L|%~uByu~+&R^{P;0IAQ=DXZ-Jc|Up~6>zFxt#ywPfM z?!nRE)Ezs&3YG0}ea7?V{x+X2QVP{CpZikEycM?ggUxDj5s`97qOu*%X4u#=P3J)c zq+?WJu`FJLp`_cbMKZ|~iy@gm+BU%MA2B?-C5m{UyePt(l!s=nZvGc*^T8Rz#hALK_{754Sx9d?<`>A)!z>#c6Zz|MU4VDFnayOIOha5%>U-?r+S8O%DJlz0~84 zMF1NgLPY!=&n^y?aKR7jGs+MfO-mVi`VcKyXb~iL=_P{-36nbcoKy?O4o>lgCC51_ zwyZaobX0@2Nh--A3~SM>H71cs6RXJ|$0DnyDJ`KzWz@+j5?14|EzL2yKE7kPK7ArnpGwb>Pcz{av^q{}XmvQ{vETjcr4k^v+2j1fiGnY(wU|t4Dqdu;dnEhf z|13bj$8SGz?^Aw%HB!PAF5rTYu=fvtf5REN6fkxx2?$qKyt|{`P4R7qUc(im|q&+wk#O%I_O3lC9Pb+v)lXck?LiZo~Qh%~8S8 z?K+gf46_vkwP49sgQd{?TB}SGWi%SP)s!F1#Zo7W(x$B(lEtpyenStu{CN$}`)r^@ zto~Z8@z7Vj`HPz}$nQ5qBy^r(M<%nzayu$C+C7xMtoR#3vf+>DKKDZic>e{U3=$6i z_a*0l>b4sUiQsojU>~G_XSy2yL4tKbWXK-Nyf9fg-r)V&=5{h8Z9SLk!P@( zp*W**wCdy6U9w1KQ-+;VJ=`nDO0HM@_;jHhNE62t#A;&Ri8aH<8Y!C~p%95AsVT^+ zn_B6XA{b4oPG~kZ^6w}s~B5P1Ny4%N|dZV&vuh4Ma_>2w%27@eR;ftgtG>F zBtC-_H2wv6bbm7q@X=#Hij|9C$6b)HSON)N9u12-2G^JdGW!uP1Iw8)VvC6S7UY`#9S$}L8GAmgSb0+ViM}r~1 z|4?i(nA${gXdMEDn~09uEBerv;0MA(>c6d?2QI_B2*3 zT8%N~Z7oDO9rV=5q9R=-we5l$V$gB0lb3}K<1VT)<8=pHnUiX0fU&4xS_nzK)|Zta zVT%dkSpxf?18sO_@GJj(HXAtqa4ko-f$MrM+Fi{pt(AB zrS8kF4uBP(Ku-eHflAlJo0v#w1+dRX2zVLZkN~H-5hA-Y^PKP)=kxbCYd$--ZDz&b znPM@EzG|7&GPxLMLXog=r4$T9Yzx*g?cw0qAP&~{iwFGQKRq~WCUKCd6_eExGnox} zhhdn7#84uKNikV-bg0DR_E{go^tL5XWj!V5Q5c|5(#;r-DR1*RDqBPVD_F!!Rs}CN%vVq$vs$73XdbZ z#>Z=lF)eFaht^7yH}6({PjZh1^;0*aipi)`g~cUVSu;(xcAcHyzv0q2-oxv=uXz1G zKRniUbhOk+Su9D~lWeq}q)4(xFd-^hwIY|)YG_xXt&?dYj*YcVvofyVd}E7;BaDy% zEL=R3^=o!vl5fmBCn}tjYzVl$k(}tUH~(|nALc#|Iiy4t6V#RiSU}K~iTA4dNPz^{ z3*eRcJOV=QNu->`TOqQ!K7Yk^<}|}t(^w+oD@dWfCd{;=YEdb!t7uIvDFsbC?QBxp z+}++>oazBnIPk;MS2=2?QMJ6edFRb&g_#&LMkCWRhGiOOH3|w2=2WRk*HTSXY1KCD z;bJfEcwHzFKL{RrZszJ5)bC8T1GP67MTHNIERyhlukJg+%X0xT$HAr`s5d59LicQ- zgAnLyuTcjfF$8eP5R&%j(=mYaqQ&~d?LvrfzI?@pYfe*A!pf$kypgpm(ef68X69~~ zsD?&m@@o}sQY)FVwDxf5Wrv8Pb@zuW{=fgn)DAx|?(4gEcD$oMuY&#Ozyp6N&Pe?kzQklt z?a1Gv0-xUaT`73x!Nj&0cEfjDG_ zy^SxcV1r4QZr>N?+mdrOIE@F z;6NY0G?Z4ZWJ=YNs7C~c&1|e)fA$sO%_Y3h&Nw~Eic60>eXmW$jMDWCevHX~{Dy_U zMg=bT$=4AQ#-5;CNOt_O2IybXC2-vd|NHA79{Mi?WIqMeJ0N-E=Wf@xmH-^PWseUH z0f+z&ofR>RIL+|D`SR{wqhYYxrmtZ6!v&LkuJhTFh+^eUyHu-(W3a=p4Xf=iE{E$Y zs2nlI_3a}+{BMqjn;rLR4ht5PNlkP5E4L0$%pRPn(hO?Pxpgg z`N;PcfP^cz0GxpCYYw^o?psg!B}h11?~$4V5(ykg#8gy7s9b-1;KQ{kvO*cGn3O@* z$MTLyV;N;!nd?$@6|z(&*D`5zf2EvtT)%&-15(HA_VqA@c0wV=Q9UfozVVoJ4k$lRXa(L2rxHQ4=2U;4-Y*}!I-jSS3+C9-;2 z&+Az|mYlJ9@5dam&Dy}Z%)22n@bM>pdWSFRmTY|0eOu7`>z@N8g5F=e5TMfT!IPgo z@$7#9f>TeV_bDm@A=xZ)G|B|wG4}X1uP1XdY-aZ&-=k=!h@osFO%|d#u|-p}wA57I z(l~cCwNoMM>>+pKsO3Dr$N%~_k8YzJhGD`Ola^r_X7tPwd{E=G5)M^6y=yg5XH`yW zsu{DjcGO|pSDYta7jls6y!-MK|9YDV6-|q)aaD_(nx5AN&8(J72ZM_zi6IOQHgAV+H!G!8Hf7=p}JvO-gO1fJP0!$4+05bm- zCfp~3M8X`RrrlyFJdVR&4?dkm#Bj_S8}-%vVTJ`=c`NG5of*B9c9jc7%H?)PQ|DEN zW{#JiAG$#0pbt+^-{61w`e<%vX!QJ^?^&72cgyDz%wTKSviwwTN%K7dHz3!ko{>IyHzhA19+P)V^`l!TYOw~jxXAY>TOx}p4%>g~H4(q6DRV_8b z1s}(_nzhsTJkQ@A9WZzJtY3G9YFL8RKjp<=AHRMkliJ|$YhOH#o`UXAeymFZ+upJF zM1$KRfYd3GqfufoG||c9`D9HWG$!`0XA;2{X0XCmuPFiQ^~J zz0*kKsH`F%9vz7H*A&~_VFgizP(Jrj5=bUzsRj)Dnzyj>z40oO|H<40+s& zw=(e)KYk!vm95-H4Tjm6A!$Beid)j5QYuMKj)P;u)NEvzcPyT}JdW=A z#3X!u)Ob?Icd^#|kstjSW8x7{O z<-4D)o*?Bt(yn%6%Ph2@5SgO{ zajY_VqlVoIRxF7H7hPjkN!r*7tA(!bzQZHW$Nvw|-;U>18x^hGE^5_=-E?Yx&2QVC zn%D3vl0K?xJeRVRjFyX8u|(F*#xi~?VZ@EJo)Xgt_kZB5!&r--DsiM5!L9)bDv0e^ z^G5-OFNhv1*s2g3F4wp5G?R^jlG})&DB?=4@>qh4X)Yw$G}Y3|vM68MOh%TiaQ%ud zP>-9G)A^XU{`gY1MS>wlChIh}LA89ZmbIvr{94*gJ1Hk9RJ*udvEZm?t87PQd-o03 zCte@TvtxG}UL&X|t!Bds%FVJ?v>Q&nW>=jy{WRlx+(>Il5lbX=Gp!A1Sv4OQIVOI&pwCrDO%*S8_cNlTXsMmI1O)3hy3W5j!TsoTXH*cznhsOB*gO0$gdHy zxK;B+yKlcKdaY9TSNcBv zjO)TO5-#d}cq^?pI%A$C2puBaoo>=U1;GQ9ddn zM`ai4rV{fr*}SC;>3Mx1mWu1SOj;;HQ52ylk-)#= z3C!zR>*uXvM2TGAqC352&&+pT_$vr`*ycpeRd2IPcoe_?HC|4PVPiHc$dk|VrwJ{{ zd-9ex6*=of&81l9#DUeOIqN){_WJX0?j8K_@bnq({m^nkV5~cu(QL+@7HJY{MM%u& zDz;V_o{6hXCT*#1C9W~EqqeMEzy3Kr@^Jb8JNg^QAixiNZM<2aS$4{9(JeWxsy!Ci zvMv3N4CtfK)Vw~BNhIS#?m%KlCIbfd)F~vOp71;SwLU5TR9l5^H!&tN1 zuTla)@{x4(n9urFczoW7+>fDx`q)?Pk3{8Ts3uvuWhyLkP>J%!i6%{r#d+-ATL#Lp zE8adi;gerDurnI$2EpcbY_;+R`D}z($-8j2tlczN)iZD~7p$Lr8fRUa}ExwNI_)j=a;WYc~^PxuMl&~-~F z;kDq#d6*ZSP8iy6fs9%o>r&J;YZK6Y=BY$Z?;#E&T%QlTgjqu?w%yR?PkS~ZZ&;Rh z>XKApuqu@FNXr+l=|nk}*YCbz2gg6w51z$uy;!w7Mpk2%Hp*;v_>A^QQr@H)+08_! zb(_?r74Ay9+7xN&+}cJ>y1x64r$^pA{QrA>4eCLo)2P`^yP#DYW#6qgTWZy7c=e9s z&_~tshZ6CamDMwb-7>lX)Q+V#Y&HL{B! zBpm+>C3+MeV%HGa`0$qV^i7x>zz4ibXav<1;%J4Tu!w4w*ef+nF@%-vSrqFq{h&_S6oLO#1)9(aDk z$CsZV`dslFZoO@{gSuO{Tb*V_E44~`wOMXUhdwIluC$f(b9zF}TCs$hN~JU_mdF^| zBpbc!A7e1|)jbJMAF6(&>JA#_8i`?$YOvPwHCrA~E|WcoB+Q%zYyonL>)dGPS?Q%OHPx1qM|rtdfXwpVnk9jD?Jy^259 z@>kMV!%||p8P{WK+RLW$x<07pwE-hz8d=TkQ|IM>zxe7qu+y1`5vEdy;^L^Kq*-I* z2g}Br*)2CFf;MhtA3}8F;~Or=v|60GyN3C$h=NkjiadQ{%p1AM(&f^hLTo|V!gNfo zU%fLAkVmhEi+e9}|M!h*(lDK7hS`i$v4#+oSj%@vBF^%8(oS=mR6HF`I#Xt$!*(?G z<}1#(%umYF4xGU8Wyf{YLZhISYn{4X^v2yry;-EcrfR5~p_=-DHW*h2l7r4rVi2WW zsTAIQYOL6S+=tH360+(Xjh7ZitiBHHMpS*8*tua+^oI8@mmoAVtPL@Qf(a{;w|qu3 zYSp?GRT||T{H8mIqheui9`2Q+$KcIziy!{uhY@bn+zpM5tqtX~R?-@VB_vs}a+ol^ zhv*V2nKF(b=UHdRcznmxIQmJcuTpx_Bk*O=DZ2$xuasMidqrowQ>){+u5<$ai~7Jm zM;|{z_*z2)EWKki>P6UVIh2syukiYcHiN|pMJ;cLsg_Yb7nvTCL@pj!7MA*@$I_s- z4bu7Ut#eg$Xe#NcS+wInvG1e>wPRDNtO(aIj%|b!a5-J;?-f%suW+r2eS&1r`yfNkX$Y)k{HLJ_=S(4>5s?})Itmf|0 zuHS!SgLM3Me(u%@zwh-Exrs@Fj96l7Wk)MflP01p*39siI-}Jjq19E5cIIvCTsjBY z&UbHkV3*4uZKfq35}TkUgI9^qMv+!qH|s zr%Hn#@(_v^^7c@(jx;HsL4ruNR-1!qX>QwjUcYyLn# z`KesXUVDek;u^0fD|a`yNy_9q`Z7o&=qsgbx||Eyvq~u_WYkv75glW*gI(D$F24Fc zUitmgn91F-+1NBQM#~!nA)oX6nlP=Fu0<2ciiJ)NqiiwRS*-_#m-F(+^I)fou0A_o z0#IX#L3PMdhqb&m5KG7PTs9?CRTHYBYKoXC@xBh15w82edGP7=V7oIJ$J`xJk9>xn z$-6{jA!S*OsC8YC{Z>AwDAvpt*LUpoCxePNuTJ=VuO0;3Y?w$eu|_SyZ7pO>NFxb$ z)k@1MrQ}-iES*Fx$J%Ti+s=pY*pch;$N6C7!#6+s+;gur>Mi;i%MNO(lr^Ab^&y$c z=~>NGVv1%Ox?(Cb5?8|IgpK1kI7cuvIBi0bzj-BYhom^B&F(mbLP@7qHY)+H3 zoNwszlYyDDue^_Yzj1NgG?Dwww#H`360Hg87WE)$X|k5(Zdpx&DuOh5w(8_iC3AVi zn>?R?+^@H@;=NVHqETo!+jhfi1$L|HJ8j>kpP5)9n~v$3bV|t>*_3J;DKBjpMl7Le z)4n@j;MA?cC)dlt(@WNf7L792RQjl}d^Y4WM9FEaEC|a=s2~!h>8ul0jrJ8ge?mxn z?dnB--Iv~d<~SK+hK9A^7Bt08v@DXv%4!)}8K*-kq}^9ERolsC+o>IU{EqWwUON78 zz?yn~vr+RpO{d~j{c>Q}>rJKN*1cN0U8j#i1cUKd+)C?dkw~UBGnI^KX*FXcwRFhB z`VHH*{Z^|LG@M%1ZaCvwrBQI*8vQi1lpa?TmP~7wl1s!BYC4lv66vfSON;5sU4LP} zPY}w#dHcYJ=U^L|k(e6E?}PpFmWGI6h($@t21_Me32LT1IL}#Uae2czgbl>!ruXri z|6AW|)`=M#yK~wNk++3eXwZa7sn!H(huB({u53^lWhuw14jtN+y>LDM&_q9_-%>hG zJMdew6O6kR+pbjGUdbyot4+H@KMh5X#Z}EvEhDE5Y36{Q*9MG?X=XF=>A)@jX1`An z!sCa_!Sk!_q!?ja+9>o9LVZOFGD7lvFSJymSQM(9r!{ORS>m{U^(}iu$^<&`&U^Xd zzv|@ymfMYnncb`P1c`{9&nEet8;XiuuDd0JbfvlVIoYVFZEt_h`7VFhRQiD@UEdEp zS1Gi{^is{PbxPj2*CQdOa$Dw+@?5>>tq zhkl9>?zn#Q!0RDq*s!U&pZuCV^Ooe-71bqZ-lD9eQspx(HESb|8Z#~rEJWB~=8G@9 z#DDO=`u*U}2s6#PCk73gcOhEMlom(Zd21p`l($j~acs1h*>*mB#nZ{oKU~uHKuQ^4 zm%VYX>=inGxn9s$*|nBy`%d84j-;=~pf+SAGg%{+%_P;FK42-TZU!+`R}C$C%>Npk zcs^mg@$raPmfXgqEi9Tp$VDHYe;|@1D+y}rJSIE8R!Ozm+ m>o-5c1BwwQE}!Fn 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 z zwzA$duhSxh!u8J<^yIJek~`kZ_LUc>v;`E6wa zX}(v!cR)+FD>_@YHW3@;A~_#o47e+(DmN2%`)f&&$W5r~tBv{rjLs86BFEk8xsypK zB8E$U2;4ARpJuSXHdl$`wVOklr{2J@QhKW%Q`ub1}NWT~>zwU#P)oBafZ%3=kOqVwuiG2>u(vl719) zk=CDU0OrcCFFnEg)^W%{6S~NTGF&$~o+qh9{7pj9vY>OpNaH*5=3B&)8kkfXAlEzE zEkjTO&ixId_4bvnfKEQZB^6R)UiLeKr!wcQkO3GN)1|xSY+RDLpp~B{@aAZGD#pRa zNoQI3b!k6Nc>loOh+;SPAG{~d#^EfqFcB}jiI}}Mx#vRH8@_vxTcKh`Wz4fIDoQCK z3_#o2g1A!@#uuSYO0)U9^>*53v3NY*swRQTz8R9-Yf|vX5Cynn98_fz3!OH#20=0|w z{rE^Rl2V2js2w#y>+tBg?mt^z2a=Ry7`w(&&2xRlq}w|zI?hSk*+B}{XYVK9ziTI} z0=TbwI_lcxEiVmC-GB**?`vqzZ1QTnK9gh43pO!55?SIr7w32S53oL6@V-BIM*M33 zcy~ffs7MqBqI!1g4_WJbJkP+SCjmh5X?mLx*&}z6dYZ3?>(%t z(tgz0W#W=7l^rTY`}5`$wj`EJV5(3o)trtXl`N65F*(An2MW<&Lt~@zJ@CqEE=yf` zCU^^R#)lg8xnu0+rcju4f+!s83`B|wa{bE8MqCkcWfZ0`X2b^^yNPe|RArBIcP{hg z6f3+UjzuQ3lpx%5C!*?wFRmj)4)PO}7uhu-&T6hVmuHw)`r5tm)g!>;3gpHFpp6MJ zUdZcGpb*DPE^CFCkk9okoe797h}+)JQvO@&(B-GHfbGI@Q-1XZ)((*5uM( zSSgTvJ1SQh=~9ndKx&Dg`2tk!EK%EywDr(5BT4mKyH9lo?lU60Nkvrb7c>iN!l9$%>zJsms> za1L!4@`(+O2#a<0<^ufDQ zQhm)6P9JN(=G%fL$!+wLc{TWmQTnx}<<23O3;Oq?N%df`&K&DrHM~shTja#=YZ+-O zKf@0&ap4&Tr;B!Jr8$tVR31)) zerbS@z3FW|u6oi5IqJUpIeK?UL+B~gDt9NcS&RRyd25vV+FB05Hc@j@R4L;ns@%|X zB#ICg+n~qg>Sky;)E&JTcq7aSBLRS!&0+jBsSnX|pjfgMc_OOzceiwxEO9s|$~0kn2k=yGp6q}t zqLZ^yNw4&kbR;GVdf|F5L#EBn3F#N_XA7C=gE0)KfPkiFMmWPJ<8M`WfUrUfqjV*9 z_w93bHQtuV=7dXU69u^XTP!Rp$ju>*Q52=4G!s%m%P?por>)@=>63)~&KBbbWCYDo zxIyYeOt`}AR7_nVBm|vGlc>IG~EDNE(PMV7sVmfHkz|K zypX^RD4Xt`?rB< z_AV1%EI)!x_S}vLwfmlw2|W@=P>sml6p`n|Wq{k?KcvB##90GyG(s-KM0xL75G30C zM?0_+q%(yGoNF0BS)cVRBQ<`)>K8c!&1_y&-4uq3Fu&L3O1!lGpb7zWO<~OA$~c@B zkH3$mx+I6l%Umg+`YIS}{(l;+_ME}?cCzPCWHs$---2LFfrbsnvWe^LcqB% z0#*pAUc>1qMeCLB=zi^u0vXM<&zf`*0Gj1u90fD%2jD#iY&1!NB@4$WqGgyik$A^BQB%JgO54ey%(h!mCGke55cBuw+q8EFV@nYwiDbx?5=dPV z^hax<4W}6K_0x1E5wK6rBNRkdatFS%w!1{NZ)RmZxuT=vutMxt< z^;#y<)@*@XK|V;2iTbuOdttOKc%85AdiObbxVs2gZ%Y)MYD!5dKk$xR5n;i zW(%M;qQxmEK*bG{@v-jz>~qIbsa&5)tV~TkCQG}8dh2bcmnj+^7q6s@b6sJM6k`U6 zc1SfXMJT~4&=480ZeK4)t#OoDziy1!e=Xx$7c}{x6_hREN}htcRdW47bGOBc2alU$ zIFuAm0M1bhVBmo_8B}Eo8u!`K-BwauY71b*fSOb&X{R`Cbq=CVMSAXf3Vi4RLmr6K z+H&&(JanWM$sK{Tl4m(DP4@zPT_{=?l@R8~wMSMT#!`nNy@+hDJ02r^#VRE-IW%_U1+#U06UTVr_h8N(Lc-FruoqS8 zqo@Lv^A#VU7?{Q9!a9PSMZ)v>9d@CDahn)J0!l|Zv_>%LJlgjc&T&nH{)Zx zh4sUqiKZ$U*#e4Rh-DdAP?@$X@(K}XucsTW&*uhFjR8LH>;GZeb0OiQ7lOlpg^g!I znS~6t%8IBNO#p#}at-6sq3XTB!QGqzBqWcPf4tC*(myMgnDdG-MM{l^YTvO*fy zQ9P_@Y-S`=wE-hJ*{n9V^F%kT zP1pK?(!9aTy_ZVBNwl3kk1MP+;47R>PWvh$su&wZK}(UR6H1=_e`U|z^SaV|p5gPWILWu~yYtYD?U<`j7CoCD-q0rOsbE61AgD9`&R zNEl(XiA$?0{1K;b&5r`3Xc!$-W;54}?66gKK+2k7``%?^2L%8$yFW#8n`-2r zGkjkn&f(2I`RY>A3d$>07`!$Hw^=;@ithZIo*Pc!2cERsXx;wf$b2B94o9EkX|+Ofr} zO@vb~rm}AA&isJPi;I{hYG#k_x|(>N%`kWSZDvgKpR=HU(9lY*P>oZcY_IRQ6Trrh zL&zrNpQIwlvr}&h?txy)V2xN%D>5_S6JuA}=brf$=-*Jh3>k0Ya<}y!Z6MU(kmjfo z;2|$Sy$>c31tZgn_hz4%Nc4b|gidcmEC-|jYZMTyQ1}a4>YbhnH6>VvrN9gcNK;e) zmuB>^^BXql_;Z6KQ=uEt7=mb-e2F4uLddn&HH;BWjL6xB(#hA3TMKR47 z&|OE%@&4%~*HX&{z*YpeRgJ}H=FQ6%7qf(-iHQ;ysFHF`yi&C!2zzsFC&DBxj~ftB zDI=BGl7mML773yO{9p_Sq1cUVCYVFEUOru+)O9k`2^X$^(-GO$0%Bu2V_B}>Sys#w z90H0`f>odt7)#Ro14tXt*4omF`=`eX(?V@$4#7r!NWz-7XF_RC&YPHM4EQ)pzh=%6 z0nhQ7ff1Qw3eGGKNt|q~u`?>ifIq(TblqN59n^fr z^=M>83R53xns2jnYExwU{uaJOTM&KI%q)6AvTtOw?;;^H-d3Fh0b1#Qln-Ktq$0lV z^WByibDwHdT@=%Y5(kAm7E{Pq3dpb}Y~p|rv9lL<4ZMmK?l@@^FJEBh$Jo`P56W)w zBOQvBtJC-n2zrS|X(kbh);~cO{<4xP8!rPtQFQ9~iDB%7=$Yy{Z$l^fUx8?b&y{I& zTbu_A3-fkcS?_l=HmSxtQfT9b)wi*CFAA*n)9KMdGlBo|doE|`k-JUU8>=2aJ%rfv z1e1`NU=Q(u1fD(w9y)}I@srv&IO85nn7EJn$p6%M1Bj=-IV~ytQuHvN#>#=*Yjn6n zVXgaW{*Uur%>P@qxwJqgjSKgB$L0cB1#soi%y001!ek|{+{^BR!S(D+b~+|N8<++F zrH7GZe)gravWs)xYpY?e&PHG1mMY`t2JvdH_|LA*MCn7z^a&yn4?#N%>6+#o@ndQ3 z;LZ=iduz-vUJ~0SGPywJJ-@~bN0_JiR2`?3#sLFNyXw}H2toqvRZboW!fzAk`pWhZ z_7Q{(4}w#&%&f26`*#8QYf34l zy9YBy*4M^Z`{RmnHmZt2*`K$uCz{tfs3|BA7!*TSoSnMeEkv50oCfQ=YT7v*Lc> zWKKfW@rt;3w(uXH0SSUCdC+A+T#tM$9cOEEAn-XfD0%b7-1Y@9uWWa`P+vLHEcjzZ zp(Z`V(S)=p?iROsnpP9W{q$+AMiP5qFNz?h$ZVu)sIZ zgS>2r@;H;V|3)qJoRX}0AnLZ{P+wz;$b|)WzEwgmQZvY0p!kIT4^HifoJwhYVB_4P zV&rfj?B2o*;{ZlS^3ZzfQJ^8l>5Bar1RtldC~~9_dY!^1H4eP$RJZT))DF<=gQs^n z8L}acFX)NPV}Toy&iGc|ZI$0R^>ExgHLx`9Atr_R{-;bHx7h?5G`;!%1=gEq#br5! z)tO!R(zO%yh_4ECZjOE~A!;i~g?b945M#XNruft!YAhfU3;#FWg@Jadz;`R7$Y;!M z+hRF}1qRNOY#EI0?2D$HXx4BojMq){)7p|+tZN1~>_3Cb4PI(H&Uah@9g<})wSUz^ z$9E||GbBD%xG=tfAw?GIM;(J5gT$2N@8;~;6>s3S$;rkvoi83hUq;?%1MW9NkeYWJ zq_#yEpYV+?)w0KMh~Tw6gXlQlCy&8zkl*4>lrIUs_o8AmXI~0rr_o-l^W*2&KK4#J zO}2#pq7kG$XSW5`E_8lSN2$vN)&Y$fG=gh9hRSTnOqA^y^1c(L#B>1;f=(Mq@^e_p zVBkG76_orbY35c#7eC9J}?6yJdq z01qL?$bWnW7Ze;z`Y|p|Bb8xR9KngcAK!ruOmWlkQNrKWuLjq~xXx|e_2EzN1S?AqDtnVw{&vTHDu z?yTQrklxDcZiIqZa?u4s^i(KMn79@qHaT$tmTs5(Nq^(T-yJ#zZvIKNmiylJQ^W`L z$FSA_xfK6Atl9GDx;FvVxd6HmCLqv9oU4nxqZI2Cuy-UhEnKOzO+Z<=jjMBMU)sIaEh9dQ4Yiy+j0=IIxiHEkpOvW|zn&vvw_TnU{0QCjZ7G0Q?QwBR%Lg{jq|83pfZ=-gZH!dzRIY$Nc=`H*OGMQKOfC~hQP zOOXhGCiq>h)HmHZID$>4;wLlfN;9au8qI2gE|N+4o(j++S78KQHVm3T=&;2!0ty_M zo*UYz9FuHK&|BJlD5UDCzesDLQ&kzCsfp%B8Ja6oNQ|Cc`kI61IoEPy0P&BZeZ zFjd>!e9n4U9E@A;;Mwd%-k3lo=+`YU?7&fS)cm5xq?CDkeC`QjABo-n7X-=$yRugd z9gAA`2-~WX`g9JeO0Q7<80-G5nqCw_$EUm@fUb?BE&pbUz8V#^(9@p=E#j84r2B2r ziJ~;(p^{9;X~&Qo5Pm?48Wp%uKO-Shfrk5Cfp3IYS*ebtbx=jP-DvSm+TtFDGeBaJ zk(uKT`zs7*^*t?M&Te5I^%i70394rNUe@l*&Bqd=DG*0BaUr1?dbtZcwI6-9;=7_{*owC-{<5sIGyF;;}Kzr%WZY_Z}w+uas&=uiV&nVipxmn2z^X32l1E4DcS#k? zmrk&e3({xFCX3xy`2jZ1ARur&NYxQN8#Jfi=xpw3!T?6IM(8R)>-?2yEhekRGS`@-U4h)`?BPL9$nI~o)lRwZQ6elAc6RHv2o>7h=3qzDsYpvx z^$0EGkXx?hRqT!FD3*xQ#jC0Opp0f3ol{&i-#ezVAu_FxF$1Au6fx7xx=((lLbmJq z52BJiCPcs_N8Gl2s5YiNV>pZz@Fqr8R)@ykWV6ZuR&5N6NHJ}}((d-F zX?+?m_mf!piqXsva|w2zyq2^4jNVh^7owO1+SjQ769EKBT>eZEM(a{%^5ROV#Il|p z`RjC^*7CF(_rkdOPC7@_FfQp*(U^cS$`ZTXCG{0THGwKn^`+L`V$OJ|P3bCrX=kV0 zSLwgkF~;yIW*ViWZK|JCRYlDEPAb++XE(uu6U+v6$qa-|xG|Sl1MzGYk{+?qMB@zD zwuKQk24v7fR9|nt;2SF|;_J%(i_SlH2-~WdDr!Wyo8>er-DFIN>*EK>*xa>mrQ%T< zWkAE0Zg);~V*Yv-7M#<(XY!u%(kk?OdYmR!_Ns4!5&q^M>Qq_)66=Q3Y`)H#ayuK$ zydO|`-}3)wg2nyv0CMzYRw%lYzVh08a;hF>DiB0yLxW*QVoXttx*Au7ua1;cu& zU!8qcvnyOUt^_%GtmtdJJz!BUw5#=~3?@fp3{kJImSFvNG}J(s6rTiEE5@Ef$z|Z^ z=V5D|yuku}m{B(9LI0?l0BxbwU&!M{AwuM$`jcX^c=iL#ADR%5slEtvEcdve3T>rv|Iv?)d3%eKE(WAHA zDfcgwcbYvc1N4T^*UZtz2J1QhGnQn?)}+w8N$6@#>bhqS(SqR@`(mHIVzzX%;^~I( z()*JfoCr`6lgRBKeST53!G@ArV$+BRe_b^cx^9HGF$WQ+pE$31Ld2iaQwY$8oBz6j zh&8n`BH!+T^bs8QtbPW~84NP;&#Hu}EO-n29XjE;7tmI(b2`8lT@E!m} z>5Q+1c6af+edwy48X}M4vwTslQDL9lNg%$V6*PdaQhM+alz+*-HjJxDyie?5@%!7= zY88R`0te%N-)>TX2pjYAttJm^mzph%h5lgw3}k)Q&%seb@e9+(xR=MWwne4v?J z54g<_Pf&!xp!=IXP5unXoI=bGe9pf2t-#$FR2FjxTUFNj63Q!m9^(D#@abd{Z5V>s zoRLOCNFy*aGa21w5pRlBT)nJ=qZ6iS!y|L)e@pl1bS=nK z2Xj3(+;#JG2R=GrfGr_(5u304(g7-{YM97-HWPu%idqn2#mIE_ci{b5J*YFVX#(Rc zVQikCb8gi>gnKs_AWbsN)c=C?f6$w|j8#nDOUF;TQ@Q6Ud-j>JWZxz)pynm{cgvcb z8v6{T`QhARISD?o-Tp(;Wh~fGsn>X{lqIUm3XWJwQF|B!r$b6%rIH%X0?ie`&o_n8 zAMUrMBYctdgnZA-Sy*~*oVzTy7l*ptnypv)|2fte3aT@n5+5q;a_NHzcHe*)7iw=p zmwv#!S=PX6cFtLQHh$n(m%p7H{tb_djKVtZjDOUSoUFbAO}fw#OfH+cM+E3(A{WJ< zUs?5~M_5ZAdqQ{3U2Y(TY+EY|A6~`%B18;EKfKI{PTP_J{EW;^WDZ>Wv~e-j-@UDm zvjpocDeK_|dHmHrPDWOb5Of6<8<^Z4t3OI>dR98xm=y^pJ19fdpXx?)hKVfhP5HC+ zTv+H|RJ!!Ti6T;(k+P`IXJv1mTzX}*F8bfcWg&PeK1Y)QbdxV}XW*$1nv4tMJC&ZC zBKSD)8FN9L9fcPaxY%;|j#Nid#Ci7|9uK$J%+;JeY{3HBW9dQ2Ok$1h82&0xN{ueqena~vuzS&8 zy`fcZNkq@V_O^B^_A9WTl`f{{l;vZ6g%UrgvROE3Kv^z)8{2Bm(2VL?hFQ>&+6-B$ z)7=ok5dgtG9ky79d`u0VhYrl2tJewC?r{bygo40k*DqwpULb0*wN&kK4t8(NIV&&Fhf*LVSq_byd8E+((*v;g8XYMl;3#D>bK3S5h5v%(>V z`_{GBWvA}zmN$MoPiv+dAegxa5hsb~8CL^*rrl2(94v~h!dDD1bA(xdnLsn9OybE4 zqL#w*yYl3!(t;IX-Y$Gy^18X*K=}CyqJPpxSXc6U=8c7OuzNlga|&lIE71P zkpPVzkdLL*O_f*dLq%}N>{aC~d6N5FONg(DzZX{*OLQ?Nns`GHBP5H94}5(;M#}yq z?ZkWcvj&c=vtg0UR1{C`6j*_I;TLmxQKxa+ge6>!PmZzWn9y%$=dsamUZHGvW+Ks` zFlYR-5FpolRD--05&!Yc%3n%%%RrubdC za4ZBeA8mK_iOtMdC9np|Xuk#Y)is~(bRSFh382|-H-K)-JTA{K`0BU^=B33=*N_2N z^>K!$M+m*>t?d+_Plb9kLo7|SuQSPMTrf@4i5=bKuiRU{h&{+`oFC7_u$sMB&Ef%2 zIJ$Yg%4Kb9nBuv>v`17iBSnO<8#4*zKcuu6t4fF3dIk;{#M=^24&iR(N-{tzKA1VU2q}3R0F+|0b39cJH82`%PA>#YY#G5|f=Y$W6!d(0)>@MWo zY-?dMb4oH;qf0e{fB}fa{AoeGNae1owh1!fkGm)#`~Q4EGr$?pGtR^xqlv>@T=j=y z9D2incAAN7PtqQ;#*-DHFJk*-ejcS(Mkp6*nYvPgSYS#1Hpu=9H31I%U}r8W(5|L= z7Nwh8%DIyt1R}tFl3HopDVV9di%$|cw>M|Iy6N2kpVq%M4-igUTV3=F4iLl@0YpwYCQ$njyWRv zh06dg_r6D#1QoRCPUApG80Y?3Go=wlZBiA?;I!xk1aIB3MT_*F?5@U70R4^ve1CcyA+kUJtr;Y!sMs5>^fXy5gDCCy?47r0zIB#j&QfLah@ zAWzt^g?IXcrTm9%?BBs@`OE6PfcB!M=N3uusD!8uhSOHV(D6l1+S7sp1p9_-5!4@K z%=qruJhjf*OlaP;8B>|YUDGVn;}k9yPLWx`51QXo$pcC>Zx3cf#yj#y0-+Eu5^00< zPk=KRYjmH~6pSH9oW9j&%Qf{$&e}YAeciu@FI#zk*rM5k#C{%$W0bM+>N0iah$Ova zjjS_Nl4fS;FT;4%W$7v+NCDO!@95Llj<2)DuM0RF8mf12}`+Mu~Oa@M<6-LTfe_NwnDr}K`-_n974Z+*zPR)kCfFcf-Fr!SY+qrreyXt)SA^FKo97Me+kS z9IDy70~bO&Wz4aXpj`NqQ*{e~c4+SF+ zZWQ$UA7Xz0xoSGMl-cAc->$mxj~*VgY(GH;am!XhX`f!rJlKqw z7{!E7#^Ji1imi_z9*rZGpd_T4d; zAMvzLolbSm94WZh_clTJb7JP2n(UR5+b->;sM*0;3ON` zJj5U=q=wm;7&2k+WYIkK-M+ZHZXl~%ljg@>I#z#BR3NANDRg>UE-^loK;|jzWRr=< zTZ}4ZQhb;0C4Jb8gn-;&KXfT~?Gnw(rq8Tq$_A}JAEniEp!EQc_F(Z!X{tCFXfCNN z$^>p#twef4?AtNy`(u}%s=dg~9ZymUKJRSq2X1S$G0BpEcf2=mXAN;al-K>j%K%G2 zw7&-G*Vq$`mqzK-an5W!l+f|D$MUC&RN&gUoVCz_XgHeCBO6LX$AR5sV*LRGJBN8OQv%v@}5t21srhNmSR+FETY z(~`+iLR^5xkme<++(dAW@bZ(7u&x$+g~Da{zDd@u4+dKCvMN_=Sek8H(K4R=>8C>m z%E?VN>_RXsdWFEc>ePVgQ88Ne?##*k4UJcR;MHX$YMkEBZS8|oy?j^UfhZGS8Fg?vuA1rhCVoAmC>K@8Hu&N~8tsq!jRPaxEEXfVbb3dl|9XXxX zAm~j^MY^d0^^m7IXoUcxI=HO&J1WAKb8Pv%TdAP;SJc~p^C*0=0)yqq2qD7qV&D>0 zIIPvc8Xc*k{ih7f#_5?~V)lmdI<>L6Yrhty^YJ2^|%bYV9DQvRSclkcaP_M%}NGQ1;?WR`+@zEjSagbI0neIpgLPh zUew^XiX{arcz>w05RL>MYHq|;zQ$42GPPk=FUN%RzNFmwyxN5fdI-EiM7buKz*7Qy z%8&*mQ4U<-a%rV}B3)EwGDss#+Bh#z#yf;IQ0d;05Z~6k*%zPpjaad#YnzGouf}ng zq&6F>7y!YpR{#Si!8e@f-P2~wPSm3Ak{MtT@P_My{&XO0R>yjhBj#y7E*qY)>iBK$}hUZ z_qSs|sf+6TrQdZsQZi|P@;9K`i>a%Pkn#pr z>nLCJ@I4E8B84fCAHX@$nMHaN{*>pvbv4le52_Ri5F$8O&p<`6PlxZYVaJgd)o3W<=lP>4zXG9_qKGIkfVG5A4;Yz)1TmVNq{U4 z^MF7lGErymverz?bzt92b@O$>y-s_$M_UPT2~z|AR){c9m@*)Kiw%I07976qo3 zbsJ>rG614SHP*R5j3^cyQiu{ks%@6ea4;z^64lQA5vBI5c30R18)xB@3aorE#-aHI zL1fx|K%MIR%5GHmMj`Lcv|;>7Pg14DZWa5I&Q;2V8JakJ_J#ZEnRP!oOI$m|0(*)bk2~631LM(SQ;61?mTp%7qpJ zs0UEJPB2c#M8I;F6>FITw=u_%O(45QI}edJoBC^<#d4%U(V6F4B^hsf4k0jVZ!I^T zFJxXsLi>KyANPSo;<3S=#Os5o3Z6-1gN+=ujeP&O0eyG$-)akBA4-kMu-7I3$I zd}}}4Ldcg~{!_rWn?$H5j&r%r$<|dj(yUKdMJ~zgWYnd9OLcNrcWnos!c(Dx_}F8r zXA~oE@xKjX!Kha5OiBV@0pRRKmIz95rDgT~8v#WeUQk0?hFo^q5kq%K4!j{%gqvf% zP6se!T?=dQhg*V(C+%mDK;DXGFH{|naJWPLsjv$2cc0;rFe^E4X?bkU3^5#+4;Bbu z(m|Ln0<@4i!f4{L-Uw;x!GmEn+fp#P=mf*L7cx-$7)Zt=j(UB&;awJIgi9wHA>i%R zf87_LS5fWeO7iM`Ns~;FMZD(Li^7~VC-~kQq&F#>ykWejc;8R6b@^B)1$+S6OTb=gWKtjA<^{nlXGZjs;>T$61a^W&+(i4s3(|du_pTlUz;a9~*6+CU3m9sijCT=( z#|Qx6mPtd7)^x}iYihB=*gML5wIg0$*Ubf7?;Z%;;YVek7R0iCNm}fG`Q4rp#VWS- z7ePB980O)_P(?#5xhlr&l~R&nwS^?uA}d?krR^&qxFtcXWXmCR|2AA?L}>&rjLwmo z&=GE#XpI8^u_>vVJ+FxTsVO`4|KRJufCxS5pV(W$j>mIE^m+UGSzK$2Jk^G>zL^Ds zBUXiAvSP(}4=mwp!{G`Lh4W>W!AJIW9 z5^#mDF|P0-6_pnfr?g_Pb7E%4y{K3b` zmLq;hb}P)nw^*GpT)UI*%BIu0JwXp`sq=ERr47jN$#TC&7Dt6EB>GG!>`{&L%qPdh z(zrM{Tfw~m=%R{@ z6=A}|ewGRUB}?7Ny0E~i4VN4R1VwxLSNXk2C3a*1kP6;#E&5?Vu7EoB4N_}KM6=tcWeBKvfg$(&YBGrz z?Kkwg1bu*CH^CJpA3HJpux!g5sV-IGCNIxX+Z3WJb+<|Hx74RQwG>-Chm#(}ZQF96 zu43Jx(U;GaTD}Rf9PXmA2Q^i-HJe>i2JG_Pj=hk6c=;&1(+&Q1I3+fyGh@2oMa^M3 zY5XJVl+fRq8}FxnZ6PE6FV!4tIok#5iX>9D$!H`L(;9+1iKjk;rz=-gBo!s*^0-1l#4D!Xw!wZ+h0y-RrW4|E{LRxavVdZZ z6R}d&gBSWB<1i(z5T%+^{)7_eNTHl8!^kjK!QAsu?xhnqZq}{voTj=~%w)&?Y+n$NPpZu4xA@Vw^d6`fG}0A4QUl;9R*^HQ1gGe2d2RvN^ZfWT|}KaM?~+K94HsQyN37Q`RWN1t+2Oq z8HHz+t@vPP8n|7GEuSGVXyivdG6x|}4A4IGB8Xsu;W#DPjEpqO(~KiWWAC9_FxNY? z6_HX|{Y*>G&MNUaM3T1f3-q87{Msf*^6|wHMOR3RW!*lJaeZa9dt+9!yrAnJ4t>ac z^<;gw$~+`bpf0W-Z2nWX5v)5YtM7rt0^#uB`nKlLQ)LvW?Gseue;z<>#BnTZ>RHnfX=jC~x?@FRjHHM9hj74qvubn6BPAw{2Tpk&vn_6M^bS58^d z$Wpu6Uq=|fe$jvgOXVpRAnJ3fp65YEFHXOvBAhG>xwx07o+KVNy^CdiiB{r$G?bK; zUiWB5CRV)B6EB+TF_SZCo5ig=oxs=>ZsqCTe1yu=CbZFQc`s2cbeA%vF@r?bUmqSg;y!{}3NITG zC>J&Muk$Ri6M3OsnzrBWfWN6}9(X{}g*JiXGI!MJS z!jS;i+q|Jo>)t-FV|y{tE2ehQFCbzib}<~}z&zJNSH~YDM!#Mg(RV8F>u-Gc3&a9Pj{?87oH4()H*R9a(mhqY zmKC8-`*vlHt-rQ#2#KSvujc@Eh@IXk)YMr8(m+_0C%d%6Wqr;Uw4}HND?Va`@^5dN zS7O}M&tO(>exwblVjHH1IB5vRKWg5=9w>WxE6t#r0i^-HV3yU0oG9ecjJ9oHfXJ-? zotducHBa|qwT*vQ^Ap|!$U9}Q3O9v1G0{r!T^ohKl?3R=8zTbE0a*b+8X8D{6?pjcGN~8QA#bI9PDh| ze@R`Ge9U1jgwt??7yc-BBoZbfmt%P2Zpj=#;fi22#1L*Z{T>xcBtuuDLduV6xe%>9`h3L1e+M z;glQm&?^Ff(bx_j3!zJX)*Uz0wbWJY*)fMuZdjpGv@}sHF?`aP5XH%BfjPXFgF6IG z4wMdO*3_~Caie!(the2r7oU)}6+7Kx>}72$;*3sdMFELQFSNq~RJOj5zeIb!<7i{t z|EXGQJY!)+EoI1Sq7{cIKcXB(gmVkZ61>hPy3cIgFn~+c1EU5NYVI0(u!>pX(Ut)l zyqMuFn}GFpdP7piTFyU4626y0mRZGfIYHT68^>vb5vbwpA=Lz-XW??H7NmKhslu>s zf>8J$mZd5kq`guCUJpbZkEuram=+(#`uxCdmBCMI<3{vfacx<3X$YuX{7|N}9_VNM#VT zO)c{1^C9w45h2%O1UEPSP0|gq%hK-+nFUsus}4w}K;=$&jFISuZ-6loQr6-ufG(7x zix54hI2ryHz@h6**~9B0wo-^ zO=ZsqZzX9{xN^(xRgZg5RS|aq)t7}tBgvoDGxfkh74GEEj&Ve&$U}E&zivyC zE|~=&uD8Y@9d(Nj^Jc_Q@QU`@T+!Cj25|bmey2|82CdKd__k-U@WOi`?N`GXQvTX@ z_ucY54to$B%#Jb-KmO~q33k%o4bpsWu?-kup{^zmbt7gvMs#up1~Xj=Nh3}gm1eqw zIBE^X0yDN1bcdefT6Y~OLKQlgK^BS*N#*nM&s_*n!x;Cy!@=hMlx`BnWAZYGhQOUV zBfmZO5u>mm0}$bK$T9!1oF@EzaTX@4*4h)kHq|;fLUv~Ha0cqsI}l0BcXBBH1mX&l z(T{aH@oz;?HRtE&_^sBW#A!Y6^cu^&T*#w)91&&2s*j7_zW2D4w{F4rrozjcCVpNT zqh~~fAhRVt)&e&Vbpod+1UXQvyfO6)thY0{*z#3!z{z;Bhi^OlxZV3WEe$~~b>P&3 zHk;BCaxU;OPT&6{TYhN~21HpMw}PzE8Pu?0>>7ZW6c0_O#*wCQ z#pNm~s;qu6EGw9F=^~VR|i|1Micbs#Mw=S`mEob*N&95+?Vm9d}eY_5BSRhJmhQUGD zbUlFtT_05zJuhzR?K4K(md8ZIyNBST<4N5_Ilo3MYYw1fHo$(e|KF3sna~PET zS&;D;SZI_Q`lP|;Pg5FA3lq4+TV8a6WIRku()vVM`4ig@yi8wd$W*ou7ZMqsZ<@A;(W9wekcJ_DSM&tiXD5m020obk z?8|g_1rK6@Q7rQ2Z5VbZZtTloWk1n0dfNSN#Kdvq9$7+m-hBTEo;sO$w`;-l=cE~h zt-!&*t@hCEiaG;LK0evU1FCa;MQVbv<1dI7l&YJ{!+`b|F(|lm${;h^qKG0{=_#et z_0STqFG@z&lmJxHa){iHt~c^x|AUwe173y9LUiW2$`;U`Vu zmNF%EQtVuqB+DIMa|JefK;VxTLVnE1oK=x5vq~tyAI;>$=z<>bcQrFRysSV0)8cDh z6*hgpF#7H;o+JDBVx36s6MLxA7VB>`y)d<1=-j`U&Ri%Z$l6&FmB^@=lj#JNecn$@iK`2Fiwayc@;9v>(UHEbw1VFpeQj&?Wgql$vF^amqeiv^$I@Sdd+u zR;$E2bF8|K1C0x6s3i|otr-6ne`z+bkiufI^PP~@B-~hh3p2L_+4plV0>OWk0aNhB_EGP-oH&9T5XTh+FH0zQ_*5 zqx1#LC+k2G87}s_!6LJ)H$?St>^jQ=3u3jLvx?5g0nd=Y$&8wkdGke(=RZ9_m*cPN zWZK37GG^stBZO^xWR6IFH`ddr(`$9uF7oiGN73roIgw+1e@(+q=Cev_MOB(qNac5F|39NM+flh#Xfv)$|<3sy+PkWI8=)V>EHzzy9+G5dXM*iM{ z4K|dbUMu-lEFIUiuTv(xs?IPYCdBbinW?U}Z0MEs-#`p!hX|_-i)Tg7?oDGr#Sa1? zVrGNQo`rm<=FD;4N^-a%s5SxB5wUVY>OPFpS8wGCJI2PR2WostD0+xshmon+fwczE z1(QfMByZc((OXqB^~<*|F-C>+=)H^^f}Y2aoK{At53mW8XbcKe*03PRvb0$@w8_~x zXN&DK8&+b&!9!V3Y#&8OfBekN^?s(dap+Y)=y|qOve#)Mridbucd7kBLQEa;X^*wW zR+*7K0ld&mu3u;GSVoHXL2czkKcL*3&=Ec+i{pz({uZJP^aZ2nZa2TD1$ibV!RQ@? z{gO9}r7fTSH^xSEDZlyWfQDr3C>a8e?$h=FBgNKyzXqKp%Z8d$l!*x?1n4Z zDN$POAscUX#=H0Hz^)m=S`FPhXFguFkP4=S61~LgfQ>uON?&0WyWByycZHF?82gtdeBkTUo5=0lJouCNF!6;tXQR`W zUxk;RhzCo&)buW(SFtEc1cbEKv0KTuCpc4ctuK7M zL01uGZ(~;1&I8!Ex!FLQ4r34r!jiF3))ZxczphF#Ibq|pbRd)97XEF1dk zeP%RPX*`xNNMXOTCwqVqlRE0e;-#?g@7xybWcfVd^F6k@Ef~O+S0-4qYjAXx=ycX{Q?>+k9J*E1_hO|yosn$t`%Xx~))`F~}F@ zV9n32t)EWB<5}KTs^?s()mz47N&H`7RJS+4IY|vZ0z0 z8V6C745~4I-k(3q=%*~q5>q#~p-wb5-H?lPk|0!n9kTAT+COVaY|*Q64*jO&2Kgi< z9ZTKGaMbt+di#5k=}%Jj?mRwKI7-qt_0_CSm|H3G!t*(g=!2p(y~9hQtjY`Cg(li< z>3^?B)*pCXQj$=>hE%*CY*^|gI5ke4jQtoQYY;sHt{rKf70ZHbKl|70O{&;in{a{= z_P+s=GFEaK`qN7Vd5NhP*)p?P$`wKr zqck&c@j4E2zoUkiF+jLhauz^+)n=#0(pF)=d+f37ER37jCEs-%p%79l$s13+~Z`l zj9BZcPMpKlfiCvx8su(Ru)Eh%(|u=muO4Z^5?EXsD&ehyHo*LgN%V}A!H!d7T1v;` zIzjMWRPi*jWmr1YVHUyq}n-1(=FV z7bu!s%_gHUtU^+{U{G-q{`$~OlC2e+B7Ae6-?b_K^;;8t{3;73V5sKRBMrk`e~5as z|GT3Gp&#Q7cO9aJCT8;jiM0%CgTX=uz0D_yPGL0yXEIOqIAr6Mz7O+*bE;UjfW#4; zH>V6yfbyI@q}R~IH=_dkw#rUI(ehWV1TQ`FSJl=alJ*9SIdFrDBD7M4tU9 zBkSs^z26b|4+8+lXEVDXanHh@mUT@Z^*L)ntC{udH}#P-ZP+=_J(A%%LOeTa1sy8wi5GsmeBFst18j@xa- z0QH!K&FO&y@doG^BgWvl`&^*lktr1D>wCj3aFI%i1RM#Xx1Cq=T?X@xEz45?!`Mwm zx8cDpWwXbz;*roMlX>K|a5HSV(r*EQe8VVwxQ-EcPV#ZMXDda@{;~iwXru13xIA z>mNBlxf2%Q*}PB&P&H$Rl@q@rs412}gAOmKoMv?7>|{0`4?8IRp%8C%F@=vhw#jp` zs*QMwKw~>clqK6PV95LbP^W}!?Z9d%4P7NQ*VBbn5h|Ji{QgC_SQaTjXZ2j)X|ce3 zIY%&Yd$)uiAc<^b>)*aT7*bW_zby-!Wxw-IOQNg05!hpa>W99hZM)UY1%jz=spT+V zE&renrjJDI!+Qc+A{b65_yC_wz%LEhJrdg*g#b_&&Q;LpxIa`bx=m3USI$1KT)Ytp z?>QUwN*{tpMkHHh)@~r|a1506Q4d||<}Ix^ZG#kXYI(dHJtXV4v4+stVYi#bwnPV> zvYq1usMHkSEMEg{x>Z2|tjn>QrMll2PvXJ2M_bg=$2odekr@+&-}a<(J1T}P;^aqb zih$O>Ah(!KolEFd73uUV`zUw9LNJzY#}pM=+%4A9FTSido$BpSKbae1C)Y0a5Q;ga zJHn=4!U&H|B~!kb)Mo|RfiGH2Z{0qob<`YqcvMd*+_`vu7wj}=jQZoW-c~*cLm#dB zFn{+iRcMHA5fs{4=k&gNYy%rjd&-tzF1owXN`I*!^~V;Lfp8b*N%8biuX6z{tjj@V z7fa~#B}eiX@i8R^6G~3ke0jruSQN1`VX*#NL%2*YhQl_`0?~d3P zWGwtOf=4jx#Jyb|9>F3)wsa<;z0`v)ZztqKDJX!#^ht~2T@213swB#wGi2Jzoc56L z_0!V7((VzmTIjpltLgB993m0H)>5ev60W3r zqZtN$%qpkWtw^uIUHdWtoOh3Lf18V`_I1hf<`s3xN0gpCl?%OxFN%h5OI;$^N6aAX zX-^P!U6bb4!B;9dH%7fD^tMsnl3N1=tG`iO;XPQMH(C6x5n5E>Y0~kE8E}a7S<+U= zFte{$uRPEY!2?Ng5anW_Jh(-IzY5Cj`j>v|e|PObHAS4Y2l|i{Vm_sKjRPr{CFco+ zU3CP$N1&pA0O$?6qY}zudElMe1dJ@%MlfKZXuU?L3D5Ntr$MGRtMi^9csGQTTtZy- zs&C|!VqU`;IECFac8Gv?W75z5+lZ+#!_bzC^}{1FR{An!k53V0&9b$Awww`R^ed66 zZ2R*${AFwJYgbF^t6?l0g=ux!=ahRhxKz_$^Vg%Ak>{us*<-C28FWPDCoD;wJiOR5E%(c%?KJSB*1Y@E9`J8ey4j`tht@o90hk%FdHDTkF|EWN zE2rVM$cYKtD688C!T*Xe01FuQSNGZPI)DwuRvooRyh8(E^pk~l?~NqcZ307}X{}Eh zo@>~kix=xm1?e*?O<&cmQen6N0000R+%Hcz*((24mQ>#ZuMOQb zsATVC*pEd}v>fV#dhf72zi;M#3s6xeBj3IT{*wBEK_Uq7E0Q2f53W+5g!Q#G*!dy1(APu`9yN160_8h&#kg3g31J{iWoZJ8#&-l-n|=t zl$Bqy5nu`!ixYfe_YA2XLMw)FFP+M(Ji$s|BmynM z^XttXDsKEe?miK|&rr0|0 zG-J9B-+j=oRAUuQEh`~6IqH9io*2o%!e)TwE?|qNC*!z1BoDAp-ch6N>0>wVYZ*Mj z{<3@aSOqD+!%gxttTVEY^60grdm>|;C=AAdAwVb5V#&-tX6(#LqI39x838*|d zlJ>^6Rq(;(PugpWn$p9wgJ%298qNu1an=M5j2W{3wgJJl@!Nvo5G)SsD5nmH_)FEE zF+;c3yV9KTEUT(QqPK$ws3kyL z9JJM+gcbjUYsYbFAkKR!V8X-|cKq*9MHa#ZD+s*h#~8~Hof8w~ z?5+uPvPbeyeu@%JXuTCftsnw6YaR4M%v7N<2auKYREs~|-l}-E(McZBfp*ruZ%YF%HhIY)IHrjI1;d_61fxWZXKCqfXhHVjxu0|&Kfa1 zTcG%ZxmZ*cN#@c2#yx?YCRuL_QiF!pdp2JBEG3%>z_{ClSnL%<7P@C}{8ms`4@kJQ zJM4+!fHyG2w4`a`_$H>$3o?ef6~N1Gi6vNBO2puqyqL`pGdKF{N8X)Wk?Qegx`g>b zxTb517#SRk>=Gh08iH9DN9DD44pNILQ+w2gI?e@zC3PMpDTRTMB8qkZL2jkw{B9NPa@f?ks({&Kxqv?r)1uh=}i_CF@2HwBLXC9(c*u zbLM3GdarvCQ3`$3L;~GE9|cz798Q1T2= zmd+1x5-&MN^t%Oi;IS?aNNTLZkq#}pvoe{((S44EOcZ_P75Yymw-P{)J+ScE=>*YNLeO=h1n`yJ6h&)9N(>o5{8 zaN%G4R$%bw&vlN2bK-7Nlz=U|{c~BAG?1SN2&V7ko14hNAWy5D$Iu)Ax`)s)L-j*W z!jQJU$RxLV;sCp`!-IJ<$*2F%*utsgkzd}7!D*uJjhc19$RmloA@}pJ?B2p`HUNye z2>q3q1$b;Ix+R+h8V5=}%`*Dh+pSXn+pYniS$6Fo6iGSIW>W|`@;`#iD1tWN^>3mA zCP3DRC{QlW=#zFeJVXq+kplYZAn=U{?&3FcOpJKQuoH}mpqiF%rgW1T#i}n-jxsOv z0IuMGR8Yk0R?sm*A+AU9KkTURV*@}%NOHM#6zsU(M|mvM&v>C&_0j9zt6o*RkhD8ytg7XZVGjF} zk|9O-3V|V#8}eatGpl8iJ{#nGd2KG=eOOzdB;Alxgsa9m`}cMF^nVAQj_IbtH!s|H zI#zRYWn&Nd)y*MMLuS=>&$`{q^5ALP#;@MTjS`Q48FBA!NGH+ktm2x2^$E+!m`^3d ziKzzeW@YC4eib?rnU{{uupTT z|JZ}}s7Ky_K~|$U`X-wa6OwBeDGc)?Ouldb)jP)X!{Y3CYU(j}D$n2%6Bw{QK zY09qVZ=ZR0l>xP`Z>a+>JXOH&;Qif8zXI5ElP@>SZ^2wJzKq1&3rhx-V1^)yiW=XO zLlOwC#$^*9b4P>2k$1VB-x#gO?%VV(V=|6~l5Vfkr9gZ*0*g5@Z0Ub{b%PGV{a%#U2tw`m?;zP0-l z0t@!OlCdCd^Dx{kHd#XGGtxCXHD*MR2a;v*EZMQSMda7*FSy|wCPj6I?}Maci5Jz` z{2pTjqozBQj))-JUbyo?j-_rJ`!ENy$9*lq%{2_UA2oH(0IQW>i=FuKdb-CigLig^ z;LHKfBuImCUs!mB4VdvgEW(ibgE~V68DOq5SgtlJB5e#2WQ5#BA9h+coFmA)11jq2 zf60v}mUE-7r~ZRBPu&4hNT*g&+z!r)Mf$9eGFS%7`h1^lsyZS@Y3|2;oaT6E;{IstGejU>_U0W+8eMsKs--6ktq*!NB^*%q;*uPm<_>_`lIN*X{r1J=6Tx$(&y*B4Bv0=TZ8 zGs)~Mq3xwNB&obu;bq!d1qOQpB&8a<`Rx77!JBPG- zjdsJjLjb~%6N|Znyee$?3pV;O{I$sEz;wAsA?^<4aN>Bfz?A~dldz~g-eI-6jIw8s1X_7 zF9XGLA5|rQcgfmeRg7N8FcTi@_AIdZP8DKq4}CeKVae8FOhkx$;qu04MY1DD70lU2 zHf>!-A=O~mQJRCyDI~ZyT-Xgy5w(044rc((J2_Zmd>JuvUAZSn>3{gPL{0Jn9OK{70`8cS_^eGXDsYpG2OS@hy}&Ti#A)wr>eJG z1hnAG?T!_>$1{uXkNq%mGGC6V<2OEiA_D?^M9wI4hFbiPU>7A|5{zIE5nyLqt$W$N zW{>a_5*wi@k=A^T&?*5AMn9$I*02y8^t6%OQC159;{wP(#W*kWHhw_A4IY_LQ#CkS zP5FoFvOfInY2!B8Nq$Oip^5|V6MAa`*a8gUMf$-A3V`?G`TgG!TBdQrVYu-Hu0xwo ziowh|{knfFC2`%YY_b9sDM^9Ru&1inj9g*1Nd^<1Q6FaCbh7@TI&^HuHcGQ< zP4S=lqr+*$GO`c~HG5na5k%E|eS)+Fi4v8+$-$rMJcr+4pm1F3rVKWtcbM7~o^y3WMSESzNT;*1cmgjwyWD%H=Rh!3gd8i&D8|dDK&GDACrf{DijCIFg zDw(w?u${)22S9?lPD!xE=k!onB-tu}LDx?q)D7V?V;noRl%@xJtOq!Bh&2LpX_+-> zX}i8phzl0h!YDSylDX9ZsS}X!YYp+pLVBEFZ|1$>Gqd6%r#gt#%9ZHqdSCgL0fTKR z!h^($sDU^w(S-q%r^FseW>w0`dYr;M%RfA8_7x0Y#of}kKmwhCRfZZJZvL3x^+gFo zqBjl;V4+{ngPVHhI4($T3qsOGKd-l`7qTY_UKb!9ZnjskcmUZvW63$1lC`hULuRSi^Yor2A@!ZzQ+D#bNZKmUlz9^Ck&5jaJg z;9(VDc47j8+QxXnNTaA0l5B z2vj7+77uCx?icTe zcdb=&jgf!UvDT?){>S|9;FIZ}a-3 zou%bJd^!|D07HTPF^EN{nWjSMIzu!j!qhE}4D{#JgOxJ2dE5Q6hCw$fsVtxsQJ=Z? zi_27}omBG7J>hgg_jN`bxRPPl3pmC)Vwpr`NY(0Jx9nV15CY_b8S-NaHGyy{PmAYM zk~@@2J_QS)>Pf1zQu&wMVIz@I@Qp=lv^DA}HbUCOq$!Ct?XaMK4v`+uRT?~B9YJT) z*G#!%sApuXhNU0CKf7R%Il|TBIL5m2!_Uv+9~ibx!;*-~U2sdEA%hXF+4yr1)M!FU zvdkaeHd%Hq1)Xv4=z~o{e#Xpb(g&Pz%>_6$r$<%h+WP(U=siHKp&yUjTr{$x(wS@A z>9RM5ga!~Rig~-nkpEEMX=AP?tIW`S)T(-Igq|E$&K?Co1cm5#N&}1qW^tcUJ|xQ0 zs(dkE;Y_CdLhUYL^B1*SCX1|w0@`y9DA`>*VcoQHiIvd%^L)b%Mlhk ziMnmgGenW@hQ}^;e*hfEg;@=`gIwSnTvILdnE@s$sWFUv?jCo zmqV+|DG`@Q+=Y@zYX_1|Of3l5hu*l^5^JIoKw9OsQolO0;Zykk=a|t=Up?;@KEj1 zi$*C$tTY2H?QG~Aavg6~^__Z&EJ10J6;(dIK@rBHmd{sDa%p&|urZkBC?Z(3(*?)o zq}6_k*Bp7<2?1|cPv9Wr{>Nfg*rIDZ{&)(jmdI8L9)cXX2&V0>`F)BC)V#8ODJ1$|i}J%e}q=wOgJ|xo0EixB!jG9rEkb<%) zy6~F+Z}ujCbg1*CmMj9~+I>%&>|Bd1h#=xapn^6e;2=H^HLoG7M>5hDvEtbJi`~Vw zEztr~pbugP>?wEmWx%RyHfvOzQsM*XP6xG zUmhejLV|u&sO2jz*3O}qcyd^+Rv!{BOr08)OK-$racQw_PoocXp^oXCb4LlFo%N_- z7RnXr+J7N=0-jZrvbkL|9LVh^hf2`K1xgFQ%yJ>POBv9Y##$04d6ywVex!P((E7aF zhBvAg?G}m453hbId)k)zTau$UkyJwKPaNqZk+z-a411yXH0lVV0&)%c#PB`WiZCca zNE}}k(VKb2l}IkMKx&!dc=vyA>|!N zwHjdB0t`m6B|i!>2h`QaK9OfxbR-jG60PiW$Kt9vd|vBZ1D)L}q=np*gAzDJ1VUqRs<-W-<~-oyXl z3C)Q}?IaPjxoUX-*Z6hK$p?XARDb~pusc|4KW6s(m{vMzP(Uw0Cm&?s`+dt`H#i3p z4d@Zooh<6TR8FpilXkAW%+Cl#H3v%q<<~}V+9ltaiL9QV7p`j_!>q{VLwUnM^Y!G8 zht$X+M%mymfCR66B7%deS!5Gnv$Hzhj{)V|l;zXLE z9#3F<;{IAe4%m*5&;x(ut4WI@GYoi^$Gxl#*MMw?HqeAZ!;-j|K&ESgP5qN`OJpR| z9SDr=b;Jf?G@kFg5&;T+P!fwd$cd`~eEouZUsfLZMZc6vFBa@IV`xL-x24gD`9RmJ zA-u}>=+JQcc2hnUFX#>%vcQ5_!tZ^YNdrHVVktep?F-{nRM2^+NY^q};1_{pT7n#IW}eDUOe8>nRAi_~pc2i{chS6{?g>lP zD$G4RVz~-a|F-^`8P0@VHlTIBa`qI_U)F=gq1QV3VAe~R+$k+=Ed~S`-sW#jjFoF% z*R#g*b{!H?-*VNS!KnoyG3R_mV!&m+jnRRQ=y;S112iXSvzF;^)Gje+TeB`POeX^c zC>8@yrLt~>wG3h-ocrYBvSn$sL`g#aGODIEvXKPN4R3wgO3*cqha?I$b2rBPa+x}s zb|iQkNK>N(AC9K?ZHnfxB?Juf^-Jn1vP-?QKLAZYvcGnLF%xEy$Oe}5?B<;>Fy>?X zUV*C`6<3~81rLeM@8q~zEv5kef`@s_Tx_Uyyu&VprrG&st(CCG;oIx)o)*`g67%a^ zw`tY2drRH-xR`B%c;y#C!eJuzLO?#1sW}E3Dn6DS{X?3}NOaQZ1twQrU0mY-kDXS0 zN7qtNx$SqP?ERf|+8iOa~1S>6~ z_0ECxWL%~U)VHv4tIbx=zy2-_f-^LmAkT!H=T?CHw~TDGXep&wp3j)1tWEPrkza?7GkM9;bg}3lFDaa=x~>vzoVQzNpfxl!jSkTDW8% zR&m+}kj+Iu$2@jfwh}5>U#wPU_^LDx;iLY9X`7pI#&vL>?=ZlSLk-Lp&P?;nGarpD@^PrFT{1mhbS=x) z3^ldyt3QB$ndC?9LG$zhpO)9!?GPD0cs8XJly$6W;hBbaJD;8g6iX4hm`6=At|qEE zJhZO;C7ogEsEuN{MXAkcr9W7D_y^;nma@9?w zILSThrmN#D`=GeH>dfOH?HX`?A>J|Pn^|njMRcPODlo)GpH}!0!o9uXvH8}*=Zp$E zea|5|Di@ID2(X3ynjpyGH&qoSN#;j01MLprEbM1Xpl7OU%za$ds#G_7AL zW{Jx`5kH8CkX$nLy!(~E0mcmY>9q(bUb7d;rOcJObqgQ0bMhSevl4!T?gkfDFUx%F z_)ZKf&+Q+uR+X?_6x*`$H5zdxX7OwhUqlX`MuOvlH^uF$3uHS2pR(!ZWLNVqw1FF8 z2)PdN1t5z{G6I5SdIp_L1jD64ET<3SLF0Q79gZHo;Tp(o9eqIMY%(&Tigl>7ea7UX zsL$eIgZV=)374_numAu60001}FZ82+7n=duKdQO3S_u8Q=7SuZ4L4N9TBYy)yR+453exnQ@Xf9LjK&@3 za=-CE0U|m*u|xiq>K*SKS*;uuQe*?j`l)zzz727U)$oB5bMH?6Zp<^Ks;M&D#`z9F z^@p<|x=N;eVDn{(J9V!$IuY(g7M9+$WV7pt^Uo|~aee~9l$De5Rr)a+l67@m6S zu-;b!&hqqYs6X6=Yz%c8sla@N3~VRxZ0mxn1SN%s!qmY%6MuBMVW zx--j^*50E@+_l8`Ugo%)D31}y3?%3RBr(y@mHMn)JtGBgjCFd?N_iphkm@I z)|vCC`80AZMUE6zYZ!Yi$N-0?QP>%tj|cq%K;xJkSm}j>bs1$IEWqfY^D-3I`*=4E z0zv-fGRg%nQ6gav6cA$FKSmrDP(81aK|%JElBxC!v8`oG1K`voZVKh`oLT+;?YY^W z{CjR`m;GUE_$o?X#_-}hxkx8bA%sS=S7*8LJm?)&F1FWf&;~huvku0Yqvi)wxd#wa zD8=|}V~D-bgC4*MQ7I~%uy~nE#FfM3Z7skwkNIfX^Yn%XCiGvONz08IDHH>>aI}|* z?aeXB2}JGS8IDu+wuuzvyOzJ2O99-$98G)!WwAyh1+;r;XK*}wZZ_!90L`FHCaCo0 zVPDW-?IPECUD>#B1SpG_wrAHReI_&f4;?)dv+3=tx-lRkbb8}kHS-Riu=1y5 z6zF;Ob333;+LNwW3U$4)EydMjow~r%hNb;ywq;krYY9a&fx{ujHAQW5bCEAQ84!=M z%L{IUy`s)Dpn_Z<*B6+CQp2h>uu1k50@|*Fs4nQg?K#r=;;oOzj)5tPY|aD-#@ z$4UPjZlWorett|SQ6jJGAq{C&$P@q_rm_)$+Nh{OO6q4M>4yocfl-8J{`F>`W9%UDU!~QSxG|skRq`|a zEepM(!69psvI?66JWPjca3agnMG;Kw$5r7eDLD|`fMWFHs+Q3XnN}WNnP+;Vhc&CP zFlJ&=uS0hbMjziKB~`YCehzvpT_TE%PWLa*qUcC{N^?1;1K-yy4a*dx1vO+z(#3hP!fbm!K{Sf_MZAt+7gx%Rjik_Kxn7 za7=GLGoBqEINk%I@hz->*xjB`8Nw2oh`!PD5@uN}<;v(~8$EJ%fP0m^_l6o>Rh{v zK{_fVhY}nOt4pUD6oZ)f79(KCRisi@Y4(NpZ8AZ3*y&TPUuMmcNLIFiISd(Xy$S~B z-XoB^hk0QGhze-B^NQKs%$z*V$a-b!H}x}dUBHKN+MLV*UdTQLKs7QZD$IUG(%sd5?^*8Q~vl;z5? zzx#VBz6Yr1l)$IHQ-vg(a<}ep(^0X`_=6OlN(5Pxk)2uL$RxF|ms+mE4gmLzZUvCI ziPae#-@Y+2lYx%X(Nd3gn44SB!EEiTD{qc*_Nqh{E6_rSr)-mt zX?cBzNYG}L2L*|(DL?;6z)d*KwgNc2O+BIU5MYuRxKmH^`3hM>?@v41S2L+3aqpY9 zSoVBwt|Jm#ez;)*EU(1r-bQVRsfur94vC1aVgFl(-^$j~*AX^6%APiD#~D5aE(Gaq z`!QgSh-`(FQV;s!4?Im;XzH70K$-T!x`KXl^E#qX!;$~5`6N(p?D+AJ*Q|~gQriMR z&Ru}k9^>j3s)zWUY>6Od$~qf(gS-@M2Cy=Di1|0c!NybCRD>TGl0X}(IHo-)fb0od zmbl2?z^GAroIi|W9Yd+0U;Jr}e{eQWvV-H^M1_7^59ylEYN3CSTfQ+F@qmw>IhWk( z1a6e5DDfpQ&v)3C;nA1h&Ly(%bHe5W-`PKZ0g{YuDg#5!d3?}eqt|WYudfjCOCh*RgZ=-mY=oDsi6ZxS*4DpH)?lSs|Yf4Bw z8Q*nmLd*|FRJ%3~EKRJAI#!_89UN5#IPX$qGbm<}nd1;_9Ca5alQf5}HcGe$i6eE5 zSZwa(?E`T#RFy$UePh=5nEywq&f&ufF*J!yu=jt@mOUAW?fDj#H~l)k|LHDmy%{_A z;t@dLgO$kLE-A=6jhNse>}+qZ2t*PrC^YL+H?h#Y?4Hz!-I>Z8oHTNMy-d>s=O;Qe zz7L&u>Q=On2(awr@tB!dy$#PlsR*)UvnGPG6MHe<7lifwdtUyk7XGO%PdJhe++;5+ zdB^-%Hv)|SNE~uhIp3?uo_8xC<7tAU%OkiN&2jfrp+A;1*ZqGiFaBo&!4U`fQq2Iz z_3Jt6W6K5fCcdu&4z2!vu#=yA{nLzhwLKhWb6#9xI-osVf>38}B#oDkjSDPiY4cM@ z!HD}(wGxTBmi%Kn5vJcN_!?=?)NK(u{H{fAw-Du@oPX9Qv|z!0l^)>=3^U^hw-RX? z)APGR&nVl5TIU6?T#ufY^X% z;_;+{8XE(rZ&(_&LhtrJ*E73#LW_K!IF1szpu3HcW>>*t{1-fTR`99{z~I&_jJcHk?KH(5PD)V@nE6ly4WZjb=K!z?joe+`F` z*W06)d&;`-xrfF^CT$|=2ekeFOus*f)FzUoEj_wNO?2+2q)1f8?-vK3YN<)Mq)>M%mJe%ay)3d1+;Ohv&?j3jXs{)-+s&k< zijXe_;e8)Qg6^-cmHFxCxnl?FwP+Rey9NzQoTB29^6<_zcPQ{?7vf-Mi?AXfXxN+L(==D$_M6pKkDX1+{ zIOFNeGwuWsm6YN_Y&-L5k}`6wI@RGyWOA7Ipd*+fRQzQ3sYhnPfcHILrosR?1qwOA zXa3QzVt|~|SXlkKc%JVhu#=k*KE1H@D`KE7A*kN2BsEK8gNK{N>nKHe#4w&I`gc8c zN_yEyb4|`VrbXG}Z?(h(0}df;!BFkunCzv1jy%vjiESj*zT@+Y?zO=8Fs6Sm`dA4! zucQz6wGIW-<%wjAIFLDWKbP{Gkb4Ggny<@X7PxY~A&8!kleJGHWHoNn>=0apRy&<5 zOCv`c^WzWHAba8`tS_y?J)mC;QUHBSgsXz*M)58oj%f0F!97cOj4|`yo%BngTzn6A zU_{B|biDx%y1+@bAP~(S^<&clDHQM~Hvwogre2JX(gpJ*ydSTJn$N~x`HA4D%$*%= zyo9$y7mqIANvie=P&Uw@Vq0y{FN|$^d{3IHe7(2$_&gR4-j}EP z=N~oKoNdsC;VDi#5(hXtZSd1e1vu7&t$&1kZq#}3mYf_bGC?H>pgwu0lWrXfGeafW znAxJ-|07TU2Hz(7ZsKzhf1DDQ*TgE8pp=BmpZ*(vIBJ#(n{c!%hVIl$)lx0(`FX{b}%AB3=>-mz#sL5(YU*S#S@n8JZi z!H6fXzyNd;aPMmjNC;GXlj{L)K4F5C{@LZ356kj?EiWJ^Y=Id`_}J7vIPK_Jqqfj> zKJy7sY86Fcs(i~B>D;KNQU8CHEpu^FVMNy*64RaShX$di=|h>npa<`;4fD;OOu1WiY`VzZ1-GYi6g>lzKq*gQSegDX8(h?g#B_uO!2WeDU48uHB zClLr{1jXGDPO$1(C`kpW1(x9{-z&{9OV=@%;ILOQ^O47M7#i15Cg5v;s7$5OaqTY^-HS#J=<^4nz~hxo21?e*HG<_L)%35<$<3B!KL zLJ09a73%z<&aF6PtlcF>)H5hLV~l!y%9HB`Mf*zQulm=dUgfdmL9-`{3X8E=`13GF zEOVvA3vr;ZAhGuiUVm?6pd@r`ODEY3hc$gjib*XiH{=HbFZixxL&o4DZo%L{3|Pl5 zOsEUVk*<);{`?LYm}JnfL3HT1(}ox;Wc-#8%U)!HL5oE1{rT9#^;it;>eF*`AuEfJ zfo28^;y!YjRBb)!Sk(W@O|5$4Cy@aizbRS(SG&= zKnUO(~8ufF4jz(nccY&)2!`8j!yyR+*t4!YHB`;*1So9jpNFU_lLxOgvYl_z{=Xt zUDSMHg0Ky*kN9Uo#?)Jcug@2#?e|_V|Ckzu!o|e#U#HZ?*4e(32$NQ)H9%uRW;0vy zULc~&R+LG5=}W~-1nXLGss~J2Hcy!Al{41v^TFjan77W` z|6Frmsos;ij6|#IDa#pgm}{qt-y{uptl9rwMAoXfgKUC&mWpI8{PKRf1Cz2U4#*uA zUg$ii-=gswr5V246?B^YN+NbhcmV3^9|xksvw$UhTNZ~#J56}Cmb>Wvh##@BcqHN| zYGl8?rR#-IVxd)fn-3L~F)`2_T+4HW@+KkicQ?#fbB52#FnVx3e^>W|Y4MKEGi)z&_?dqIHYCI8c>$Xn6 zoIOFeeUgSiiQ+O&UKJFnrLtWl23Yw=#XKD{UOn_1qiKy@j@8UV#ow}_zc7l1r#*cN znWF}JImdg7ndhHyy7SdR(4w5HhEYuS% zknSN2Y3-M~szzP{FYEn(M_*w|Rj@f)uW!TC5@-x}E{N1~XaNav4Bj2v>+yqJXc1^1 zpb=P~@^nw2s@M|5y=VD|)={{peE5zkZ=whXP~=knivov8PbefiB~a8=_WeL+*jH&K zsa2;`{gvw-gM5k)k{du4P|%3`dXI*m!%^#N>G3|J$!@q~B(-hiaxhNz>1xV4VVH`p z6LTN;S#A4=b_zG%fk`0vI_yE^tzHUsB!t@xYw6X4#-Al;`sf?$!N^JTz!_~+g5W!R zcDG`K8OyIjpIAu6P3TZc(MLr9FjYQ{(0+?{aW4Cw!e}4?y+CaYW->~Q!D=GitOadh zMhIcoi>p;nIGr7I{0r|!#46zi^ol* z3*%kJbaD+C;TaOf8q4LOsSGT&N>FcFPufIYsyt>xO6;#y-oHL3yt$uXfM~D(^^Whc z@OrOI?n7i%(VG~%70$&yr`4`2O}X7_g$OumOyqG zKJ+l>aoce6pZS0I_JY;Vzcr8!?(sB8@i?R{K4=35$zCz2+&YhcqAYd95qWM1i7?15 zu42Q=rHC_aJE%rGyfqN+_;s+F2VdAn!&*K==Y6&|4OgRsJ zjic_IXCjY#h0^}Iia63N@!w$uH2Acibsgn*?DrcyPSxF%S2TK9${PzRosE%6pYS&? z6mEyyHSxWC8^aGog?#Xhi#ZBM=%)VU*46zit3^_BA8 zzi{UNk%DQfchbyLRrK@>)YQ>J7aBKBxD6t zlV}hGTS}4b>(cB!LVml3B2+{+FSiWs6HkQ18v`59&%5nY>w6%yqqS*J?-27o6U{aPcDoTTbl>Hv&a)o{x&4ZP#Fspo19$R`0~)0c zX<^FtOy7PJ#taEwgbVv`k&Ep5%C0LTJ(w9hD{REL;afW)OS^u)o4A8m*-#E#%ik=F zl7pR+<$hX>O$J@`Q z^$^NNXTFb7D?j&J+hqU}bV4Me)k_568>!JI zxmPo(3+J&8tFn=c16lTj?`5BhP@#GvFzQ6xnlQRADucWA(CelMG=r$Q3_SmKknOOp z_%REC(Cjacn2CljyT>pL>vsSHykB5(s$1ANa(z=#I8z-t1VY{;)UP50lN z3$Gza2V!JKg_CG>4%X)uVav+~8+Qvgjn>Y*uSX$|2`@I^Pdws53O7CrMXDZ77hc1+ z==mG*R8BU#MWmH|2L$9b3J)ClPzdKD&wMa4>JCMslF|goOFlCk104)gmttI&S4?>= z{K0$iF%4Xwp4TO^>m>{;HEd{fS|ywFOn>h4fIkscD$|4;s4S;*m|1{0bKCb>VFtZ_ zz1U%$z|8t{%G+?X6B0vfAq;X~ek2sQ-d1(asr>*f1pxY?5;ZgK&t-{S8Jn#9s6H#H zTiD?McqjjBwp5CrlOQ0=dkM9A70|DcU=ytZXG=a@LKm&1MFd9=cIgh5kWw;vOs2nB zwv1-5O?PGgP_!j-VsW`c9KHrdhdWKJewxfmVa|>n)Ja?hJGz&bqnz zWsLA!b1(L*h7Ov^zPprnKE)Fny!IAfzB{Q&C2qT%gUuUl0ufAJI6+=$ z`IsjGS>^h+xe1ltG7i)tBT@5th5Ea@gpqhH-VS#73O>OIQbVeWZswv9Hkz`{t6%=aZ5V#nzF z$%UGnp=2bAzTBt=o|4XnWWgJoJ_EQ(ZQKnsAAzJ6#2Z&P#zdEWo@`y+{mgpH+!O?8 z;;kxB3yG94JwqBUPJhDFduI#{5OquUFIzLO%TEJ-r$|y{A|{ci>xrBb+-635Paa>jp(|tf{Qf<;7njYH%X`(Y^ zIRW3^D8ED89+5hP|0=;ofd*g`#<#dS>(M#13P4>u>dAbEZl|0wmAqscndo67%NP{<6{sx$zind*@)xbN29 z*Pwy{e~XCa1SP3k+`i;kaS}m#{}M2)FP?azZe4@8$h&O6O9og6mZbm!YkIApqFJbD z*rl9Xvo`Xg&ut9^OvSox?$7t)gh#%fh0IU@Um~oWYDz<9R%qESDIY8)Ot8J7W7RAD z-4KsUq?9uOjbV%k`;_Gb8c^#5Xg`44VKPpW?Ru-9gag!lNyDR=cM7yt!(9oJf+E!L zW7n|&Q9{3ZY%zX2{bPI6b+`5`50x6%k<$Fcx<2Re&rHjDPZL<40m6oLJFX^$h-Aq^ z*imHq$nI;vmO@O8f>bNr?0R&FMPcd(glO%$jR*gK(>IM6Q2nVcaX?TTi8X}7E5lk5 z+Jv>F0=u1$Vh!;T<-KMb+JSBH_+9^mK9=|$b}N#3`}DZ5|J!Xnv-`UnnroiT_QDWE zgRes^2eBJ%*?6DxOzThqeO3wlrzSP?J4mXp4)6kwa22s&Ax=T{Hrd~;M&q+qjV$nf zqi=bpcR(Wpi2eC-tt*%CvLv<4u?cA)3;Q|K-8C4OUzA1wsr~p>dBLs*ci&QtBcg`r zzVK~e?Ts?o+ovujs94VUsW|6lB%GAtF|s$LGx(XkC8s zy;Rr3pbCi@$PN`B2}j~?t+q{teRy{QvC5l=3ImsOoHf}a25r}f{ATS(W(tTN%zHI) z@f!+?OPd>6ob_zD}k0(5@O*bn_%y;AWMvmRg+%ca_>+aczkD zm{`S(VENY{#?bAqh1*NzT#|q0`_g)%McM{44?haw4KOTCKr_qBtI(rII?g*Y7zz9{!@@t zDufm8ac24_Sqpij1XISdF$l_DD|p&q2OVO}n)M_(_)f4MR?|9yo#3isEPFq|5dz*@ zNU+ub*bqT8oO*^=gO>x0M8O7WY0uqacG5*^xFwus9snPN%kwYHzd7%y;Dl7$`udj3 zYP?7oqXQs}S@fxhNgd=C02|DqrN9u&ys58{3jNXJ=s0_fNj#T@E?dN%fie9pN{s&e zzxpO@U~hMYWWbG1xDJ*JJpA`={&2}>j%*4$`mIZzduK)!1-WAuuC!)gUn{&w;UyMu zP4MNrf$|MMsGxGSw9o)xplORflt!hE@OVtbk0ew(K`O1dYB_jGay5d53$-h9bMZwUb*Rl9)f$-NtgWi@L8LIsG(T(Jy=qlbP@F*4-9!w7;;Rl z>rO4L@;M>A{KKQCaYYe7C8M!gW~<-b;)&Col6KL5|7;0${Lc>8or`i+JhK`geggG_ z5=}L9UOKiy{!Q3*Q!t1`id_@Qv(hcj4jFFUM}K!A`NXYnywfAqFmoe!lY5BA;^!8g zY6u}vzbqMHLScB0eyTo$AJxW`)f;;1w;K&CO9R_-g3{X|!PnkJTBC_f>LggD$gh84 zbx9SF7#|uyV)_S_h?>ig9E2r1%zuxmQWHu@>H*0^kMI#STP*Rj@jo(A*~n z0!0zj)=Mb~w|uUyN&Iz-CZPz z5T(aUXXV|tWKvDSyo8+eyoZl37xgV?1+vC89@ZriTfd z%aCw&2q9lEX{T%x3v5%U)AIq0arbVdQxDLS7mh-Nl~@~>)YOAk=bF0*KPDCz+gW?8 zwn%+qrMl}kPN2S|NCY>9>$r57J^N}wPyL@(#MD2Oe{nNp6Omy=XR&uC_gzETct zd@w7)p~Dz94B2sT^XG?#uttdP7xL)!HI#XRF0O7JJAaW4O3TsO`q9V#JE@Dujd7-w z(<@7ysFKv6f0ttH6<{L_ZFM)KUNk!v#8oAscdQlgtt5kl4mzr<0zb|=DT#5h>E}bj zM#)U5Xv#!e!;an)|h~bDS3I zk+?K76qYyDLzr@k@9IZH~hs?ROLm7Y3ZbW zx{6jY0UdLAk0korXB)llWQMb181B@MWp~E8W~%4^Cqrceho;`3_z0((vYdL*>Lq(4 zPzRg@8qXBqb$K~gHtFY^0$_RtZTW7=yN2=Wld7v4^BJq8Xp3WId&zE7tHiHN%rM2R5ylxF z_$i{NKQa!*5Nt^&^Wi(uj<;e4;S;6wFy53CzmdH? zt@Ka|M`}b(0 zfkV6_F1t?qA*MvYSVz${lfHd0Y-WtFy}P1`L;j11Zvrm;a1_+?7zqw6Dc|qg@Ik@e z#MDoveI28az;V?4k{0I6iF)Nr>275S9m)6sp{BhpOEH?ZD5_VnYk@fvX13>q#0dxV{SbokEcqFz@PtmHdRkdJ zHK^)VYGHiKzjB9mkOiX`K9alN3)&z>PhZp`=Hl383fJH2FC*+VrJl7g9SG*;Xn?&j*!(b8muV(~-12V+XSXKG5 zILPM^nIHkMNEsJdxVwEC#|7rA<}GQ3s9rlQDlm}!3K&91hetR6D8zqoYqXS)26T26 z=an{;=v2~g=t`oK)*jRbh*?nu$0)@XZF6ZA*ofW##gc&GI8Wgu zdqGcwtNb&v=LF>L?@+4iORCN67RAt5cjHqs>_peooEgPzv4iOrZzQsYxnei&NNtbrIrH zLrSOdzFON!GeDb`6a2dLAA~%P{MI?ioMo)LiRAg5Fq2!}g)6QXl)$!}LMn`}E$)eP zc{IduF85VD5n|ge`rFF;x0~>U5Q3?Fi+>avM(FVoq8;e~b)2ZLozxoOe)FXyeyd)p z76r!v=AmL;aSbdlx1L+Xx%)%1@yD!Vht%{a@#rfYvSRj$53lkWcl0gldB|kn0zl)A zmdQ(Y{nYe z;3VPzb)wUQxFK=~Be-=S)6v2WL;+*|)@@!hlVP{)i>hEvNAIws{qU?<9`iCEJHHfz zM+`#zX51;;xHk=yBml$unv}xNG{)(>|CA+Ahv1S2-SADNw)-Ech)WU-#raG4mfpB+ zF|)*gt5Xki{_~ByBoLI$%s%8Rm5Hn*=FanNaLsC9M&RT=bL)p&RB}7 zY!|mCnf99)hgJYtcDb@eczTl`d3^M`taj7ey>y=}>%Db0jl+WN>1O}I@dRf>5dai; z9+rT%4i^Cwp74TJqWGe#cI>Yb%RiqCkj3c|+psm$Hlve$ufydTuRZsdn$@lc8_u=etNXUab~Uw$pTkHt z=HSFDxD0P<`C*0<*7+cOktQ1I_|__VP*)E~%+w$V{vK)A4dgOZqx)yt5v>|shG&ze zzukv|sLRg9WmFpl^572Z!ud^r_HhG zYzY0WS_)3I78$A3$(2<9kC9qEU_|lc(iL?y&fD0OI+GN;Z3>)*ZG5kj?qn2kB9S}W zj^iThUqP!>VA1EMxj?|n5z6g^MTdn;7-WUB@xeJk$`jQIXXkZV8(%8T_RGUiCi2`$ zfax=jWqvK^!p`*_Toa_O)Ao|STFBwu6X46b|KuIxir*DNgVg1EM^o?iFpt#IRfbRz z3!H}bRpCVP_T8Po&zR;gCh>FmaDilI%fhrd^hO99qY20NAkou}n&kRZjZ>f;n} zEHni-r*n;@i}=N@>}WWPkF2g$79J;i`F@5tyiNV#O{k zkAdKFKjO>dilKxry41PQJ@9lE)}W0{rN!JoVy3DTG|DZjSSy)4z4ezCI7HDm7Q2eh zqyf(BFqNf$tT4<3rV9%1X&aJK62-WOZ%E3c~FO3x*mcc{nIHvs!wg&j*1{4h!UTlYvM>ZUZ4 zBd8$&j;GxP8)7RhHv*wENWAZ0SM)$pl3YZ?Yvqh>8hM*|({IVY!7UBwqBX-YaJ_Tl z93f*q`q2DhST=hnj#aZ|^ib^QPz_Z-F{5w;{v?IgDZ{m9yThsLzVb!={VX_|n~n8X zgxSt(8x#4-%#Pk9yB?1D8*}cC!h#4`ZwERlWvy24cmb4fr8f)3u=o+5DMO=EO;6*O zfea#V#Bp`%U1Zo+JOYPRqsPouQ6d4kW(1>J){RegX!u4A8MW@CM2?Nx_pUh`s(-ss z+WhQ_hwIc6sxBLmdFK~^py^g6fPA?I+*vxv9sC1K9l-$o$hIt{TX^_nr6@>-;&lz-k%QlNa z1Tn`eC;!VD)wLZL-U_xB0>?%Aqcmqe7hYi~d`S5V4BJPUb~|6VYwP{mqxfNf6c@!E zKPom2JTUdE&KWrX6k{^Ov_9gq)pO3NzAP?E8*pHX+hW+N3QJ`Qby9=$KE_T+l6#Kg zt@&it3-pat!T>`GecYb}mR&K)r4J20u{n*V%U1|);% zJaMaa?tBZo6v(`!#S;{ZaoLIcJ%sG~I{8(}VFFd6kjzf>d?2S5NWX`zjo2%Y({>zY z>bR6bgxCVst&SANjuhlwd8M|wHa(u`XSs91lD_5JLG8M*6CKog&Z3B0dVZQEAopHa zT==Q7#@peK`&u>CDtZt(fs$us92#&5<69pEE6QKP0~k^3>7t{GG=siY7vf{~L5Txv zSmqZ1DoJ%79+4vN4?lJs^ivS}MoD8R}w6|xg_eJgs(a%bo|c_|y-wD5yRr^e7+Bp1Kncid{PUPx%s zJ&=Nld4E`A1&N+|FXagG)Wouo+dYse;l=d79}4i9T9>zv`ggC)V>kw#G4bh3H+;{O zdR27CKU*5{+K*9l(A`yKG#Rwy5|F1h!jA$<L&#I&-2oSycQ8Qyvx!Scvjq=>HW#6m6!SB(R+OsQ0|D_B)EpA=R`Xci?BH#K0 z4YDhlh3F0OOwxpH?> zs_$ZP%E1Q!9E$33Bqkvo9l0lX-S(C8HwARPC^%NYi}dlXV(a||p{liAqWGU{^Di7) zGM+bn?L=?Z7O>-1Ke77K+ME>~)|vomGOnHNVT33>VW$HX)0`FO{tsQg^z@M=d*?nk zWhyZ?*`ia)B^7zjLn^c?^Feo1R{R|?a~$HSdQbZUbtRWWWc~Wze-g{NHDTb4*9IN0 z;zsMKX4*`KzTT6PPxG!yhw2Sl2^3camca8`3|t)E#)e0~h=l1bjmIDcKv_#hI3kTM zNI*2W0>>dwlZjjP8h!46{*eV-W!SkWP2Szuj9IwLT@TpG`zT={<+RxUKU$Vd4w zXR^K!2I0XUPB?}^E%7d&GqyvnSb?G?&!8WhD5GyvQ5R1*PM(F9D!!lZK6Z~Gfr(g3LDs53(CZG(;9T&{R(^L0j z8oS3u_s<~H4-#UnH)5*E!~)$jXB6u=P?Y(gi2YDEQ~}lW;hZMTt(>yStBGM_>nTUw z7J@rTS)EI!QFDVK^yoKcXFov*%El9AnCk;YtN|4zvxW7dH5{_$qx~RzsM%txrJ6F` zr9Wi5znHxX7SmC58}aWkGpaQzb9~T*l9vBKhop&pOHJV99RK?gxf+MRG=6++NYvD$ zz!{gbZQFu+BX)z4k?|#*gh3y`~txrhML!CYaMEXw6``=|M>Z{ZY5r zxUf&b<2KYikl^A2={c}svNr(nu>k>hd8l24PagU0$mZ62D$_ON)oTHa5FF`_xFUBZ z?5Y|vUPa%s#q1eB)p5f+e|tK`QY@3>PlNJXzSxe z0y*_IZ~tZMPB52~We~krdWy2$5rD%OQ1d!neL8zPI|%~nJum4oRoR5hPP0g&!N3&e zvlZFRtJhII-=$1L{Dok%=c(Xo!yPf_hs+HXQz-*w8T`Z}i>dqbEMbF(L4ca%EO23m-!Pe;Y!zMzVy zS%J`#*BY#&)F*PQFJdi!)ai|AT{qL*$VAbiJT;qFKLz`R$PKosUJNs5@#_2JbO++i zcf0+}gde}L_{|UPrB794E5Jh+1cD@T7qEt-WzaavxS>ndPhM^NiY$bc6rCGI@ovK9 z(mrIO2NtevPXhgaol0L;XJx!4xTYB$Bw#U+vv6n_dq{EgWxymWAqWpwJLCi-sPZI6 z{faMKr7y$|Z7}g5DfNTUs-1MRl;p<+VuA6h6Fp?*!nYE2cThsO=Cc*|PVJS9Xr&;( z&xs$n8djgr{F?iJZ^Rq?;%;^FYTNxTPIdM?KYRbtg!yKCE;k4XsLchLPF^H3aV!3F zl4jt=9*-aSQ<7u3o76LoWK5C=8@k->=`#>H&A8YXxoObgmTS=ZjJFi&As3hbi%MVV z|7obC0_erKdf2qq=S~HwlA(!nvJ?BPA}k&o^u!7pHi@KR$Px|-l1okkBg)!ASD=tQ z$%`>|EC^T6P7#KLV`AHyLDv9%EMe9PI<8bj;M|yWCpKo@R9R1mWj6^H&N*m^&@AJ% z`HM zV+pKycmMmjXYAjQ#;#?rH}FKj37sI9BVp7^Ja}kTJO-~)C{SPSC?X!rUU#Br3~tI^ zt%qXSsJ^;9`s~{j=K5A}lrw4omT+1~;iXWE{{arW zc^A(ETOOw-aoeJy1gd9TBk3 z9RX1^sVbXxIB-)oIWq2=&be>?Ivnxoy--pS+T7ogl&vF@B%wBdypbi!X~y_5K85RD zn}mS&Q_d^YsKc1>s;1zjJtdw!tUw_Av^T*kL2}%8IDNAgek?h)Qo$(7C7ZY>_q|4k zVC;=N5#LVG5wjEG%vM4GrY%z z+^W+^gx|va40pF6bevj-&V~T^T(I&*0rCdPR@&>rZcNCpFLFV4C87?sYufvn;Va1a z0U>R^6WTsdiHJs0T9mBGJYdmK!sXN9dLK?2L4hW19lc|73ikIw9V1%2- zvU(_2at=i!Inv>$dK3jy$azmCgU=n=flKkO<=(UoEgo@ix>qU{v1u3ol2J+3O}{nG z@9B|uABw#oLk$JMb`<62{O(*y^`gQ4gE38*x&Jr>D_e|pxq5OfgNUMj2|R7BGHZGm z2?*sY4Ex1O%?h6p4=$}h!wvT3wM7ieo?Xwd>w3vCSCi|~ z+wq{66s)*JUzhQiS_}%jk_d$B$&nX$eS zv7vpJR-0e3C0Z@71^vVUxbK8^;f<+69%rbJ%!0W~DXEb60bQDeLt->RiZG4IH1A~$ zvVNp#eIggLkpk({lqITyFU_d#)*Hzt4(gQ9X*(%>ZC}^pGi!;_Xtp8C(^4P`*6(DN zq{01`@u|2rcmsmLZ*a`Tb?9V)U$TM)H4Uu5D0x?R1la>e65F>(2?I`#D~Hkm5|xyC zJ)_SGuD+zv_GWuOnhw}ycp%H~rJc%p6`I6nFswFgwX}XVm#s-A?u#zWk4RIKMmZ~~ zWBP|kbA>oQk3>oH0@f2cpRN2yCx+u2;Y`sP*E|Xt#-8JFFx6TQ;K+CgsURGkW?}^v zWn+?bVOB)?LnRy(3$@Vda8}C+nggQ})?az8gVBT03p2xdK$u2Y`7sD29 zGJks@e!CY&$(2Dq>6`}L_c>TMJ15b3EeV$cS2o<#v8-2M^BvoUn!xU77o}qEr@zJ@ z`^`{F^I@3IrMHo-=;CTW7|tZasmy4sN+T(i1TbR8xA#;nOJ^b^W@jXxkPRmF68o~B z7=IrO4ir?zv`+~9!*qz$a$hk)E4Oj6gA7jWKt*#jYU?5+w@q;RX=;HTr-^zPBTFYQ zyYLB%kU(xN-emCe14m!y3?dfv{&Cc?rt#T}9Kif&JB)eY;n+3ac4%&yG|N?8E#Eo* zS;Go7j0)O7j9-n@qAr(;#x*ZJ`8LEfGhV$xk5W!`M$rRWXrL{K#sFBE;nfROou!GU}CFo#*}boQWu{qmHI@K znv#an!bP^ka4(0Twn9XO^nb~X>N`^?zVLJu(VLTK_HhT%2s-eXX*18c%Cv6Q;A!nX zP>L7}VL^)Bg%iV-;Y~<)f<<)|t0kGvve2Fckg)mCpZ|?^BT9g3a8W2A4YnG+_ZX1E zn6inSLrPXp!sDoPU$qwht`mQ0|E!*vfHkp^5IUoPO`(_mh=o8@v^@of_gC644hAbR zI_stIG3byqY2ZP~4d?Co7Xax21vOQ*HcU5+he>xzDvvsp8eo6ew3pK(xSjpwPykOL z+&Ta+KmEP~6x%0q12}dLUbGb`x5KoxDC+obEMJRt^7-Q^<}Y1_g|L+~}8mubg#r;-$94ueru`9x5*B3K1bx@p;afn zIy?|S0fZoikCAS1$yk-01}zGI6;RaKKCbo1mPrzUleJhocY{41*KutT_mcr50#= zs{$P-b{Ai!lFlX%H8duO%5Y!?v!5!XlD_}TooL3jCSbG{kBPd5=e=O-tM(GC2;K!` zq*6Mhjs)FkenTj6bY!{#cAt2pK%h134aUSob4U^Eu%VBesh?~nYZ9W(yrv6ul5vW_o~PVYKz0a zYSOTyRCBgVJlxF_Jk(83h-H_2?94U&rfU<6vQJlvw5SjrPdC)kiR_O$1>S;d%Ij!* z;jtHUfZ>RmH)n)Ao-knUA%O^T3H0fHPNq99FMk7KEz3(lBt1?3ZpR3Fp9yysRM2gK zeAHC;e*fhG_k9T{XYRRUBxh_My+JrBPINuoNY(i+{)h&wtzclI(U7%KRo^`px;=)H zzTau>@5^}whC`%uPrESs=siky$-845tnrRYmzx^p>lZhtH}3rDXv6#kFn&clnH*No zTT`7>dsx+Gp~*GAl_*HK3=?C0>Ppw!p_Ol0Ljh zt^g{G=ogVd66Y+9?V+kLDHPnSdVvV-0{X{F3RmYy+K9`P2a2kt>^$ z01X608X}qFn0F`G4ksBzZ>f4JgB0_~fmU1#I2^E@%XgXR6K?bR#O2V&obxBD5_&?W z_o1g}?Fjb1uK*~;m*9zFQ1|jxWBVl4)BqEWj@4@_)s({37Y?W5hZ}u>_&WfigCkZY ztw=IRqm5#M`L@DGt}m5AnBk`!LfL(sqxKnOSk zaK2e_cl9x<`{_p8NmS`_Mwzp4^yCLP^gr&4O{=Y4y^6}s5E2!3OOykGc(U-Fqm==B z7uv)C2k(t2wtU+~gl5ztfOi~=Dn|trxI&|H28rM;U=Kj?X9hf%53M{pd>r>!9-yy| zbA|ci=*kW{bP1zS1vVWYWa2Xnm1>HfbOP&V90TntcUBp@ooy{9YKitE_p?D+e+MaZ z;1Hw=e{soWo7V=={zfZ7zL78(#BsnL@d{;v`gsoc^wD^-C&(x+yaN2f{v{hnrkIs9 zOm;?a8NvcP2Ih7l=doy{{ki41juKMrwSXADJ&1tqk)rcL4bXd5#_n$kCu(b zrY_IjC6*NR3%ftPmJ#;E+mLTKY>2Kdb-gr+J18duzqs{8T^~_GCqp=_!LF7xC^zBc zmFn-EWJhpNc>N~{7|FV0Zvzt^jA0WT% z|5c@_${wb(koki^i8B@r;v`FSpL1j9rd!DY5mx=)A?CH(^`l29)qp1i-4sywGC=b| zW;ian>#OYkK^85JHYic#W0!4Kusb)zFaZDm3>?p``{tIv=omi_d6xWzji~FOk$b{M zPk>vpKH1m(s4V`wFJ!u_p@_HX`ABI!s=aLYT}BqbiJiIuea)^6FNy_{VaP+Ax*q%e zm7IL90o7=YVFLthQ8=5I1o+YkXZ_-3HwNO)&jA+TY#H!3tl_6Yq9zRZ7Z_3(BDUpJ z$Y)ol4G~{PEP4J;m9)iAqx>0_j?3n&zMd*`-4gWz|zQchOL z#b!U|<&UXJffzFP6<)~Z#)T_xkWprYUL8}bJp@E=ZYU=W`C>rWuleqT0XPh^ETU(D zvr_>-e;HIEC?N)XK+2V~`(Qe<7zbO*{og^PZwrL)I;0H?@$xEHgeePK)&PN7hZ$r!mNNo%C|2LQxNBy;1+`L3k^->?a>6Q; zvD^h0P(XL1C?0=}2LqvOHzz00lTf#T2SOGDWm2O!t(jIV`#taA12yi4UWwuV}Zoa)vh+8@W)c!;`(c^1KyO z6Y#HnWeR;QN}bA5H$G%vF(?PnoXqlw;@*46H0T5CVDe3CKYVg6;(WY=g@I^XlwsO$ zNaCX8{)g@C+Lu-`a$#=~1=clBSTzo1_%vgW>!sq%X2}gVjN1Vs0Yj=CfJ)Ei+fWv( zpUJ+zVD~L@jen>4cgLPmWh3%(jFbNLknOSAA&Za`2Od-e!3^hNcjNwYM(MTsnpkcL zJW;!ruWSfe-SxJuT{N@HMd>zNVSkPlM%RG5PO9MZ>(xlhHU#>PY5BkebUQ|IVay88k zYhIA~E_BeLWdtXjx7}17Mh?wF%OsegBV$oplJ^ycQk~4*2#q+s1zXCz!5?3R zzE|`H(|irZ*T2wc%YHm<(e3WH4=23jU+}EL#NF(G4xo9ALaoxwFM~CBj=90}O{ap?Se!C{K?88%abOU(^;BfoM~2 z^doK#&B3LQ8~>caTB(5_XfozmSqJF=;z?qryg;`zEQ*&tB=+@UW^bN{apM88j^MR z__w)_0i6?j0q)Tr{;18*oj9s$X{y}H>H;aLq*LJQY+hh~WJjcA1Z$qs?e|&Mo^+gg zsnF_nt?&8B2-n%ItYA-$(wIJGRF6u&?sKn!IUEhb9h{joL7!u1nb!RwiP<5@DZc_c zr(0tJ7T9Cq{7#_w`6{Bqm|(d53 zpEPD=8b1edcX+E2s!+}6zVoLVs>EN*BA=UJmC&8=8NOwNGu`P_aX?nEY71g9d^*G` zpBrso^E+Ns19*{4?#zjLgV)7O!#21F|JIcM{9N25rGCs!E<$@tqRCpNkB%aiE>on` z5o7>&rn|-5&?(jT*r__S4Hz%a1FQ+ACnMH3u&3|)J{>eDxAzGP;dl24@VePfOa^&z z_eW4{R81W$oDZoIO>CSv7C~LN=I{M}Q-Y6nrYg4{{8{9snr1SA z=1F-Gm%V^>0EAzHX#iniR(^b1-dHvO=Wf+l2cn{z&Om;ol!HBEqRO8rP!K-CzY*=tNFyc)ITbWTfc`7k(${b zk-76GysGilz6K94+^?#L(v*} z48?vSkUPZ1ZMO0VJ9XfscXB}eK0JK9%;?Q1`)4UsHA_MIo3@GWpPrexAE`;~F~#!7 z+s!+n({RQOtt5hJJ(^-Ws@5^UyuxIC$07dY^2NQd(LmIz{h5Ri@3W3~Hb*PS4%KSQ zoaz>KEj_Cp=ycn&$5opJFwUA9*~rm)ONr9;%IN;mN)5iE+f(AJ*)*17=+2#RWV6r* zN9mQz+{W6D%WBGpYsuTh*2W(bMP7$Ka6ZtEJ|>2;>!`*jYatK zN!H86B63Zit^EUA_X`%zWyNT^5wYVoHvAAWp^NBsQ9dr;)#W(nj3?_VmR)1~ZFkZH zO72;W{9cz2NM`e0Bn5@lszI=c6E`_e$NzP&zuoLKf2f>fleRz1n44Ro`r|?mzb){R zp&!+X5JVc9ZICmCj(Jp37bsFz;$yz|g;<3Fk;kjE!wb?rb}IB&DFwg)q0t_3H{2xs z^URA=$((Ue%b*17IZYenz^G`Ux?WEJ7+nAe#nC&YM=Q(!SO7;6iyn&B zjlXu`8U+Q2svCI+$~nNUh+}I8pdU^EGu(>_tEI9tH~?+{tbHiJ&ZQ#FEz}30wFg1~ z1fARRU0$5c;^1rg0-lHf0000q_yPNxK1`$MOD0zxpT%MrxPU1SO#8M;V{Qn{)E2#% z9P~MhfuX-fk$t0|IMCr8qwqwU(p8Mtp(=xVA2Jz*$XKnxqHCVtPAPh7!85Okb|YC4oh_|zXauX zb^*6EqnCFTTjxMmc6x0hRP{1EvoGtk`IBJJ&Lp0T5%E=Kx8B*QC$^xL)%)-ZL>X!X zd?7m1mJb4heP22fvwY653hN{7};jTqi#m>q91 zxJ>1J&CrsF;?Kr#Cgg(3wf$ybxL5IPeF$>r7A^UBm78`GFJP?A(8t@6Z8hv`u9UlU z*vv0cRtKWj$m^I6`0s3lTPIFqsPn_7=nD|x>-Fsa7aBdHuex}7xTgXY9Z{&=cXb%0 zG=-HsdPH-Fc~uQgt9wt=S??KkVWu=!xsbME1;X)Sx`MSz#ijUua%?|ET4gk;fPNcs z2$H2~kr@pD!qPu7>ztu%Y+o%rjE-zU)H6wyD1{A@dq~B>120tugzUe(Yll+q30ZjT zPQ1AN7}9KM6c}m;$q)%Dv#oK6OUd6X9T8`=1D>|HIzuK799A?54|)MZCU`oOIW91M zVBe^}o9-v$SmxanKIt<-$X3Sw2cI}^z2L4akQUz;hs&dshJi4L6;&KdW`>2UR-;8v z-6DnOBLu2FAv|YlfzqqsP9XNRpg@hP?$8^@wNni{XY&p^BYlXARez5oA}|z1nJ*Ew zQhvI@$sSp>e)nlYt7y#w&_>Wdo;}uOa6%DCum2rVr3a2Kr)N3fon0=Q5n?NS~toFGYM1^u@o<{P-#WJ6R|32&0}d33SFLeL?$o!H}vL37_` zfkqa{X0~ZMawtdvj}Qf?x-D186omnRW)c-eJW>ENIav_H&x33b7X4in5K0s@0F@2; z^UoP;qH81M=~U=3)RnkAzVoXrLv6Z9c7z=3%;E_d&1S5;e~cy}r!at?ULEpNN_0_k zcxwvQYrRODPe7Ib9Psvq>s3Z`#o2;>GM01VDTx~cKA#BUh(3gUsIGGn^UCySG|^}- z)V(r})-fD*olS5@7Ov0zt?rp`?c7*>D+FyX*^9;XZ*%$hyHj>xnM4|WXRvh0A!Xl} zf=5hjOOUE6LY2#J8UX=4Md}OP-l`|Hv~B{pDqF2e!D?l9;O}X z{cwhUH(Kr?U>Si0FxJWt4ZGPINuzuRCV<5Ni_%A&&WyFW0SQ$wd1kxzyjs3Sg4q_R zQn`W1vl2@RA^rs$8)Ssdk=ETpYa#hWFJv@$=K_G-n>KWs9>@1h0EBoW6MMDGdd*I5 zSs7yp32$R`tnng&Ckxa$@E(2N#?n7tg2;803yDbDXzp zxC`4p=3g-ojfiPtStYDgtJ;~Qa^+)@7Q-d5kqjSCl0S^7Sn0-8hJ`0c%I38NV4F%~ z?}sDAX#5*kv`<5y!V0`fu9j_#ovohEeS_x;+Slmqa_k@b0(JT`aKh6|&C#MK*|C;O zzEq}pe%5Oz`)w{L(&#=t@4d}qtodu}+E$_)$X(c-)aklOH>RCnjM%SxP~j0mALBRJ zT8A%eiJ7-tLn>H89*2vm|-~j0y$k|bggXUt`j$Az-+(W-JmktzF2~22><@{h$>WP#1Znd9|ZP-1CA;$Fy4WE$L0) zG*E>Z8uqrum{TUyFUE}mYtDK_wOUr5UL;$dG?9IJ3G3!Xq`0u?kN1e8v;SZK00cw< zJnZH=<>Q(TgLvZ1*9QPUfoP z8y0I(v2igzE>-4fA?3 zF*6HdomXpOs`W(Po|D!C!kKg789c02L&s<>bykuN9Wxb|e5V-9!~D1o5I?B{{eB$N|G6k?=A>YS*J~CO6mG&=;mbq8 zdW9^8SwSudLrlavp&8(CB8RCzov5X?)=Y+ou(rY_gC?iaLh#Zz!u&-EG}e`X@1zV3 zHrnko?V~;DASv6+;qK5yjNF0{BPg<(0yqaM^JPx|10}4h?Z7wWEIzPrxcRQ>1nyxO z|0Ju0myL(8(*Egqi*Fh+|}O;SzNQ1Y*Pr zJ-kpNv>Ki)4B|NjTdw^KEV`Lu{5u+Ji5@^>25bh^gKJ{|lV1!%c&M60lbwZtuAbeJ#Dii4ExOy*jx#0p9vjIkg(6d;~`&kgR{uy;5opKbT0za+6G?85dFfEsXyA__3Z+JId>iVy*ShG-*} zF=$XoJ6<1!aBqZWP*Sal>)DfrVPNX)7)DsqMpD7e3nP-lrwW?A4LlOhl5h5*-Y3?E zDfw`xOfwQKrX2LaCjPx9;~U){5tBt6zV3g{p>j#iZ3=di1EWsc9Jk|fF^LF^zR8f# z1AS}+L3i-~1n!NBLk_wWy7@&tCnhWQ_$`aFnA!F0xu`S`qr)Hy`$dg6i#ly3eeK2) zY6I#Rc3o<%`(bN46^LpOfduK?H#lG^t->55n_jNkaaSM8s?3qYu3L9pRDz}Qh-ivb z0tL*hMYiRsLLlD(fJHq@3p*k>rG^jDkRJMpn63!Sl{O>Y+G#YGh*5?8h600Bln=z78Lu z*Y-W)pw*9M4CWSEq}5=tb5J$eA4dZy>BSI z+DYOvsi45mE>1@MMw;dJwTxl)=$+4bm`bsWQws;>6HoZamFrCEhvZ29&a}MK*!_rt z^IZY=gZ0o!@SWM|23ZSv;|l=>hqka*2;SR7eLYr0*zI}oXm$lq>)lifXiTf$$EJNu zfbSqF+T$1e&iCACJkney(gNajvFJk;5cK9mfYsXVME|t^nP)K!E&W}SSUPO|=S7&d2@;x8eEn3$(sI`~kKiNWQI zu#ue?SFWa91^cA~@dLO%i|HlapWT|Pv)`MtGQE9?_Kk7y|4Pb>+w z+UvV}Z~v~5;GO^C2h-DDMt@7;4E38|zy-6lkW?RDKHaq^J#mbWWWupG)mcE=g-NjD zMOTh@>miMib5mfg$mEagZPVhxlO0S3LEU+-ex1kV@dD@yHVkTwE0fkB3 zpX-dtvkOsmef`iKeOETutz+t>x};sJ#tU`@tHJJILR6Su;YQ^xWw(DA(9MZRY&h!i@I$W!l6OmeHe6o;sw9up`LB_Ha%vvPz34IyXtiK8AU3lOgzGdu0D0eXE=Eq@=)`)&oCw%ylP-%HMeak` zl`Lv|j0uZ20#veVdMLnABDZJ`ih`m#MkWMo{i!KPnfocEX zu`GDntVg5NYVrg%lv#7SP8`SwP)#PtkHPVgCy_B?VV;8PZWH*(kg#nja0&+zcZfCR z_QW4=wh_Mmb1Qye37A7{Up>Bj0aWb5H+4mVGc4+Bz02$!O;|3T)b$Fc$iFBfDrg2+ z0j=+I{7H_AR>cFL)Ouf5lOTmjIf3RiX9M z=yQfD;Ukm6#BA}1zkyyA(?GcI{=0x1d#Kel??!pB$&>+G1C)|i;R@Kfn+^g`X;hWN zA3|Vx|F9t-(Cn)iR`Kd1y+WLf@f{g@HduTYWr}Nr>FXYeq5WWK6sn(lVfXtlkh=Pe z6~zYsSPYeX(=m>7a#1*ty|c}GUnDv zpWj7J8o?FX4nY!)RD3@e6m8UsUwJMI7f+&sbmtD9!Bem{bhiA239nzc{@KIZ;92I= z>Qzh55x^{B@9Jtg2o=2mIe^eIX^^BrpvW#-h_>S&6j5~31X?HFymi&{9K3_duOSa< z_(qsHkIhxUi?{aD#d&ow__dRLy(f{=RzT?D)(td5PwCn*{Su`oz4zI|h(1}-tCYUliyM8Js=Xd{k(X;4y&aFDFLo!s><6WaDjVRG z4EP6`*8ig53sbWEZ@+~h>#t#Yt{(0j6b;oPv32hDss)$lKn!=PjuggS*i`^lYGyfb-99^z3Lw_n}|I|iL z+0&S|k&b%S1S_tjmvPL(uin=*-~Gv(@?p1pfh!S`t+%!d~j{J zZgh+m8V-7U6a`#8J_rIv#wxd9aKeqq{?`ws_n+guw8#CRmuFD-!ViJBs^b0fw5M_HpJiYz!T0pA`HWM%f z)fqCi0&(OhsqytzrF;IEp~@|Hr*JOD&s&u~lFE0BDC9k#@-tZ7)el>bfH%kV&4C5)J&l1yiA zA8Xjf-Cv3PE&~l(Jx7zF1QLfKF{hi&v2!58aR9!A%D&wZOA~^R^DN-F45GxSdTyA< z8xC)v#ERYJq!GxTThFneKXGTItz7-XI-oevozZ4K8wr%oSgGB9*Qau$VgmX_?DMSF zbS_ykZF_lny?*%Ru15mez<<=9Z~P3$ny_t0tS&G-07!UnrV+KU>kUk%Jy%dTA>3hR zx_95L^j8-QtL|-B4M2I6xd;=XhLC^ydD>1sos&7YRI=xb=_}>q&))oSr>9ZV)&Kk% zND-?skUi_&f&fcQuMGi9-dqa9LpR=pQEfL6ERij_LY?AfNwt#3#|!U!ljEBsgEbT6 z$hc<`Zl8`;B(|8h8J`(G5?>|Hnuua#-sPBNc-CF_U$pAGcZ@m2k%821XaJ2J!hf5^ zdu2i-Ksd)vxhHmXOp3Xw1POWNJSK39=D$?eY<%(4zbcyV&_(E9tz93ot7$gp(qT9x zv=FZJgA^YSoDX9{7no(I1QX9JnC5&AyrUaq5Q<>Qz&`3n0ts!A+%n3*nm$*7?P}0w z3LiZ|us6Lf4R?62kYVcKa~J%y3!vjDhPpv6q@0rAih#C1xEpT2=GKr9q%Knx)MadB zaDpkfOY|Gaqq4yA5xTKSr^U<2z}TuElC?<+_6j%Xxg!BQYMYW=D%_6J=`q z4jDY#F^ixc#%)Jg=`uFX{FNTZ_R0OXTJA!Lt{vuQ(HZ-zdTFYgG1N#lWMxCJ@UF6a z$KIsjXV#2&pT*Bxym*dwOzf?GkV$dItLD-9yHYJoDW=!Ka?v*j#$}u_3uO52@z_bT z=xT2rm*fAmq+=)l!mbo(=$BV8@n-N&8IFL*Ikl3E7bQR}f)O2g6LWBp;H{RuTx6z} zYEqpYwnUxXIKUH0PA+%fFF+0p+Z~S%Kbn`Sso^`ootp@>UNsXPb^Z+@Fnb2z*WQ0< zs?O%mM)NMb>@LX;?+ToiX*xwpL?oMwc@#gB^crJpb7yWy`YHMVU+|ZH+D&|HCV*>h-hKg z5@KW0bF!*4Bp>SFJMPTzSJATRusGtjZT9H~M-qk>q7>wH*YbUA3Yfrp_Z~N6g)u(- z2Ab^$IWJ4_u4k6lOgr|^|84V2hCsTW7q;&##^Y5X#G)Hma~8-_6rt9sdCAhVax9}D zqq-%UN$DU23sLu0RauvY3PM!Hb|LHV*XQOkFkrJS`Yssq!bi=GXs% zl2vZLv$1q?}kLHH?VxP4M!``CeCx&Ce$uqb3R%1{l7kp zWyd~_R{IQNY@Q$Pi{i4Tw$x7YjUHFlH>`s4I`fu=Mr0N z;@D3t7_m5syKQ(}U)E&LfH0G-Tn9EI;_V!Z0|hT=B9v#&m*5%uCt&f7g0KeuoF+;t zz^Z-ILG~JEJ7T&E8^U1~UO}WrPQQRUeaHac83a=Sp;4}tEH|I^ccHa_W}%IQe^0y4 z80rFW$JZf;vvuBNlF6oUpXTjDZ)w(!GHMptnIuB$RgDD83IS2%M^=zP_cFav!@*)E z!pm}2yI)Nib_7C2VQ`3D|7+oc`|&y(BPK4VKMljg#oQdl&j)rH2@dk%M|@b(kMpeQ zn5!@Am9cD3CcHi=;Od!ldEp!nD@UC5(gXoR-s2|Y@}A8OGv62}v?9T130qFblhT&| zV9g8NRI;NPRxz2;G3P)$zYgnP<5F+6dS-c(TH{RFkqTuC79@in5I0R#8kdZ27wC<%l+txR9>j!x5-S7Td^nNPEvF7i zkZn-W|08ka)63j?;2nhIG}mAt+;;*@esn23B6htebxhdr>3e0E3YQDpP|C}MYXEA} zF&2nR+yXuW%MVq~o@0h9`9iQuf?L0=NkX}2vbN{GNh!3Kxbro?gQju8%L?tTU3Bk|5Qm%mtlpu?%e}S&wu#H!|TG#k*tc(;QWHbg4f>R}Rf*TRS$=MSCrM z$I1a20ak3ltI0ue)4-(n#;+m->*-(`3;$mI8Ud)b23UuW=YEi1UI|wl2Vh3hlI@gB zG-?_NB3K&uilp9_&?I|QYZdp_E#m84a-9PFVi_p1PPBp@#qHc9z^-a|eWD>K)^D;gD2?3%&i$~{8#-L~UWR=Z+9cy4 z%E)($&e5F%9rwGacKi+xFK)X0)^0~%Gx6jMYKtou5akK+&+EDg;C|qKrB-uRX_M+f ze5+e^MR+-Kq5I7|BOMjTUv1iQhHi;jJRxJ6dk1cQSIy7Nk)JnN3 z4+#W+?aOn6@_i1ibXH3ff!gZz;IV5{#{sFWGwJWI~+VXaxy-YsCZ|f2) zA`p&xe6$b%2O=?ZZU<*>#eG2rO2m5Ny7j== zFGx6=tSoAGp&Mr7<>c+W=nn{n(;0)`C5dTxGo*+zRdJ(e)DVh5UKu}81(ZAef&&6ySLG{FA~zS~K16&C@O zhFi(nL(W8%7bl-G3Ck_TasD_UgQG7+C^0GjxR)o}^KG_%e&S=rY$9j@(yzIt zZ&e?wOX1I5oJ*e47^Lqki^b=AZ@P&$Cbc$Zm2O)AK!4HLxNC$yaP4c@Y)y$cpPA!H zK-l=`M@nHuZM2mA4Eoex z&fRA6Skn;}!B{;ZP)I10lN(rchh4d@hB_UX92$kAzyawRKWCHj0o<05$q$7RYeIbos_hTh zmZRxVkVE6p2{7c6P6AW_efYMTP0}zr2-H+)k0YdVDNjdoDbq}$k=g8r(DxQ~P2o+> z%zKFf)e1RR8+#AYQK4XaXdXmfezWZ4Br-^aV+0G4#Ak82`@# zlGw+NhjIU;=+pI7D$lu|&tl|kgbE(ZyWTwbJ=?roK3~#1_LZsX%VszHWdMbh^90*} z<+J=LAcgD(SFzMP^wAhcMi`T>{BgH6ZW|$Yk&k0Drp?n{M)DF=gQn4ztl>8R}3w*QB za*i8O%~Lu=qq>YkXn*rCvx5vMz;(sM*JbKSG{I(ckFQAmv8@McVDO26;>*=SU2u#I zdSn3kQ_kQ{$vi7UsjIy~i}~J93y4X0a4JGvv|SE`q&_3r1bwn6{Pcwe|K^vUjl{bp z-3j*)5A42bRl;l#V&&(a=&HT1z?<@@6=W`O#cT*2RkPHgEpK4d8Lf_aj}3G8UMU=7yr4Gc35A9WD6#?kH8G`hcmc zeqb3uDrB-^6{>M~?d{)qBuLn1q?)gHUF%S+TR-fzQ;kEe`9-|8z>CXW6ynHLktbY& zvR~qU!U+Tp2o|__PZ)FjDd$6|_DU~RE^3xjVNk_E@(nRV>vdhBQ8xSwJoMuUwG<#c zn?E-Js$26wi5l({pL@D4rrKS5lbLaNZpp~Aj0KQ8(!Q|BkN`EA_|AsJNJ(sLNZhkH z8=8U0AS&^3X5;!;l2G~P)f1hzuUdG>)YZZ-7Q*f;<{rl{*SGh_L=P%KvG@g%&t^8T zGIKgd@eH6a(IL#JofkjqpD-&iS@-<7b2aFdI=Mpq>TMXC-YOB!B=dItH&F1&|MnI`3)TCj7GL*R)mZ6Amwusa?^&xYIQqP^EiSd1C?O`i~@)Qcq8;87rQb~y|z0L>~L38`F& zE6*=GfP$p7+~9aG9eHGut`?Oqzuql>=@|FIg6_3xs|u$m;a4EhNC&~E6N@cFk4S~= zU9<*_oYTzEU}VTurfn?Wd$6$PnRYY}ehrG#t57owXO+V+%afCS<%0q`pnvYKaB z!k)dVehFB`p#HXruV%@eWNGwx)P#EhpQ3eTrVAm+3EQ!^^bV+*ozpeDQBUFjkJR4_ z{-`v{`|_#7=fg3VZ02zVuPj>%3antK=P{wNS4IuRP4aGUu{t z!OFZZ?+Ad6k3^h+n`maAHzbB71CjZ(eX*bk0BLWdgnAMl>R0`7GUHF?T!gb&e~KHT zGuaq%q;a4bG>ftc0M@K5%greQDy1x!n9ot7WI?6G0IK{B>I13*_BQDW{A1cpmH3A{ z*y-s)DuE;DMObQ+*Jd^RcLIN=KV0M;`unx#$b$>z*L3rlPp&-F^OEejatTVrXsTzM^Aw>lsOA070ny*~yy5 zZbIyb9V&?}c_i5Quu|2cF-nQdzj&XA?vL?uLErwd0gsQfE&=&Y&&#SImxXCB0k)Rtz zDwnmLAvX{Hxm<~zcIeRRs{)d3=g6%^kef z=li2ccSsAw`4xIo&RWFjH-Pu{h;J>X+35fInL0B>0TmQ8(9s(I``uu{cy*H5gVFyXuE zEZ=|4I6{@OI9L6+fnbkYzxt&K%y1Zo+f1{xvH$Tp29nsVZVcFMZ8l7b;iX@A>)-&B zaTd;DyQpnX45?BKy4Z}PFaH}lVNob{>05Yy;}~~&PRpi_H{y7OCLQbSIG&9^=T29Z zo;9}z(2++k4eo7(uL)nAG1ULHJ9e3ukA2+FVXZg6}4dI)`|jS^oo0AfI$ zzaA$o_G3#`pqfDo|KuM5)`@IP_dJbQl$OcXp-kj+^cj-scsxK?yJ;B>Aj^^@oRGY9{AWnXnDanR+E6#OlnzD*$e4{U#%ZM+P^Ee>hQqBCQm}3nq!>Ce@gPC?~WQNfcG!3 z^R3Xb(ZIuVdJ3KiTPgeT`s@nAXioWhZ6HnQQb(})G&hRVJRe^6Yap@(#@YAdkaQbm zLuZbZDK74)>6oc(wBi2`>0NDG(}9)SpUxaClMXA=!3WzI*J1O!Oc4L}Tc87ipA`Bu zZpSzBCIXP7CGo=Kg=Ep3@`AfStG03c!SybVfAK2=yfmj!giEQgyHvAy`|JE2d1tS% zdF|@qC47(K!Fm|m{h9E zb}R9DiH?cluc3ucJ#7EZ&jnjwQE7`}Nty+y5O;FBG_?t#_lk za2)z7_}YdC#Lo?*Y$j&CuX4tq8?C|*7YY*Flw$q)+It2+HcE5A2hp>KtGaBpl~K2& zheq6(fON}>bhWmhz&Ll()rR0BFceZ1S&m|4Qc4V@ozr@8ea-02Y8bWw;HRdZFyX>I z*SVusy3diT(iFS&XU3LY&_aDim~!w4(=v3Uyuo=hC&%yYS27v~ePTA%ElV?IdAS;i zRw$>z^mrajAip{wDQhtrYVJOfN6|mgKdr04SI?M@kQVuH*HL3I3sT5JcQqh9$#7;8 zH(5{whOGKE;x1+*M4!AzsqjS?3K?F)3f$R=z}tRmqZJw8NuO{v%AcCI&=AYr zbLe~Nj~6sPyQ;L;;^56P2l7}JM#9|p=1phQa6dq|?kG*mE`oMjqKjUZ#}pdT3y$0M zY>M<7H_ffzCwiP4Epk$bz575fsS zWq8FIWOt6);3}-j1;I!v{OIhL1!A}t&FTZ$kDNQyB>;;m^Y=UbM-=o0bHMP>F>k-$ z4RXAe=%FV*J?1-LOgJn|NaCF9qLLnR?Q6Xla5|`|**X%k4+q)cVV=EeXT!~c0~Q-$ z@Bs*j@y?6i?bR6fFAQ5HSRt~dIoQK4JZn+Cz$XhYg~dp9)SpW`B=q2vlX>yqwDf;MCErMV_Fh2(uGGnJeT$yZ-D_#E77i~h@>uthRxxe_GC*D6F7Cb8~=E|od zX*;~bj;bbIYL9wQ4X~IQJ@PzBn(?c8r-;KHGi5uI^U*{N+3%&kZA42W&pV3>`KJ412b&0@rMNt`sp)e3)CUvvm(Gp3knqdp-7=;hHsNG9+Aas`THr65BS z1IcGEbbBp_n%^B~Aj3)QXasbbQL*EdTWV@ZNE4kpgWO)b^0AkN3k+5LlEz3e79ngy zo}aC@2;R?V^OA=j3wyOBS95lXf*Px7E;`e}zbZj`m=q`&|3aSiN)3LC#NeIkmX?3K z!&~&ca3&Ou6CCG93Co{x70oJ1O)ruy*R%UyW*dep@Z@77@p<+&ThWP~)2x=4#7eCj z`{L|^rL>=JGh7*$dZxtLAu({($}yd2KW>KA*=!zRz_G)DM&&B#gAQib$NbssheX#u zY)TnvUlr+%i{{pklpD(F!f(oi>Ec~mYUC=nO+b@lrZ`fn9hbm4O_1N@|7Zf zZ>4x+ni)g5rp|=@>oN-5?Wu5>iJ3_AOb$e3F^Vh@$_j7*CzGREZ=nYaI2Dj`*anxh zC@7eeOg>-ESE{f!DJKB#}X@)=F=9MEuN4%)5?o-q-Q7q}sKwkkidTJEEp_WnUth1Sx zRRO1s9fYSgOQsh8`WOq#mf zOTEl)1otOtP(Gz)I^q^`gaEe%j$>@6ajFwyY00rbCATM)#=bieXMR36k-NZv18Pj* z+$~dZnAI8DyR~buQdKw*@qjuO1H>{@r$2d`oG>4eavw@`H z?jxFOy7G$X4`({U&x2Blx#|C{@=z?-{Qo5j6D6hMc-~20bUeIk~_BxMbn6@7>APzE+@TUk5N!LSzrFh?*5fP z>d)d0Am`uDTUz;Yb(=7=&bpeGkE=vSed(HRZ~}qVy;YZ(tkDZIZwBHePHE5Y8`Xb* zUiwBqOIrfi!l(F+EEvbSk zJEsKyid^oA3H#evDjJhk@em#TW$apYs-8f^-l0|{Mb)anL*+k&;Y9LY<7U^A zxg~~4;_~Z;O3{Ny1w^)^=CQzJ8IHXUmHjyWO#Gl;35gQc4$PKWOU-hV44;#y9)qpl zzH^g|Ayr?c^>Cz|%n3ru&gkMAm=1 zuM{%L9Vw}r&xP=wG*6?o%U$1gz9Tkw)Ou~+->2LSCD8$W@0D`201}PlrokfVF3K30 z)89}8u`B{>-~5fN-V1OdxjWPWkG=Y(9E{T~9Cg&&N)HHs%J&a{Td+XA=*%w{gHvJ_*yZG=q=GC=?7&hJgeS=D(vb zFfxS+to5G-hV%`QyzDB8kun1AQ5IJ~C}mi4*}{2++Er3Uae8qv0f~YU`b-4qdhGG$ zv{U1s{n5;!sj?JjhL@STaUbl}GVpyW0xT`VKPWd|)~Cnk42r{+*XIy6Zammr?SJ40 zV9o*Z+=V65Z@;Bcx-Fi~I+vC$`D;L3@4&-u-#9$IjwyA7AD%^{6l=8Bw& zq)iANtY1gCkaf2-=$;R#T(==X4YoBRVG;4_`#A3_lJnjI{7w9P^>h+{(`>wC0;o+2 zs&ZXz!FBL(pr3=aKk!-Q zQ(pA#SoFNk@b`yB&k-Fmig9gDuCH4oaTwk#T=c9CW{_RUF7c-0klfiOT8RF=mcasF zeLIpbBJAb1S)Yx=INlO(;q-mW)I4u-Oofko^k^t5r7}4@h%iMK>IqVuqj$8KfJE5; z%XQ7ozdD2&f?chIMKNR3e25n#&Z=u8IY0CZgeOPS2y}mH7n+*1q35`%YIWmuf0Ufs zSy0l{Lr)qLKOGOf$nv}LHFyn?RN_k|*)n-Cm^VKLO!ajqw*Nav%K({>_DvnTjL^Z7 z=Sow<(X#DpGxFuiQo$!F0!_kOdr^(Glo#ckeVdf*nTyu=)zr}G+L-OMXM&!typp0bh8M%++BTQ}VDZWku&K6> zA`R+PAAhGd;e(fFkRnFkb!t;r??#HOQ(~c?{Y*I1^FR;upIi6xlM`4GS#1wnI)Z|m zA@2(>=DQW54KFlMnO^iv7C>g<^Ar2uPvepGowtOZrmx2Y4+dOyDn2fu|WlktT#-pm!KdMS_K@+6~lqIOPW-E<@<2GtNve~zG!a9n=a$|YLnfENuP*-#vLLWg2 z6updePXQXYEw%NG7%e3!ZeG~@^{ESmCk)yAV}qSi=l%|Ari-QsWbmrwZ!UnGE*twd!* zcd8DWA`4BdKklI2)53KjPx$=Zj*EbUbY2CJ{(=D%oK5YGdcfM%JTeHY9XQ8D=Z|t` zzCWkTzb~R{rv6|ki$i{*|9_$8&(?N8)Bgd-f5?0Jjkcq&A22blJxm29PLHE z+lY16zr5aUg(NPEqK^lpAsm zgIpOlkD))lPlDjA^H^pa-saEcOIlN;lL|@Ofsaoba8mP~4b<&#hQwz>}-zt+$cxWB=AzWa(Nf8^KIM%CkgXm+{FtT16m35v5UFU`mGz}0st>Egak_M`R# z2DRu^Vx#A=0KNf|Hy~K2mfCxCubN)Fp z=FbM*J8z#9{qOe_-RW_Z)l8wlp+lDIEt{I{o(+}uhw34O=be3+j0WJIi3YK`-1UD= zTfdSh>TtaN8g6cJq)Sxm@6ZUZ!mn;E&_3}01m(u@*CK1l9)D>KH)@=gdyXGQv!O%+27*%&piXb@W^H+j)J@RzUs_TLW@hhl+b zO>7poSAY2Uia=uM(Sn^#HY^PEWE4-+Tx*si#6l=SIccuLd9>;xyA&jM#b958Iw)@N zqx>L13{VC$@E0i)O33@>!!q^Mzf0o1c65TbqyHwr4&Ui6%3_0IL@ev&xziB1ozP_R zk)2wo(tW!qW#XNDWCxsx$f*iy+8tTR@^!6WQuI?xCO%HVB=$YCOj<{GZR&uHPbK$0 zf5gUyZyyvuqn?WIOtgOF z!J>S z8tCDHZM%hN|1%h<3c8q9%l9G4E8^#$LFJ=h1cgE3cF^#L9qvDI=7}LT5K6ihBLxjd zGgtCka`=5f8EHif37vt0xUKZXmqE;LF_tiVlkh%%a22XVzx09Ok&x?M61%Z$mDu4l zSn{axQ7GtO&Vp3l+p6JOast~-_Ki2sHo5D?PmhqMKCQh%@_x^y<+&$vrD`Xrw{zl~ z;KQ$Sk@s=#kwW*SMQ;yI4;&Bs6T!^>3E3Af*NE?Lu8~6n`0T$;FvHgzcb)wH60IIJ z`9~UGV4CFv8mo_Cbd5q8LE=#&rUGdH3Bl(?R$8u$D!XYKI&SC33$1s4Qb=htJ3O+0 ze^=P(cxTes>w?;v@r2Bj*!R|;X)TIcQ#}F_=&%jtkitz&lpyd97$`6=6Xnp^sSoWB z^I|1BoxL14AyZbsiyLX*wfxwMg_&@xqgk4=5ZLo)_~sHXW;CND^6s5(sl;p&C)?Yf zG-OI=5J-p(BAl~{0GHij-?PDg*dDiq?j-;v2Y;ldBf`}AERx|v|M=rM4%V{b)7~qq zdA+Q#bEB86$Bdfl%wjp%RVlM?+uH>R} z!VK-<;C={L4`H#4SK}-unlXH^a-l&BM?F?@slu;bp;te8ECxld>@&&N3smfwQFKH= zCUdIUAo%pvZn}`@zQK77C7k~o^z9n#yIS#&yZc|%i~t5J@K7_)V*i44u_m%&;aZJH_y)O&3sCdoCYC#-QT<+hu4}S&I1{wrWsje>=y`p#jKxal z?qT%wqKI6i@#3zN%*Cga8m65KmZtg`>^G2}> zK$mQmZU-buXnOXj(9iItbxD9)RbET)=$y4mH5C9f=l+%>yv3QQIFGUf)Ykmwy#O_B z$B|N;ek3qaL@6MZJ_CznaBGif~{(e0%> zN?}=T$ZWywG)2!4lH+fBi2+i+`M$HW{Jt;{Fe9kdF^OopQp^01f9`I?y?mERAFk{Cg&vk*(z^XBQX$+HkkTITeE0it{4YxgV%9Esq> zmfgKBOzC@2+XR~I;FuiiMlRs{*yPfeu^j=XbmtT2pYJ)&kt-E4`!TjBDQ|W1p4NEI zKJwZmXqNVCQdfpcs&oLKQq47BfuFQ1o|ol>4C<$0zAJ? z^uf}W9ZLQYki%$OV-AS%US9`Gkjp$-1F<^Mfq1Hl9k^tHyw9SKX`o*Q7*qLm&TTiVum`;iX6kZ++|(fXJcH}vkmJg?qm?w$IFM2qS4Wj(>MEpCZJd`xPA_PB_0+Ax zkN6=p^bQJibgI)~qt2`cz2&5J;z#yyI^Ik5=o>+sXdOB;GCse{DnP#?dBN!d5UsJv zWiG8Zy#q=h>{>Tp&_jPY-wZjv*SkeS`z_7%YBm{sHiAzv(DV_CdR>g#nG;A{YISWB zU~j?|AAB{5*VuMhp|ERfo}QbVx2!l!9^*o`!!pT?ujR92fh*g?vn_Zt>@;f?OOR_m z+qI(Zlw96HT8JdA#UP?G;c9Col^N_?6p;u!eC=q>^nGqY3uqL5L=Mo+?N|%sk4?X? z0gJq1+W-ZuqZ;aVx(W&A?=-uO?54IeVV^XC|CXaZWd$s~rj7#)|D2u>UT06zhcG*F zcABN9hh;qqQW6)oI8Fpoam=4iNF(I{V+5+U2cFwmm?W|a=9Z}tivX_qVD;i z^d&)ZjCWGKZ^nsmBD&V0ofaw2ol&wzMOK0kGj73M8LZfB7pY&c90|fj%ZCpw&sQs} zdKjI|uPqAiM=($#4}2KBCz4aik^rEVwzG|%@6BJZ9jVfE_))yeTZ`V94aB0KG)~^K5KI z;uSi`EU1FlnUjqnNe`G8<3K|gFqNBlOw;rJbyCkRUcUsLi$c@u`brYkMF5xsl15>Q+eqMz^PPm_grd5o59B1a%R$a zdz-m1QYC~?El=@x2z_19So3ua`cNHOv*~wceUT6*IC`e}HjA(Q6+!l~m?zS~J*z}r zE(>|OJKX43QYY+UIhRpG7@7iXc%BeH51c*l65CwqN1RDMZMlE|0IpX-svP%IwFT>9 zjNpEKjF3f*dG>TlQ2P#++_thE=L|#W4$cFhAWzU$4A@36qsXpL7!k;wJb-cl1HF7@ z1h5LIIg|L&ujx(Gq|JPZwQA`p7JO)jEqEo_DXRA@(-iW#t2$(GleFmnYsF|#O4w+| zEDdZ~VG}M1nQS-NtZ7 zg3P9J?oGN45(GW(&0S)zjjEje7biodBA=iwlCTKw1qr@d&?v1008V3wz=ofZ(wB32 z%9P{zm6PZ_waAyvmE;Rd)6_3oqB_uj&mcV1hrEBHBn9SKJL3ql9}+eaPCIQs6Pi2T z?ExP%qXdUXc88b8KCGdcFUX!dGNW4~@hhLVzpD;7P6q?aGld{&R{%xo32g6#@ zxX-K+w@i0Dt_h)O7eD5IO%XO-^AhXUdvRuHpGZVx{~Lq~(pSc)-nU3n!DnM!qZ@7| z?hM2WIom02j~6hGn=+*DfKQ#jn|ePXsc1<_>Gn@focWs}n7htSzP*$hf^}G5fz{EoIKlDh~;hG=YH)m6$R{r=M5cMVbYs z@H!e2FYqqiG`TfIPtvGZc(~y`q>~c<9*DV4U|vIEDHH~gNp4iE#5t;%Fn81+8x?P&-UCDz$NDFSEMBtV9T@D5@Ovv< zPR51Sf~0rqgWIBcGx#o|cTul1Wj*q21mRkj6qv{My{w>*5?wKGnVWL|K(NWc_|%|S zlG1P}Jt_eX)0%Bi9xq5CDByht{wM5=J&4x|F+`siIA-aGe&mnY5^J1g(!BTN0x$}e zp_IH3SE3J4PSknpBg!t$dl*cEMBk6BaZWAwu4Yw^pE+(eO*CHZamfcduAVGV1hv(T z;aD(Z`0}!`8(0`zSnfpu%e(I$t!m)+1suG55s~ZN?e&aM5y|EAs=hNO52--N!135l z#J^1)$z*#l;5p`=?Q~ziSkYZN0xnYkQ5xBli;j~}{3|@f%?FY#>>fU4&n&_p)EYIv z2$J94DF?jg!~A0sY2v(0jF8hpOoA<-N&DdI1$RkTHXkTWjVAax^M*C8;iYzrx#!^T zXItSftA=ciVVl!}6RRjX=j{jiHZj%H`jqi7vosD+J=*akqnqC?H9Fv5vg2Ee%BoJ{ z+c6L5+!{T>F^-!2Vr737ah6w08T%kpiv4Q9pBFQ=cTQQhWPh=TK0{!j0NOt!O67TA z$3DO=X%WY%61A6iYt~WF_OwLJBL`wdCaXsI1E#`|c6T#h(``*Yi&n0{_ZvE@QBIr& zT5jKIEwj4~%ZIA*YNmPoGg^|S z@XM#@3ospNxDM{0FhtuP<+N{08uHm_r5^#0Lc0P89I-8{{CT7ltV)i;cis)ny%as zWD&+@#sfMyo$!h%2jYRXN1($h7|EZ__QUjHO3 ztaYAwRn>!zHpETP>J0>;X_kGGo0l(q@TvH>m<-pHz1H3$>wtm$fSLJXH-+Gc)f&GD zPs_A^Mcyx$*0zNou}Z}mIdVh6y^^o=Jd!RR@|_ss2$9cd^1&?I+9OMpw*LXIU}gd# zN-y{NHN9^vkIbLtSB&&KNb@7oR@%V7ZK5c|b0paAqnWyFsw`$ejKEg3uoNZaIR;Ec z$_P-|eu;BtLGzrVa-c%f^Po>w(=~=NDs^^V8kLo z<@n@^!&n+4&HO0I3ecLq%c`6+6jd#$!r`2YJ28T@BC3-Adp0-1rmQy4m3e1>s4{Yc zNnnk`I;yiU&mr5X5vSOevxh$+e3C6Whcic|XDa(D-2k38!to0;)Ho~I+z@!gQ@r_dY zulnIZoLU>0E;_pMk(l)+4ytKNu~k1P zDR%Yf5@+2AFkQ$hIPU~u;y{mo3a0h5=StD|)S6*SEJQ)nb%mibYY0Sc*&I=eS$7qH zyKDFi{1l}{nh2g~0FCloP{2$@plA!;vz?q~o>5DeLIBe`boohzz!!DeZ^dGuCqmW8 zDB=$%yE(a+awxiMQUujS5G-&)8Ap_Qz9vSCmpvx7gE3_BH3qPJ0;Gq>4x`|dphs#Z zwHC!JNym3tROOMkpRg_0u6*2(L=hTIvHCtLQ3=GFv8H9A8j(HWjeL7z>Z1AzlrQO& z6io=<_dUFlzoz{*qT84Eug%((|Jm2x5^ivu513<@=B@0qDr5y#V#gT= z-Q2hZ9d-afSun&aoj&rgMKrmL6{zail=-Hc+WM3z5nTSrs1kRpE12DpiF@lC`ceux zHMx(-$fLszeN{XxTlwEJi#{G{NtlAVp!@uH2v}(yOlMSWv|Z-C-{*d0?^Ls=h`@hK zuyl`l!YpSh|4t<{UULz5`V5v~YX=uxdYEiTc6SQJ1;)K`z}vm%IOL3}pMJ8{!F5+^ zel$@V{4n}s57{)5-=DE15Y;*Tw|}Mz6IWub%$*Z&)wJoAaOQ|noTPVC`BIH%O&up( zVq$hZ0!HWnv1h`ji$MxDT+FV?({5eZnslw**N2Lz+diI*1v#RLZF*5@&hr+%KIwrM{diy}6zZdH7LVJ|fFt6^lx)KP6R zyb7EvRFI=d7~s?*LvLgDB<%gg+&7P;FU?=rz=sLe!5B?TX-?)K)sLPE>`EPLWz&$l zdrs2YE?X8)lvPrfUt7J&sYVN;TcqxH<3b=6e`Ct`;Em!=)KcBFBv_;~aj&Asidf(% z8Lr>Z?w8g394ZNQuE(uY8$yTBV+0KfQ;P4iCn|bDwgDwAi;90zM$Vs_I3;+xfccps zdVTy&vik5Svc6>+1B?sgrySR(atmaql zO$r;!)jx|p>okWY+BpmozUaYAjQ|!PvdV`=7D5)<%QP|9v@y^vF*mnv6+A&YPeImD zFF8~KROfsX?GL?pEYMC+9Lr{3=1Lqpr9i9ImYh45(P*9Kb<7J4TB9w~E z4ASu%BL#Yz=!wwpvrnkifo7l(Ga7R;x9n!cv)l~Y&u`zhys5Y%emgKqx`8D~cNqb* z?X^xMZdE~(VGW7O^2ZoWQeIA-{k$hBKIH+Roo|BuXBY9x@6}l%wnhl3CJ35x`?44o zpxLLH0#R67FMfXm#h3Tj`aKjAFCmP}0jDCcV1Txm3S3X-T3@L|@5G!dWOJ^yu9|mc;*i;j zQC2w7m@I!%i)QrTqUMY%`vXb^*~LDX(DL}ZKdLpK}S5%~5N{Tf^;ew4p;Q_L?~hU4ki`LOS6_cWJUn#|$;hoHEFd=>r#v z=UNQupX>U+neX=Ico(i9KnCVsInU7A6*wjKwSmN-*ky|D1d{W^sj;X(BG;X3SN3|+ zk!O4toH5Xm;Rd}$Y{UYD^>>`HSH=J_>=EuScl{w&)`l)`^qu#|t#}(dxDQC;(f>HCI3g!34 zArc^8(P&*oKIEcxsH7ZELx&~(f>#y7I8={{TX(Q7U2z=Iv@1{9{`kx}{Wx>7NFaWo^NRgVpdnHYlCR1|Oou1=YgcG`{vG2ERh;kR=1 zuL3Mm9)p${pcro4^W@7x2Ug??eEoEJfakeO>P$HfnW#!4o6>-~=BfS+cq!@iC2e?Q zRj%T-&ZGkW$G6|ia$bRi=Rm!b;u%7175mT?mdJKBHb`r4%vY z$4TchGkvVfmD+GQ+3GVimN^?fTS|Jyr&;)|Z~(b!QKr26o{efZ`*55c=QaDd)tUI@ zPOGvNsb5A{@$5&E``!@kl<3Vtmpk4~) zfy#P-=<%<;HJ}!P} zc5#4X{C*v9$5P$|EwdqXxEz&CO*AIzBFQh*xy53#F5FA5lM*)yRr{&Y6afBA@bF`t zrI|fR!a7;~{ek=!*1)16KvKDpXL*aevEAhydj6sJoI~X6j2RfHrUa|#IJ6K+xO%tM zts@SB?FFDes-ay`zK%kiIF3680&EZ*m$6TKsyJ0_jI_1vK@P`))1PGY5!`E>lTWs+ zc?&=(q4--%pKM8}(!el44{VasCf&kJ@cV}}k?&``f`$Y6BVJt#HzQ$M_^zcrbXRB5 z%2#bpU&iO0ieyj-jE)&jZHZQsI@tKMN`Ih@Bu}D|I?2Jf0FYFSb+H_vjGZ{E+=83t z#)Y^68sGw{LqCCp%H@11d3kvGE<6Gx&VhQJMsDVYUfQZ>1##;pTTrv*bs(|T5gYkN zuu;@qFH^!4NRYAt13`;<^tJeHAWr1)y6*sMNQu=pN>`MW8qVu#c)b7s05W%EMzTt% zt@h89W?0R?gCJ)jw0NT_s`{W}p;(a;7C!?l)JRL+J;syiQn)~FFUoUk|)q@DfP>D5lgy^g~qHxmZ}+?S+J z;_xCgAQ*r7FNR%)V>e*r61}%V%UGrqZL#B^r_$+(S0u4G?YI-RNkS)I%3VF!Tni@> z6u_+Wo*hwpbNx%5Y{z7X=wFWZXMJnvj&1FaY2d0oxdp&wGHpmeO2@2mNp+s-yTaIe zPNG2yuia*!-$#Wpg9L|btI50^m*VV2M%ClaMvLLahhqNG8=S@nCp@@MPpZv0ZKz5F z0HwSJ3)hMdvBz(tL`%t;$6lrLPzn_GDjKDS+thC!Uf^NX?xMVa7C4vG^Dz}L=vXp! zmc@+BTGNU#*jBSNb~?hn5{zDuI>O(nVT=zcg*ZRXkhY#zVaFM2@j)*(x`1Zwo<{UJ zPM-ku6$yz?h>$v=M?QF6QO74nGDAx3Paq6^`Fh0~Nr8HsDe8z_5V9#oEf-Bkf;7%r z3EP;KO&mBqeiG z7-Q@mOtL21sx)M>3fnBkB$@XnnS|Zo8qg`r<**@ri~#Hu zLBfP9V`T%}-y z5ufcl)1_6xRiOIlxgqu>VAFiI^fzdVZ!^ zS?v_(`X{KN{vakXP-6_9?l)DhIh|d49po1f*FP5_0;AsNZcK|t$rnzhqNfDZ=Gxo0 zLonNIi2_}(%;2%2^441;;J+6mXQ9thIlT{ zXqL;KhzCi;7KzH|dCN&WX72peheN-0%S|FG_4+>PCAPYBP2Ap>&&PNuU2vx%TG6QM z=kH!z;s|FiXYz+t-?dLs$Jg{YrV_lw0ll#+ns}M2#m-7TQcHDEUH815nHVtWxDP&- zPz4aM$r&1v3&MeL&NX|Cf3VB?gioo~G)h6LP*n*8=`aa*bqMl0# z)^XXc%A6$zAdkR|B&bSAsT1ii?3PO^&J)5ATvosU006tBU#SRe;RT8%bToQQ2B3;h zsOu(1`o7!k&P$hZFAnr>oKiH71uk@`s3UU5SG-WI_hkiHvqY!=fuod4LFyEU(HZZP zGqCFtQB4PY@do``k)Ns58VizQX4qJr(7Wr45(V(iQ(4|q((l6&3zL4R3KT~f&^=Dy zEGIiQj?%v%l|7Hna*0Hjs`_nb#VOYSzBX9OUd!Xp%3r>m(t4 z9v3*B$=tZjk8N)#%ad|=YgzZ~+)NvRPu>tv)&Nr9#wVzMYE?;~)ndI>}jZ;<@ zl`2s{@ZFH#VIzmpS7!fh2%0QnHDHSwmkV}0%Lc`v>ov*kL@#@cW zO5Ipmaq0uCoF|rrAxyR=K~|UAR}ZJ2hL3e!IKrlX8a1XMwk8d(m>A`9U<5QUII0kt zmEe+!3zgs3e&jV)>27GF#KO<(iJ;4{py%z)Yy0-;>|2Fi>82?MJ}b1+I?q9a*cmh4 zGe!dh%DHk6VZ*Ty-^~2PyN9Wxa;Q~ZN)joQ%RD<{?9tjZ7WUm?<&^kA%W->md4sDC{E!ROoL>w{`YuO#d2wRjc?Z^dYz;Sl1;Izpopjc zVQzBEMDF?nkx#PF_>ca*I;52`=i*+B?++Tc@#>=Md+qPfq%g?K9YI4-nHw6PJf&AZ zSK3Jdh(tkE2-Tz0J-R|cqDd76>i>ybV5{_eF+>8(0tjUPuNKL=(Y8NwZHSfw$3##K znL;W!@14|q>Zr|!L%z6_6^hnatSfb$GX6eLSfC1H$ zaSiAv;b8$?iVwu&kRM|H$mplDk~d-ytN)TmfxV4FPyBJ%YNa}ULIM$!27zIHrT*bl2s@{0$i2P&1`d7VNLGR@cHhlxGOU1sL}s;WxGta zYlzaAPZf#?HXbDRAN9RmGv}L$!71T$U37GG@sA zd1k`9@LZc|@*dm+OLg;!9iwSKmf_=sWP3{SGA#^Z%x7NADD);C7qWxUU5 zq?~ep8SFI)zK~qWP4=w+pg~z6fY>$@9M_AC(Ccqgl6|cnQzY$}V{PFHNVt30u=Z$Ro;JR-;ho)Oi-u32vV&zf7zhA=GzwI?? z4_u#awY0$HW2t2Ai46Fx%%hT}Kk}=-5ryn$FCYlbv_WcAZ|L@%JJl*yYwVg;w1KE+ z8VEuJct^hdc1-a3-usq8*OLkPgs;K zFP4%I1Nh~>;+l4~jWggYUvmj|^6xe_LU%0=p~b7>^3({~-U3z^H0hl65iQq6({9Fc zzfbGD!(UXM5#FVoofP8aIS*5#SZ$@&M?=^-Vcnh-9xG7fZY|rn=6ICD`HHAqFi<;D zjTiD+kUmA}bPUz~H7D@&3Z{AZ^jP%{p{ z33&Q~&y&fc<@za*{gu#e?_NbIz3-OOrpmg~oz6A~vKtwEHiDGcSB9E~!ZtTeA0j!( zIhVZ?hP6d|O(XcA1EOo!AEVPXD1lBPOXL`Lm`o266t*&3D^SM$FR2)|vJ~Ge@t0~3 zxzlQGc8f?=(Y75XMm7CPZ-TVf;-*_Ni-D%~+Qkw5SMKQm=b%=w^K;(6{N7w*XeYjd7uS6kVwzHSRqYHSMs}!^#1ksGzg*Yv zCl`JE*Kwq+NKcWS^r?z==%N{_WDu<4aqwB^u`eQEJB+M9uK*j0n3&I1cj7HfFKhbg z1DC^`U7q@^WnSeMk@k+|laxf52^RdWQgp<)pOcBn7lyP&!@sTy+ZQt!NIJ zLyM4H%i7vUCL3|SnP`R5Z8h?3mgEM=ANZNXM1ZZeN0$rGx34R++%F9YAl2cSmC;|M z49IVcLMTjs5o1R+^rk*_GE}OgzTsFWlaFL`Lj7~Zd~=&`$T)&LP+#3lD-&X8BA&If z`2N8Y0Xrnld*tWR^{6RJi^=-q!|S!;Yym)hpr+Y&w9ryl!UWQJrElTjm!|ixwf>TZ z{!d!P@?eN3ebJ^-nppINr}&6FJmbqQlAWA;emR8{g?6ZerO$G+w!97jmOp_ZG{E?_ zbItVrX|o94Hf2jw%j7&qBj9S0Cc2u}8r)1`vRCPtW)ez%SJ3uWjstaJ>vm7#q(!j*Z8R?N@xCOtA zx6HcIXeAY{H?v8lJA0|TxsRTgIAZVeQ=KM>0Jf~EAeowub-E>es zBnFFZGb^kDy%1u?#st*$LX!iZM4saVXYc)4`3FJ@pQgr9Y%je#Gg=@f%aWPPeLSWt z?dI`7OP}=mfVrn^1RD4o8uq;9>Hr{3P%|l7C926Gk1$R(43WhXM|gC;^TbG_{-EW8 z6P}FImCOxI>J}@vDJ?m^ORR|y&KrbQe)YRLSatMk;a27e`=st*RJiFSX=ICVgbm^= z!YtQLMGt@9u3CLJLP^o_^ks|}q~a<|u8#opBHRTP)FX3>8S~+|VI3Pb&;13r25Pg* zL@&t`{Zw#u@?45e$^{VG*J&Py&GNpVkKgibu)_yrv8{thxgjaL9*WgTLa5GS>RNa% z95qUhy0U~kV#_ojL_om#m(4jjP-_50dO+oxR7pGV2Bqy|dBXe3W}-0!fC$&8FE}@OP^Wh zM%QE5H3)4Q1zi4D)#z1|uPzC27P8F2D+!Z-rd%kQaT;f(hAG`7ny}rhPo)X(3R#&b zMeMX^J^Y^|IjZe^u9>UUa1BAZyA%y=3%$u~b9`Otj}c!Yj@+4uv9|%z*Qa;!!V`F@ zCr9oiORb5#_Qso82=&CKkW$WTABS`|8V$vOxd9}Giv;urg^nA!o111|k@jCNWvf1G zM2UQIcTiKak8K zk)$-$GwXYMNAKbbIzR` zicWeKwtpy6o#sN2O)1qol|gJ*E90787(>qsDoBpavf$~`ZH@wLDy;633CiL@K|$g`hrCF|3xnLzA`?Qw7&5R-Z$N;Nt;gFB2uU0_&yhUX{wqn z@urDh$5Q@dh5?f3f8hd`fhYw85ay>li@Rxy`ls}H4WYdC55^z+AC~Y6Ml-KcONy*I z^ebP+8|t#;ILeA!jtTXq@08VFkx!Fh$*WP^1>xflF6E;{e0TgZMMFkb#ASQRmqN;? zw?sM@Xm~7zl|}Fz@q~x-oSZ0mUGb@sY=ppvf9NCjk<HCZsPWC7A9pB^?=#DWZd!=^~XNW{mp0Si?QMs%7jU<1_ya~wZ%t{ zTr;)NoCCDqb`%vvSuTZzVsf3IKv1S-xAg#wPj1Zqyb_~vZOrbCXrnbqiP!v z%(^v=hCva1@%gJ3#E9S0r&q6-fk!b7tl0xL=CuT&s5vDyi1{_ZD4To-Y8}m=fs7G47_^s5?T~~+ zmKex06zC;*3sR3EKRATd*wH!zHVl?5j87WGYuovw#2x{9vKA^(<1d}=T=_`XG2y$8 zL_yUehtoEQku6OyDNQ3GOJcI+mA>k`AiyXF&BYGyw&~~Mr){}B|7uBa@YL!3i0qS* z$E7LHI{#X$boRkd6T)a|uU1QXwW577B)PeH7}pi}Nx?1KS0Ue7lhWiL?eJRFz}MdA z$FowM@sEcTE>g~BG#&{N=IXOkIK5HL>-8k&zJum?7X!QC44!epsG)ju{r|Vrn|FG< z1@;6DTuMK%Q74yM7ro5+H=&e&fw!J`w0yKnoP-TC4HT(mp?c3hqkgAO57ho@8rtBB zt-R>a>GP%Gh0B;lE- zoXMlKsXm1G7sg{_(_z#We9hvN5$MykJ6@8D=~(=UMKSxPnQCu)bcPst{XpZlkeFeOdZN8qu`X;w#^j#A{VwB7Lr{<2cj<3G;a#HlE}R1P&}xtWN_Ia zhr{g=$YhC5<2r?^L)pp|2*rf1gGXdhwnaSx+RJBOujZ2S(52qUWNS3M7SHWRf(R0H z@SV|f-#@!|P4=|3Y2}GJd1W5m+ddenY@&3*U|*^jfQ7g*`-2DCIwDB@^ZDD|-n`v4 zj;}Uu8>&7g3+&0JhwN%X*=jm@o%4jM`+RBU`L`A}DC)6Lc53ErQfTl7Z=Ku0hnIhl zg4v`>g~I_6n@1m@-TU~TGWsFT;YQ(U2p09m@c)80!pgA_Cll;hOHqVs|lsPZ0g=67DSIn@x z;{9D$vJ10O;&9vp#+I!HU1?o>g}1Ee&NUL;vAteH;Pd{AwF2R|Fp07u4>=&Ur6yl( z3(Ox@K+2s|@<%2zWOJDJq;`FFq^ig|pSHy}0q)iD7Az_=WO^T8+DddWx^kJj3&Xzw zY&2N?tW29!PLTMZm)*viJSYp`?aMUvbP9_4MOOdX4#IiOpW~=yC7;g%rVFe>aKXNB zhjFR2w0~>Zz?7?s=3w$Uwu=e6PU(XzqvumEZdG@?fBoj*0$u!>WPj>IcP|rH5V~a# zDRFaw z1D&hM#33Z<1MM}XItAaN`w|0wCKCX1HDCYc1)@KgeapWGWv>O^$se6{Y1Q|0kT5Bs zw30xLh0P25JQB43C)jCr0oXZ)7&4_GYcBdPsGXs1`2S#v=P8Ie{PypB895npGY!gU zfXCcSi=Y_#&_sM%CKiDp?5fj-nwYGUlqajIS4&d>Y!(Ja@jqCce#M< zvobFK27wb;nl`mbNlc2&-7;UUG|`o}qOw9k+wqwd6*;ZjL@<$mx~Kjl%9o|Mu6~36 zC(^8OdCeWvwpR(?pPPA-at1F3_&3!9fnf@%tKG7|*SqH2~QruM_1}RZ!@|1*HQTrEbYelHv=_vMD zi;49;9lQ{8D>kbnR|B_>wt3wdAwwz*4?Y?e~o|vnYl38Pg9W`DfCg1)86k3P#KV z+6i!85hfd+p$AI@)X}|Ioy}}v;;*+r@fL3*BL>87Sldez z{7*4inc9f8I4e6>-9~WY~ zSn$pk6H4f9#>sy1-8d!K&4`ZUlMP{o%Y03Vd`K6n#y_W`S{0IbfA2ub8UH_biNu9R zOL;l)bJtYU^Um%w%0?WwLnxUL(Q=o~ibf$o^^P~E!@GVYRc{^EnNr|R`Er~wR*#zh zzSgh#$qjel584L~I_R%@s8rlGSaqlLgg!17j!R^S9kau))j0TOjIU%X-d=*@afwaM z-n0UBL!eMN_*Mtw|&zWOFf^;Ya(R5G;m6wNZsI|Lj6C>%zRh z?P-`!%-@hXn@NYNkm(g;ne63@!o3Bs5U4uh1qy{h;hZ6rE?Fkwe<1_4h{(3AB>gQK zfQvK}TdLFH!fL460Gfcr9uZ%vXl>Nm+B_{iSB_xj78I_{IkxDz3ZTzSQDdKquLm}a zQ5x3(F(fAijhB5&t0!@bEVpdUNOY^ef3tGmav5OFz4TWmh-~rKY8xv(%3^QHopPrl@ zvf*fq-O(9|RBweC{O|CQ3KA{C(9Rld&fh5=c&K%K!XupYENV=S-|d*Akry8)kMXJz7;Pa)?kV``NZ-&yxVUe)6xNIPTkFnN?Ds*VB z9(}vzP&j8h3c1WR)i~~!B3vPiCCS|hqZdp3_3*Cc=rYQy*ofW=A6LwdZy8>G=MdW$ zg-4y1#F%{SycNz6o({*lU+4iIkR++EB z-!oHs$eV-Ts?ELOwf)5Gn7HU%tm|I7hM~=w^c#A54hqXo%89rf2@X76^tF_}Gflsq zT0hCUN~L%wK^a!rtL;Ky0*5&#mOk!e8(tkK*3CD7=u^hskW7bF-gxkoBEOo?5Io7^ zPd4Pd??KEIoiMK0=h4`9NRtj|uJ*VI5;e+lDP+Y>VfUFV8FA-v5s|uhm_>Xi5Q|ht z9Lp8T{4|10j5H?bM3@1#tTtzv-K}Iv%h)v5B@ZYYYlaPzRx&hEf|+*2MWQP z6f%2`^w+SHz*s}))9wuntIaEg(I-T;-_>1y;q37lxX0;yZ)A)=yR`YMk2(b4V)mzK zk)%jywydw{I~CM zs=8j;nk!Pwydf?IC8Pmn7E{-xdH!Z@8RL~83YzrY-Z}yb9 z=eDVwGjijoiTQ@|DM$32t?|xtbY4?Z!=o^^_!)-Uf18AL9)gr;q4z;?h?j+wy zk3msIkoq7-h~;A>A5zParmQX_^pgrvUcawPVowaYh`&e|F_Ma~;ovT#%jd#^$nQ*f zRQkQsrYPEGQG23lUwd|-CTx3CZf^RrfN4@c1)>!q%}ONDu1-8 zw(#ud&>q`eCd8cTw8YEF1xnMG<1AkuKiA)@UzQG>@#5JpD9By3J+g7XB`Rt03~>#x z&XDhy7}biED-g{Yw78N6yis{fKdFA$f_4^?a@6IQlHJMa(j#UsHQJ9)X-)3ZA7rjl zuj>Ho8aq0XkkQbeqC8fRlG{{#r1GyaxEN84JhK##uhVb5G?cOoo7m(p0HqOcF#`_{ zE-@d2FA81g&S^m2l(GC2%MTk`$!{y_IJhb#QWiqPZ7ayCO{o#z3i37sg6v~UuH_rU zY{=_;IERHe-yd<+(c3=W0h#TC5*&9d2~o;}ig8nobct_O`?kG`#}=_e^cZ<9)>1@S za5d5}X0&n{(y|jFxLPzH=Q;@`4zN!t}*!zA!0E75t^LX0wGe;e|aWV zXi4fRpG&-7K38~%XeM1BfOQ!>t4L=VO?^wX-B6v|dS1lYP5#MEf`5UmV$mBwaj-kA z-Mg6*bbUZo;0$L{4c!-)p}y}D60rSy`xHK|e6BnHK4KCR|5RY1&*G-wzg4&0i_aakfTj_HRCNSIgSdE8gpVBNMu2~Fz zgSI$A*9}nlrB68yT|7HfpF^X26$W6fE}d#d75WNhfHpC^KoKliDBM3BbX6tp8wOBJfB81U%Cc}p<)f`#y?-@%R{9Vhr`0I^gdpAcQlHt^g{Y8_u z;+WxergzvN6B){R-H4WZdZ_7XTba;`0q=Il@(kr4?_Nfy9)j0U z>Wk>@VOyTYEkK#0Y?Y@FRBNcxg$p4PpqllB5@)bBYr>NMvH@xN;lj~W940qL3FqA( z*88HihIyN(&?mrXl7srgQaR%5dU`uJLzGb&n}LU0nL)T|`Tv**zHcVKJrTDqhpd)| zlLGab4N81HuPDb)2#}BN#KORd4;d!Vt^xJ%>^;911B+>j*u=2){#jF|(k1o~@>lI+ z9~r0!Kp|yULtIKDW@&7o3L_rnW=1zkkewZ+3fq73b~@mon#23sS`x1!^#5S_Pyu+N z(sC&9p%pcFZcysAOU}pF2Qfb7*xaH^0ZA87Dy`_Z9q*yhi5wnPWGMU(NXxD)#hxw8 z-FxM_B}|Z^SmW#Mq@pt~16h=xO-=?pXbJY67WvI*HT$!jatPobp0jik*R>ollKA&i zZ0V9AorzqVid{d2Jk6K-qx^-ntlVR1FwBNx9aS*-eNBi)ACE4eJqwFe8Uo2a><`PO zMh3SWh+-ld8PhahGNjjzmrro9JN`Fn*r@TUYoLr!x(Xs;c%JaQzOK)0M#(4(JSVr0 zIUaT-?GGqXIOi`-QtW|{`YF~13BvsRN6z5um-l3 z^g)-hkEZz6jMu+#8TJt`V+t~q&~@kaLG~(W*I$l5 z7bkgnIm4TW!@+z}`?=$R%L5Ohr)Y}{WuaI#BL@eV@CwAhjNL~9)w~yda2-3|6ZTUA z^xn{15FQ;I68TwaZYKqB$tA_lij(1YhCYjS=F8#TF7eY1a+`8a1(XlT12Q+hLBn%W z8!x{CMY6vv>tvSOr#9W8)XjYR3f&)-c+#O=wn#wJzJClr_f4{V*ZAXT$^`H5Q*5au z?S?q3mOy5YYMG6L73GVPygw*|N(!nzm>V!rKB$849!lFk@LG|~mBPtDf z%vI~adFlTwQVhyA9-<+~-0;s?vHJlr;ln&e6o%CDcYH!^a21FDL#ykZI+@NisG z2O8R_-fr>YdQJKxx%)7yv7HrGj-(CDYwA zM|Y3_=QWu9ln&MuNKF_=`RCvLZp5huOit~?N0)qWk(CFM0sKkAI@Ak%wt z9%8#yT6y&sLa6}sxhl6s-0RhRV(13PQvgn3U8d35xm^Ct!3cHgSL9hY<2m1gjJTtk}(Tu6!kamQa zxtTwJ?yCRFi6hEI-}n1c~rJ4r!k3XgRBST|QF=?)tZrC7eJE z$_zAaGHS8D>om^zW8T+SqujGWBai}BqMRXFC>=wM>>B|-F)^<7gLC*;Vr=4EIrPj>mj+I ziU1MzQyGZDx*z}(U_>p(3d?|{e-;pFwL00!NKl64<6}vRRDAM{Z!7e;j^QBHDR}a^ zlNN4Bke6IH-orJm|1`pfuku12AIiQ_%9s3??8hMVAip~Ba2M=40J>@N{#^H zI8F)SZU~O3SMCGs{01H!sw3RMvrnIi;U-iPlxE&3TQE9hzf{u=TAks8(p12}7tKpH zPGpx2(&5COzO^yVW`G;g(y7#s5Ps>Qsp>Dl{)@9 zeg++1r^XFcsek_XA^^lTrg@fGF6SBSx@EpeX~TA?6+;7QC>tP=t|z78=t_ponWyjS zTf+{#i}xA982JPiB;;vRiZ3p>JZJ&o)(NxU02Do}iSSB*I7Eke*!={KS3Q_7*~eSw z_g{W5MFJXDchKtwaZ7qE8qjw)5ChpF=x8Ad@((TNX`M_RQY;odzBCT!gbG~h0_`5N z;f}c0Dlt=b>29=2b!u7;lR$VsFq^h4*y59~^3QJd%%A(L6iexcj{cLgLXLl6;o5fV zuvODqM)M6Uxs+rb-#At3+3x8#{~SSk`dn(4VS8!n$JIa6=nO+z2a0?mwfTQG_qBgg zAc1q0H{zn;fLE~xskX;$goCd3&PWXsllh$mZL_mp4oDCGeVH?1Wq)ez!p38i^qez4 zEW)Kb#$_*_<<4RZgbiRa%@C(+$oj+#3Bh`E+Me5QAa*qgy8pA301ZuU^KfXd@-^VajEcaUAQ*~ z6Jq)zt_62Uhh_!rcclW}>3pS9=?9qK1_qr*K4h@XOAByz0Q&gBBN>fW;s@gAGL%ye zKC-Yt1G3<27qBCjDO7t`=vJ~Ez!Ag9^2sb01}9Zoo>*b~Ki-^9zb%+;A@zh2>t!0U z9G_Tu`Md(ydJSLSmM+&tlNsU?2tBIVcYli^t%}b= zp1c7#Z+6BWe|-?0{L|S-aS{uCY8xAUmsgtCLxMw->NnuPVv?a{dW+~0B@A|4m^bTt{LjHUglld5Djr!k2f#>cR@{> z`Khr8MGBdU6obQe)n$=)m8wk=l8QbU#hB-36mN;TUso}DO-4z{yEi{o^<$1jBjz_w z40Kvh(bnCfqE~`~o2pgXB{KuVYl3FzLgmqC)}07LrUP?uAFv??))Mfab+bjDy7XQj z14BSqQts{C-xXI2TTu-h+1u~KERkRA#L+7yP}6Ffd}YAH?hTzCir0GsEoSTO)_#GU z!hTgBD!UMa^t@OXQO_@1@%TYkgBXKbs8yNRHnYGAS~~3>6+5?w_&VE#^Z)~>T!y=$ z45I5BCF?VQ`|+J(G0He2?g{p8vXw8;k*~m3?ndYDGsIN~a)b9Uwn!hO&$j58k9_b)ZcI#+B$K%lhU&`m6s<;-`Lv}KJc?5qj>dFZ{l>)(dsU+3N zick6f;fuFWTSv4jujSN+Y$~h0>mN!D>VB*JN`-CLc;$Jydw|LDF8A%QfH?qQcv>1I zvX@K+1_&wVOacwv+pcWke)qLor>RZEU z%mv3XnrTbkT4{Tk>kjyx0$_iEK^YSvImxy4#MVvtb#Au~SZP&jsE~o;h_z&p$=l4+ z3r-R3B4uz0Z=*5V{ZSQBzep`iD5crFja&Q;G2^vR1M%Kf(tZz{R$fLy)~CE7h#z8D zj}&S{ev+DsRUE7@Kg22@BSmD!U8}G?TwIu-qeq_kX!!U$geH!cg*P+joGL%Nw-BK~ zsG7Avf+6SZZzJzChY`1nsi86?F33IRHdO9>t1xVq>S|RLjk(48R!#dgI=$i`Xl@Pe zEc%ovoSLyaDE@lCSMme3XMW2b3sqS>I8gK6wts#N71p%7;%+!6{XCWxtGw+|D>Ogx*VA5&@#K}HALByyhXcm7{5F~IpS-qw zty(BSQ%!EPI-K&P?(M)YPJ}-&@yD@O$v(Mu1LqH##KvFm)=%o+wM<$waU0`DkzCm_ z$BvPS?CKiz?HbgNYE2`G9=>Y$PtLip!nJ(TXTAlcm+@M8yAG?aPgfl|)&9c&%`90@ z*9uAHjd1g_ZBR|8Et?pJQ zND1nzWnVc<&nq5$WzTTv;y22#epe+?ngcc&z>AsxmMWS4Tkm&7!|TQT=%HFh76u5m z;ZHT#9Oak2y|#c4aOxYXk^1vb9|MN!FW1xq8gOjrfAbFD=h{OvE&K# z{)aU69D6q_L?_F_sa*)rUTEt3)4Du#PALW}h!n(3!C+3x!B)c6WmIGL^(RzI?kqlzIRMo6qSV0#B4lGME~Jlce5@&D6mpsbeEjW zztMF;R>qvUUO#H1T8@z!_&xQPq+9erlErU5JHXAz#VK%UXS#p0qPjrt#S$e1f@*W( z&dd|fU*&VBU%`#XdqEaPaQ*}cwwXQ>sH6#aoe4SHcnpf4zibSEm{FFEk6)oF{82#_ zeRX4j6bGif_HxkgPxm(y!9VJ9E819s=>p1 zN#~Zq;zv~aW&Oa>4a=%$f8tF--HnS_L29}OY@*x)0LUzg!lWWUbcpPEjiJ^<6ayge z-b`qU*3MGa#(hpo)|M)am6nG7yHT&{pMc+(K@xMsZB2RHLwQS?~uHhBuv) z;|IW^V#i@k>n@FG?VCaO%i-+d#YL6JfGwG}2M^LmCMij)r`0A(;k}v^_snCF z#ra?nL&ex5Je<|{X2M~Va_3{9W4CTAb~b6gyH<9YuBlu_D{pulI+il@6>BYu?)7K! z*vo`#J${46Rm_ju{zjt?j(|04C7Cmh8P~&})LfL-hGK4XR;M$vNjOc9vC5WmLpjET zJ7Z_kOYwkt>%AbWL}s6PBV(+@>j+Qa)Yo$LxG3Fem;|7RbA{Hu1~nbc7nh!ypW9Pb zn^fl)mL3VKe9o&w8EQ~8-{_B+9pc$>Hac`H=$b{~ZFZkzGI*jUbpCoYKm`7S_Lm}_ z&Erj1KDb|X!eYOpzB`g?#Q9F%sJQVUg2&p)yml?`l2Ih-7z+K1YN=`lKObh>pa#lJAxB zOo@NX>4grYe;X)YirThivn8%)gid|sS$V7)PZpob-#t96mQtd;dy;0UGdKa$T|EA0 zsgYzYy7#2oHuV{a5VHb8Us)!_YZd{j|DDelUJaw7=B)-*YN06z-$x1wrTS*7kT z^Z!)G1*+kvBg%xJawp;|&1Sur`6k9~H5H6(Su=iJEAFW}9Q|KV2wn`DYG8-*(;v%g z)%!n-U*xY`)Ulz3>JX@jbG{)=ZuMl6icS5fdHVFWZoRu1r`XQ)B}hAG5)d`I7EAcU z@T#Zy{aijFn=8l)*tG2ff2T#@jx6oX*<%*Wsob64bIJOmkDKkTc6 z`^XFJL`?U)zw)>e(Yd$Q>RL>)B0$$JdWI9Jb9%T{Jd1C`E$!6y&aaoNdYFqz`D#vB zC-dUAy*R7zkwR>`bj%E1I;217ptq@7&+h4G9zb@qU4o=0z86-EqgRrss&ELCx&z>JqiSr1K5Amgi^rqMg~v66 zf!=>f%D&A|RIy~;=zCX>5>1dZ(5;j50Wvi$pW(6$q4W45`z75MU<_zd_XTQB1S&49 zwj0~CSMFbJ`Q^0|e3AL{k;%ljqy8RF>oygq5|?i^*#mL(ujBBQ)z^!hJOL_K&0i5M z424%UDNg2W^Yd8bu+ICuZ)F}3L!qw;j8yZFp9J+CV?E0LF;z!nFg8z$Y zBj+BddPL=b#L5()HRyxxF(J3A0poFuEi-akiOr0<;Ny+T6fJ!27{O(A*-ZbVX#8vyubtbBj#lkifclU115nA2PL4cBy<`o zZ(%X1gBI!*!CCh*TvO|N)GVz(c}*pf8PDp80>9@9cOO5Dn;!Ba>p+PgjLYnJ)5M$9 z`&ObXc4C{f7SBhzfZ$)SDWI|_0=E4_L^XAYVnN>~fTA9vuNIp!M)(Ujnw+Je@s6Hi zvhlFWG5uv(ETvrk5WQE~-_ybd!ZBm7^aI`bRi&m-?47X`s>>!RBzYRybEtm-$>lcT zM!G5**5{G?Nf(J`bqRKmGFM~-#Ew$n0;#lbF67~iPtQJdu=z|_G{?P(5=w~)0{GZ0 zUXoR`nJ`pBA9hh;bnS*4AZy=I*zm>dy(vL{^aZvs95{%d`e_520fk~bFeGl1^IbpX zbb7QeGD_}WXG&mf|L!pQK*@gAva?07LA17wg6)>qz7r=?Rz+gbCa88kJaM@$c)4 zlXOx^Oakv`ZE}!zrI6+p9`hezh;4o5;Stitr2W!}u}NuoEjl%M$vuvUYYLL}WEFe7 zYE#62GjJrWu^S+YP3HCI69q`A6ky$b+<1#2PfeJ)lc|rK72sC88c_AKR3lR56mR>f z7YX^$jSX^(Z%gl^fEQ-NVK-wdHWoql2Kra?L6#58i&69D`1|Es+vPG*&&eEe>b?C^ zS}^l*Nv9x5ThcBDXPZDg0~q0h<%bRXHbk)SX(=v?3Lrpfw=eq}1(2iHBLlfMV4pqV z=Zt*GBjuBT*{8ekP>w)aG}lA9JUs|HY>yg7DLpgsH_2v+sbxuk#b29#{Iqh%RLAM9 zozw%yhll{Zq7nZAAL}_5r+w<1_+(jcDsv!vRquTB5I1i-iJk96jZ}BqsGnG|dvXCP zh7_A6$yU_fFR+6ySdpwyzDl$*5>Xi~?dyayp{!aWdxEkh$idXeyNsEvVdiCYs*_p9MF*?CeXNFT#7aQoyRlBa&)h=>bucVaEhra;r-< zvM6-=)_x1`p%HqT0Jz1QH@lvtbc*u~bYB?YC9QCdo%4-lvUA$&#S>_EL?kp zKwZ>)D963~X&nv3%Hm*ZX3l7EqfuvAw`aa<>fG%Qitb$R=C(A9YOjHnzLV%um>m)} zTae{DlhS0M#iy*5P{(Y&HesE z=^(y^_xM3X_Z$B~j@^rDM77|iox;Ng9gOy#Ue(%B(w&Shiv0SK%@rN1COr-L!)7yj zbNfiMHk;M~HI7j9{6`}Ul?O8w1!n`RfZ#g967j6WPvFr^`rZt1;o!jxc>!G{xk}KT zmN32OBvq?eNAghPS|m}*lfF>qK+&3DU8@{5P7c!?A17nAg~|TGB+!8aZ(R;F#1m+@ zvQlA~Yhf);8-w{k9^7;M{DDLthz90iw`LCIoGmUn%{I;ipXmPeYXEA^JkX^@K2@ftv@B0fHs3rD>fzYOxr0&vXYif@+NZWigcu6mq>dnssB}%w#2f^>|*aIee z-q9~9RU;VFgB`a`O^D4TV&~!ziXZ&rD#Sux0?&W_0ziWAx?}dpSN_m=-5h(a&$RY# zp6qwtXR<3rYWdM)a4_SiDIJ0U^+@WC+Twe0@y1m4E@tm( zAeQzGo8YP6lk!x&(-+vNk(Y6u0uw9>>lT-T-dt?9+2KF=>T_LB80I{mnHGmC6l2*pG14xj$IapMs4!$q~xGZkFF(AW?; zo5;R$9g4#YjUFYW?u!T+_)vzpT;c*x+i2`9eQc4)^Sk4N!VuxXAH5$dz$2(g(2E@EpAwASK5e#fa zOa#Y+Uw{CVqH9TwF>kEitq)#_U``uN7bVu3gKmm!5MAx(7-3rJ2z%Ld3D$X0u%YYJf zTrF&RBQ}a#Jk+mL#kI{G;j}{uY{VzXvr=(``nk#qRVPwvA=m``sQN+9ur9Km>s+)K z%tLDSg(P|XYv>Ha5pIQ!)9(l_JvXC|9Bv$Vs^I9BK&sz*fq0s(4^D0g*o70+u=-c; zhm*uK7ZjTwuIl$)yYf%f`l6%+Xg^x=OfJRWXV_NJ6Z~00y3y_Ghg*ng33*JvrANuI z*j(izMMF7myhN3HTZ(99En`lPP;>BK9x3kEy#q@f7eM_sy0mWh2*8`nyz}Uq#0Am8 z0(O(M`m2Ww?840KsVKcEz?dliZ*+60dW{;jA>F%BCa_}s6n z5IT~Y^=NfvF;-0~uDHCmxE=Lc8KOnly(&Xqc`Mjkv-lH;a-18xq3pYQZkvZfNo~9A z{wCkNhg!gtA`8C_VH*xhOn&EGT1Bru%(>lYVSI_|`o(nta)*6jD^b%5#iFlv;E{|* zw=@xIh+LCfGh|!jY$oVzQ%534X#2+~I0(7-Iz*DPmb&}OSsAhj>($XDLQt&voRY>6 z%-hxT*dEjGjg{BK-QN8_F)ViV*a4Bau3QUv3-Mj>8>s>9^&1x&1J8H4vM%}cWLjfM z+GitKpz)b>pZe=^Yx^b1(Mbd9S}W}CKB=LCRMAnSRexCCJf7==;HgzwF$i9G#SbMT zeS`!{^WsBBXjjaRwSs~eS2V?L*sl6e4q!~LjcqB<7_KfSpq7mxv>!w$SGKEkCS%{q zOZl-REh}rwa>4{YN?{$bilXS7TQI}Y4~5m|M=#@!?NmA`%aX=ExBy@(o67;Tvhp3d zOagq-xXX<+CeiA(!1Dz1u%WXePafoHLISNcK#%+CX)w^+T3JALn%X?&DXTwcTnJoK z_l2NBz|LHNYJ2``YDJCniyS7PY3N45_07HQJ+(c_s_`O_UYpBc-s;SPAh*Qd7XU0G zz+J=!K()IZ$p{tI*iK8&S^B@H?SY=L*F{{UBe{w*LPKZy%q=YLs>Y({T6Wx58m5zsP84?;kK_=^orVAtF=T| zCG%geM4RJ6|~QU>P%NQc&{L5eoBusxg~8JyxSKHrF_+scHk`&RDy~ zE}vi1cJxK%Ob4@aQ%kL@jvh_XsXcTdk!LdOp_$E*j08n+tb!W8vpnfJ z=HAi^O6b5M`=+|L#X^Et>UXycF9OzdldM3+aA7Pg_ZehwI$73~F5s1c+wi z@;CVRHlu?}Lw9^BXKkgd`mSTJz)y(fPt$Nl3MM3$W&)<}XpU;}vq--2k*K-y`!?-~ z5FBfd1XFson9DzdB^@-6Z+@*>O6qB|0iS>P44QS+I94qD=EeGXeINgImo{L?D$cpM zDZaocj*fc!Mq<^cBJ;YTTnaF+?F-Qlb-35D%NzEfIKf)L_u-zf;Bbxo8k{RF>A%c0 zBe!k8+hthz7C*eAQbB;6CB~1g8g=pBVmSo;bu%iVMODv*>A@xCqj;ycw(FGc;~nzG zrL;E+z(6iDl>jxR622Nx7kW zmP=8q1_2vJ08reXHUiL`V@)OPWqp&+DWgY$yba;K;8HQx;_Xtek8|VP`^y(Uub6hq zLLki;mteW@xd#46=&<$#2c6nW&2Rx%$p66sl^Cin&I{u(A77Jj!N%s8FLj*eHS(G_ zI%HgIYfK=pYD)PD%E_3M-L_|tY%MN6@z&YUH`5Hb64H~gJA;RYVS~bP&ONH0D902^ z6=)b9G1Yb&s4jZCAd7FuXy>y0PMKtp*jv8b-n{9;E@YZUNuek=vudfLea9b^L8;Fs^?7D$uhbGoGQphwZRs z+!^Ym_FTdddVIo;JZZzn&DX*_<-CC}wTWdQiCXp;Y3ES^eq+OaFIUSpD8f{K}^MmasU2oFVZrIS(F@5F$@k)d|!HPuAd z=!W0M@WP1g^>2-9maMb<0PT# zerhzxm$)PUOFqoWW&(!mx59+)$Fv}G+6Y?Y1-23`do4g}Pg($lKC}`jCa+Jn01qL? z0PqrzE6oz1Z(=VcVY7}yoa6r3M?b(H_P`RfRn`H1Uln6C1@Xp84bZ0XEK{t+N7Wd| zZgs+DweS9kQnx?=I+nk38gb(9n=cE`C%gb!36Dyyz*jHFBj{2J2lCnKPX7 z%`q1vtYY!=1bLkT|K+m}mBuITHaWW^?EKk>+OzZvUvj^W>BWXn$rPu>ZM>Nmz}8UE zbcZb&YIH9np2WdjBo{hb=wvo5jthAq{HdbV_PiE^_*(9(5^2h$uL?WREhh|kLzWhd z3IU>%IvW;b=E*>eiD1h)j`(v7D_kvOQGp;~X5fIiH}K4VSfW$_ zPyP95;7O?h_*P0&0(W%bBEh3CPdfM;Z=~YVh3?Bfs{qCd+pRlJRzdVv&`uvgULM5% zKUAmk$|J}H}rW}1xyCnE7Y#5dmJwr+H5p!_y{2(uKPsTkm52qHL_7bFNjLnr zp_vJ5pFL3}ivE~(MqnO4Bu&<3LKCAIIuZ^#) z4t~bICPG`?+du*c2$14=<*EQ;tty0cwZ4@uBkDXZIC*Tquo9PqCtOSXOG){sX4r8w ztzXa{DwFxwoWf}gX-xlBdah*&eqM#o*HWAH5R-);d(ce-U2k1cuPIuwO44|W^r=4) zwmYe);3sG1jHYs<3JWPh_O#TZ1NAnYrBxS{P7JDTjv?`5QJ>SrnN@IP5A+j96UL73 z{k)*ePz(SM9aC5LiS#V-h)~Y>qt3;D`|9v<-Pl6ccpdkbV>-q4Ze1YDF+R&mMbp8S z%K5YwHI7-i3!zN{6Uw~#+K;_Rb6C0{bID_WD z`$pQPvrPLN+mh7XL++a6qsp}?{srKDQm~rOd?ZT4!u31^?DkCREPe#|^2aC>YUgll zk1|i)SC*)2?BF6CBS&+ipZ~UYwR&QA3i+ga#Q;Y@xW6?o?b(5=Byiwn34M9udDdX8OpuC$!?;e5ktN}I5s7yX+8nzQIlM!LoEHZfEw>& z)btGkt`TfL&$(r9Ghpz63a5}ziwcR0`Ib}T4W_O&4WxaCWzcrh$4 zSdjNUTbGPXBlj&K&TRyBCuYfTvnDdSJkR$ul!RUdH74MIlS4p*yG>N3FSP#TC*#aEdPG8oeGKJJ@)!#|hjRi$!!> zMMA&=g!sN%$o7VyveoRsv2tXhD+>?$OqAXM=EY=x?(OW%M5g_2^mo~KKy`N|@3fP4>2ix@i2MTLXDoBfSVo7VLkMdjIhX zJANZESslhS#C4ocYS_d}kl}#{59o07nKiN?4Ib-Rgf(}(?K+OGBpXUyV7KE#P#$@+ zvGr%E;GY)Kd!s2oJ8<@UkCiG2xzPJu^57l=XV}|5O}gu-&6s>jiS6jw%pt~0Q-|S2 zAjgrnpF5}@<}9}$0!lztY8U1Lp)W|ZxC$-~H|_ZiV|@Esrx!2dfbtf#BBRqCoaKn? zoaLba?}l#Q%Y^rVq4a*x?Mfr`WwC(Z%+8irK4t}yo@!7hdO>cDUi$o}_6Zerc6?4Q zTEFUD#}Dh{GXig{9VgSzwiX~iI)My~EUk_p!Na5Xg`u7ml3RZS%)Yf~ku|;1kgWQ* z3sctY%uFi7$JKB>xn#re_RTIt#NnP=YCQl~rLTsvb*Y9z%){ibXFl}+VX`aW7;!Y zEO{6rM2vH8-6>wZY9XG9W!F>F2%+p)UK2y$wc+g{F_5KDhW`Aj>q? z1OWQIO?lkIV2C3RgY&F~m(M-W6HAEj7pJr;Y>{VIC9I`QW;g-r!cy0$u?#ZS+Gpz6 z8Sya4^IK?zWSwJDRbhsA2J7xSFgR@l5W>dUFEw^O9 zH)z=KpmTNf=;@Ix23Zi0CuXm6-R|P@P_>n?+P_WC*i6=n?vwe;DnEr3Y!gE>-YGLU zvZT(3a{6m%vrY)V^`-M9a=YOVTK`}?-gFoF&FO27KGBZMTF1j+x4#1=Ry>f_ghJne#z4gvZ^A&8M}0!Rtzg$(o@Xo6UU|(G7Bo$HqENh| z*j5s$NB(AUIv5DsSzO71pxn%BY}%cqkMZ>3nr{5Z@v^eSXEEfi4M;YnyAh>FOR-_@ zoi|;B{aB$=^U3glrxaKOj%4PcX&Ip9GC2$I>0jheM)g$$oqe=^h!Xe9gwQ?*RQ((m zx8OmE87JgWv{ZGsS(eAPa7Ahc2G}+WlmlqMKKA9afbt_Qs{>5RJUTi_vWk=iZC>=v z0ec9{_enMP8=mh|3CR^8h3HvU(qTLG=GUGglBD?n+hu9iYnMc%LosPVx#3o2z1X?#^a&u;C1pSEsWw55QY3>{p$>MCF_^!TR`9N zFqmS=8`?|NfZI<-QsnfyD#T3rV$D+zz0A?(KbeWqnDkT}Vx8js;yUfDeQf~L1*XUN zshcD8XE&?{2OD!98?v(rRSjTNdEm*#YBtyWO78A7(SgI+kr}&-)b$&7^y%? zYbmS8OOB^v8Zfmyr0FbuRy*FcKw&Pli<=@A)V7o?%DaH%5lWz*BY@8MLCzD(9s}6k z2`3_r5GAMT!$z>l0Mjjh{QWfMbFe(cJ?2Vg;@sA^!Q2qwH%E+x$h9%C`aXVl!JSZi zffBoZ5U$EOSGaUD`&g5C)nDQh15Qnu(-wL|8u&@Jqvsgz>+~%Ej9uf0Th7rYs?0~v zrt|n(9Pz4elUPP?bl%}Kw*wU`q&f0qU?ghAt&aKzBk z6tvhL*a=-+=>F>>m_I)?K~MJnhJ*`(v4+)%)Zmc8v|A<(U>lY5M9&MxBuIFwmJx3w zirL*Qf(yKxvZWiQ{nQ2%{&s|y7`b>Y=3)W*C&q~hza3pny~$5>wm?3M(~6?gG2$c= z-eY3i$nD@e*qXtSEj!@&v zmiZo%VUwyV@H(cN8Ez+Jw}S!U{TKAdMjchfX(q#>5FFz)%;uewq+K)R2=9hIr! zeZSVx9@w;}a&thh2!Sa}0Mk2F_^<(dGz8RV+WPqiiEE+kC!yy49{jdkK&oi0g?tW= zXBeRtdvVA0e|Pz@?(oqCKza!1Gl$v6!f+`rB2Bd3vajGKr4#sbGs(|W>J?|&8`H?m zW7HOwVquRx>Hmwff^0u)ey8XCdoQGFB(b16Y|tIA;&adQ33C+5x8H)yy?(CXNar(2 zXq+l~E{kgt(r%}Kq*dyG&|2WkSHWBQLB-0)Iq5+MXYy(O_muNp6z>#QqI^}Z%NaYj(!1(yy;#syzr}o$ULw)nu^t^eW zapfFHyhsr)v0`=XDFoE&>(YvSAhits(N03S=j_^}`A73?&e4T1-L zHW^m7;zsb5z3l!fawE1<4+;-hQ7~GdiVw0u)`bFUb|Ypjv^lnI`q)8sc3b<3_u~jM zX7-CF_ryME3-=n*P(s*1sXew3b^5n696GyPJj)mCa)D5st~jXi{;(0~Es>|jq|FP# zEw&$d#S`P<{|P z(^7j}8`w-?UH2U%D7Bdiq;BX#{dv$#iiK8RtXxrOJomsuA6X!B+SE`5|K^}bHJt(h zOt}jtMBgJdxk;^Y5RjB9cX6bm5g!D87|Z z6f`g`X|bV-MERm%BH}&y5dh=xn-9@IQu!pu@j(J%`Dx+*L@J2+5)W^K^E8jFspO`0 ze4gF7JkvQyBzBYB0!!972rYGOl(+fO=v{9D9pO-?7b4aAL>Q@&w1 zYcG%9U|Xb3&kS$u$M{Qcb`XUE-w_tk1V3sXpMtM;}4rK?lNe^$5<|?d*CXJku??T>)7MiveV^eaajH#n`l_uVA!2-&V zB9Db7Lt%e5@`7m5S>?^PwK7hXdL22+;zH4FH(R|2fUnmq7#X@OMlge zyfxww17GZsF7G*+YU?q;2pPo%Ry&GUV!*1kvqjBmiQn)=-j`ZWQr#CbI2m(YZIVlq zPK<~3wzTFATwZICjh=)2FZ*OL00TD4|`z~eSgo=Aw$f6`Bce+phvxQHPK=JijZ(>LGV5p^7z`zv6 z6c2rR2x{6V*wsy>ZL`_w7k}r~tKoqw)z;}VU+=%^d;DZ_`RBp_-}rv44Avv|wfc`S z;~*P_a3WU@16>~J0nhz4bMXLa-xbY9{mGaGgO(YiJlj|k z$6!t4KXB{J2vyUgI1P}Fn*m+oq3ayTV^t_-(5UhX>VvxFAI?|3*Z_BWG8N&t^l1Vk zOqf~7l+Z0eCLvmb*jEEjdgdV9t~<@C@zD-ZYf!53f^%{Ygu{+Pe>sS2%p9q6sMiKF z9xy)!QwybbTV%zvASt8WzsqBtu=G=f_PjezOkr_|z}#Nv0mtv2wd9 zo<(oEnVWae((Q9B`C@bK@4mVd(jr9h$`t5Fz z>)-Mi%x`o+^IhLo=n!YK~7QLTr*Z~``03R(jqW)w@L>Ne9A8o2fiY-jh)|4qi{g(wDQif)R z+5_#>c|8+oGTv;vOV9zAYe@LZ@HU!o+8E7DFWF5s0!O{EN;+pY7F5dm{rjlJ>UVWG z=C-iUHUxVk8{5G@`Q)TnzI&BUSh;x_?55#AY=QMD?*aqd&6DOg9gslZcZ3`P#-nW7 z^Cn8`+Gzbh6RP8_VtNkUH<+HIV-x|x_oHX>lCUBC?XOE6wpG}h9d7ni>9IKNA{wBE!AfExIB{`7fdXDu1H_m7 z?LNie6<|lv)2a+uLf-wU!Rq95eFrdcY;f#-i<0Q4R)o9kO^6G@^z5JfiaJ386r3+p=t6r-<%V438Wmb;YoM8f@yXV~&w zOo1<4h!Tn)f@rSm$1w!p`Fr+Qo^6@J(aj|cabv@{7FH$(R;nbYHwPy@s$UJW2z@ko@ zHP8^`_{wmjxFOJ=v*^UPmezxCXQ64gR=cTS$&F8Q>A zTEK>#uhMo?+-MXZbOCxlI_H(A^Ic%Jv{}Lh+QK7{cc?hsRs(pI0FY&*9>4pVOmbt? zQ?kD)>|m2$G3DhUy}Csr+R)51*Px`VcrMo5Df>BVKqPm_^*Bhd#9vjF!{zzf%Ok`O zdhW(mtbo*h?u<*>Ca3@El(Nr1Coqw7DS})y9*(o*S!`FncSq!uvDQ?ZYnHr4iDyH;iQ*$)XlbKHLP>xpUlt zn@G6+i5fa{?cpq+knCgXO+^a;2A3Y%`9a@$KfCzHYyC0Vr=4o>_Sve3JL>~2$N+7D zY%AUGYvP&dOG;X~KPC*?lgysWcq3t~*2w-is$1?;k zo-08Oq@I0@gZ+=PInTWs=J~Wcapaj$Xx&A$5?|OY{Dxw~{vr`+RRvh|_R0=gvMUXv znfEPBSuoIQf4QK)WDuR z>~x`uA46`zjJUISpt7zkAtp<3>Ao~TSsfLWu$X>C@q&a3C#Plh;9d{bUA$3cMZIWu zWsp*NwNZ5KN}cY_em5}hU3ULFg5bX^b6L5Tt!z5^(sjnQgY1uMXE zUHkZ_`QJExN~N3c|MeeB43knxxa1)8Rv%(rQw!pPxqPiOiWk$Cxjx1GSgX+bkWy!l z(b@h4=a}H0SA^5_bq|^qw~B8P%9s}g75sA`xB#%YoJbp#+J6X@XP2k0z9t;*{>-qy zIuC*8?WfwA7N!Oj<1&<5Q4!hF+J{rz^=69hhbAB?gtR%cu8;N1nv_B(YZQ|<*x@Zq zkwo^=RvBpl7(oV`#fUpr3au591x1~cGhyin*Y+J_M5Ne9oKqv9PC~)}m+$1nJy4*rlY6bc6=YgIA!cK;Di_}O3>xIQA z{5MpycPvqji|VLQNq)Z1bf2{>n-(dA`ttoEVQSb+xXa;eN4Wi12rD|6k(K+-<_a_9 zOhI^#mjttoNNzb-fdM7!UxaXun>7I+jiu~y>oItYJ?00IR1b)|hDVGNgCSMKw7<5n z{=~~FM+`WSHb=SO2L>>44>{GzHR!q&)wOAG$vGoCrDTaB>^O$X)yD! zL(tu5M7$nixvA09nJ7ng-LWpyMG-wbY@IhqSsTF(cDdN6h}rb zpCcd)S(x0G;J7z<=8I@z5E@nLH2sHkHHzjeghy}8Zq*eo(?z@K#IQWM6qf$=oEeLJ zT*mXnZZbdA?CS3Y77J~bE_ zr73HRq-gPEHecja{V5s`wc)%Q#(ivz9Mv!(AparKg%%=(A?mV{+zcXX9HLw_Db28_rsE z^TQKxRsz;HRW{vNG*{Cj!;`vqwFBSZ$8 zafWPxV52am6h#)DRJjCKP9O_?TGS3fa>{D;_|^Z`A_&a~Z28RCWcbuTqT38+hKr?o zcKvr63xMAo!%rF&-5Q4hXF?Fs<2hHVHGOB;!5?!^!1#3&`i_~2JePE7giCXg-5fEF zjy`S{%9m`!iDx$CFDXjY+rI(HCNZ$HJyOGRC;)x> z@hO%YR4u*rzQPx?cpj~240mGD%C#e&lJ(xn6}IlUZ+kXjVOP3yG~|$R$qvh48)KUy zU6U=glf5*_&jyHBUWq!=6O90h-!nar7%v~7rd5VkhqKhk2Lps->I@0KQzL*YiVKhoh z@&^}XX;St^bVXiI47UFi5@%!2k02AmkrfPzyksG(h5B{U~vSoaBjTU_6(Z6*7`1 z>5vMnDfsV+$go#aIq&-kcJm+v812p*p)hGIBPpQB(6DdxrcBmkv~?vfNaDd$XN??& zWT2BOew*gQ;r6nhp!{t|IV(A5(BPK{G)ao7M$Q(ZeN|Go=*n+8pg!^+X+)#-WZu^@0E6 zv*q-_TpgzGcoPqBUuGacLtcC8_xGt6)H2eoA-z`T*UdglMdg8EMxtl?7=BrLE@bu) zU;bwv9b2zC7gr+&Chy4KdUtTh)?h*IPNrk?iWBGm&pfX6FR?Z0!D_4|c;TcBWbtI@ zstVp*3_R$Qu(RKYTcmP)xl+n%tn-a-hVkp|!XTl2xSu9iF`*Na)rYSvn;!vbP8>WR zdixN9iV=I0AmD5(!gfQN=F4I=0O%1UesP^2HGKhCJ$!%~GNxpBfN`>_MQ)8o23o3} zGG#8}TCNKDk>%+(3#v1~!MVKh5YdFRD|d}n+%Z1%(knsMm=c^-`eh*f#QxmxZ?=;r z@ODymnnEsn!+$A~qF%as^LLsu-)5}OveFPlfwzI%yw~7kBMgPOc0#K=X8HfZ$Cnpa z^W~zP;lunVv@de9ZVrp$a7)}`D`s_t0XxjT?N0%Wb7s$(CPfqqcsXf-x z?Z=fQnQlkkl$J{-*#EkqR|7p7io2iL&=W*;1NBdbT&$0pj!2;tl=#}q+=RbqN;OB# zR&xq?Yaj7@T;I>nfHWJBRbydx3!-urKG~>Ig_o!bUa5=^%jbo}ck);Zj4}ZLl+l{= zgr1UKvL<3O({n+`-HK>Z;XZh5C!rj*kM{t6CQU6|Vbn+D9F@<{wQGrZkv zQEAmf*H6yaX2-%jk~kVEJxhvdYL@R zU}XoE65TrDir5BLScT~$NAZ9ASn8B{Tg~8|=g(U|tu&czip;y-0E`O3GuyJXFO;Uc zuRW^a_c?q_`x6(i+*lZ`K9cfSAgzYz_b*#OJ5ane&e<$gphrJ0eHr1;=A8x~H?30_;N7@-Kcz1O;t$g94 zxEfX6g=SkI^@2DD`-@}pghh#KsW-vdiB3pnn?dA|m9>Yw!7Y}+tonK;5UEV}BJ`!;BDCOz@cOHWKo0`A)f zoiN^7`226b%AweO0^x$0)K*voepN&&TPg=%7>GYwKbGN->zQl!i?3=&{y4#5Ez;+* z>Tx7Ljkl|}NE!v@-a>(t@-LmXnwy1DTsd?H;#T4dt%$u(?=Sw~!NFL(LYHtvf!-@*b-;3N_?rO1`xjGq*ZkKn>qy1iUJg!k{?lFrSad8m{@J6@`~*Qaab zfv>y28XwY~7ZX9O4{&H}V?gIH=EhjgTJ9*-QA#{9u_=*>XjLyMW#~Sbd#xkU@~9%^MSK!X(;bHbXZ>G?Rp{JM}P?QE-djLXh!U zL`~e|YxE~pxAEO??d#V<=RH%f?rGlg@Hdu!LX4iRyhS%q^4sjxI+~gAE6+=l=cLCu z1H2L$oc+6luN9$ENAq%WkhH9Di9#<(h zQ={{XxjCS2o&R!5Bc!a;>5Uv7iO{C#7Z{itAhQS>JhU`m^Z-9q=Dl*|-ms>z zGr*95yu1=za|0NwG{+1jh}ZkpTHuNsm7wslq%X>yt@~on=n?6ZwF^0y6;EWEaUS}w z6LK6gdXs3ij`#>(W(8TnO!12X6$#2>dl?$8#pV}8JGpGi;)|h%C+zbxjn^yAuSb}~ z_@>oJZ)J*i;bo1bUs)3^UNTu|!S{B@^FF!-kNJk4OwRWF%2l*WVO?jQ=2js`-lUgLU6-1Qq3UwU?0X$6lAu35Af z;tj1$Uj#@oFMD+U<{ag|a}w-SNRL#9^NE5-1dsF6>U+fYZp#fYo~%WB8eJE75yh4I{`5Ft31%l zj=%)?AbuY9I5w5VwB2~%6}?-PWhT%xtlQT2In}dm?TvG(<$-N;zMU%SNSIYmrjHU$ zh@t~Oc0zJIoXOuY0{M~R6c(LU>7K@Ms^`y=H75hrln0*d{5O^0*-Or-qlNVx(WPHi zI*78CBf0R3h7MX0kk0zOc{c8!^y4B$OFU<=`c(|1+o|a-GBypdeHHqR zRguyx(PicPv=c?gv#&aa{CNtN(hWxj|Kec~+_;Q!bq>(mA3^DSdeHImC2`ilwtBYR z_@LS~8-gw`(rkw{{-ptzP1|zPD7SFfeinlxxScecJW=(<6gL4(p;_ai?fVlmPIHS@ z#plbQAc_Oy^@SpYf?7I|jEe0d%~nM`483Lf3z zCFx&aD`*$rgZ>w@n(%yQe_wwCX=mIf{+EguZERj-D$uIdSdsit)Y1EZ8j?Vr1-)oz z9@0SUW5B0kUHjvue8e1p$1hE*CEX-z@yXpeW$Gr+hN_Bb{DTN$`&ZR_L@REJFxUP( zuw?Yp(7rj{FNx0oI&CZEFp2?f%|TrKhzsDLt+@T z2A7JKJKg+&t?7#2pfX@?+~B0f&!ZSCQK3gqgc>tl%M= zYO<+&jIfm8DA1AFEOZZaS@t@rXMWG)A1dBt8?To2g!Lm8=)I?(h?{K;!su$3VYpF^ z$(9L@n?#0*Ia#-Mc;|DN+0G7&jbhvdaWz*UJ+Ovx^WT5C9b74h7K*f)@ToP+Xc1Fj z?e8GP?s37!u9Oio8conk5%HCN9^>^RR&8aZLYiVS0JuEg zrD(@zbmCnRIb&7kYS?Q3G&ffNCV#REMBP0!maw-*|3tqG_eCk%;ChQlo);{XpW6fe^%fA#*JK zdKW@%1*fd@MS_-%ea>7T7;CHCDoiDjCm{b!jyP#+UvW@mrI{NNrP;@+U2R9Fx=H_g zG*kqA$@nROsMltXo#;OV*TvdPlT7z4Y_@qknh0`zdI(}5wss53H-LNe;lulC0sS+X z6lOrFMF43v%!Qf92=0JdzQ0_BjRCW9vEZsD3Y{?} z?botktXzag3W0_mfjPo|5QKCJ0I|}dl zx9|Y!vbgoYARm}JmY|OE2T?|Jn!=_-mHvQ*YAoon{uQox#$8TX@Y0@d?;UPO*fSg1AY>CrKjA71-f?1N{}d=NwfOgvqW zJ^MHFfM+gtJY(ZE4_fKrRllPWjoi?Tw6&{Ijc5}%)zb|i-O+Ec^mKDN0aiW_q^0s6 z%B)j7PdP}#7dks=X7?E^?qL%Go}R3wzXD_WTkxDT6g2w&6{aF2YmmZ2uW1ojnbnqO zcA!0!2NV7uk#JZUDk(EuQ{NO@zj3cxo`8#2VaNtS7ek-s-+kvV4|kNe78p9PdyR*m z!NPMXuLvl8Q&_LEuE5s&w?-=3qGqz;p;tYYwm5x#6IvUVZibqP4ipA-dae(rsB&!* zvCK6ieJ;RJ)boN{*(-${78tCDSD}RKDT_dqq^pi=Er+PEwb>h9SUQ-1nXh5@UZE{n zCL+rmOG(K-$pXE4;7}H63fuzaBj`joMkF1Nj%u~#0}O+P#}aj%SF zH9D#1nWfsmA%zwrY?SAd&hHVp9&o;K0axy1)m-z5_-!$0JYr={Kt&#i~Mk_c(Ewd z5AsQ+^*)kuYn!$^8jf|yOB3xqD$d5b(88491^_qz%PV`v;9FZ`=%BD2;>=gCz?#vk zZ>4?i(Su3HDcpJ~-M_p(X2SIGV1#%6cOG00l7vfND!L%QHrMwibK*5g_Jd&)YDu zxf*ga-Y0s&ZU)7Cio%aNtSL+aurUO7`K)i-ddcH9$}18zf-?r+FE=2Zcb|ijuXo2J z7TIcphk!Gt*;XaDv9b9-hz1?$KOd=p3IvLjTp(Ec!E2v=94&&-tKJ0>JjsfJPQUm1 z>u~QrQy^xo09*7|O!6f=MK?+<9|QH+qjI_z$;5GZ0UA_J%(Zs6{8)MszCrkTkD$|bA0zNxlkn}eXSDef z*xg(B_iyQ%izF#d{@&(KUiY-^*HW%pi-YW4_Y8b?;YfF;w0CNO0g`$@?ARRy@r|7Y zHyo=g;Ltr*0F}Gcp}SuZvswKJuR?8IT;%+gn&14&2>6-~TIqz`sY?6xhG1(}q_~jo z7n53ctHAm9OJ9Cx1C z%d_Ttxig z_`;oSw$tn-;WK4w<@G*442WnG2LI_DZqlaFBn)sOJbv3c4w3lTLwwIPu(|I1g~G2D z+KH;6B!lI=aKJwcXb}7D^~4wjqwotjZzUF5xfA7Y0Ym#ANQbVcLp8Exq=ap#dYM2y z#ClJYfA5V1PIC8~>Ju`41_VF_wbA!*8h4km*jd>@0Py_jR*?`af5(M2iEv(1gW&pf z;QAKXiJR9U>Z2=b+V<4rYHU#38mLO<+K!+`;{rf*B+fHuqYU+pOuZQQ&mIBt$J=r~ zrGqg9PgTxN2j;EZM(_vxAKYKnp-VM@-jUYxb-DSLLHqBrqA#mT8JIy&26Ci4IOT;3 zZm^!IU1nqoF?oqu7H>*ZaVTeidv}OCWVAHhjnbTnR5sTZIJ$qW8Qz>YAO4wa3K|N{ z+~-Y%{s7zi_g%w_I5j>MIr3>jGZxhzIa55)OA~9C6u|`B)nJQfT!MQ9N`49GlwSNQ z3WxdBHcD0D&-g0ie2<$>YwwagYK8g2k|e^7*Z7VDLcrbnBi=3@!@G7Y>CmKp7z{5A zZ1G^;*GADvv~*{vpfOb(mZU(e5G6^+D< zrI2_$vjeEsNLmA7)O^3%ibrQ-o4(U)$=)1W^31>ldOJ+HluL;ab6cj~82ixhJUN#D zUarKZ#jl52%qayHzpNUs#Er(6C>QhdC;jA^L(_VWTz|}LZ$?-pksNfw=0`%)8UPe8 z$kiU4A$f8ro&K{sD(0H$a6s9$gTuO~FwpEh>DMv_7N*Xe=RNjLpAOsaZTb=G^EnGQ z#w>9ZiL#Yeo#tw$il#NH*JYRjMx?)3LGl^C!PSU^>A+P}xgk`@UMaMvo}|L*ZP zW|;6<(xs!UI}`kzS$dqtHsxCBwP9lW?j|sxY&kcOekF8Hc)7~Rma;IM7H2a{1F68v zqY-Ak=2@O-vqK6DW^fXE4TBlo`!fAe?s5*p>8^XJCH{i46kv98y5lKU*dAj&`eU7A z<@SSa&+kw!g)eaX^?=@WZ7%_c%k*~Eh2rJKflLD}PsCN%n*D1vCA3<{ou>p{`U#%Z z#_SrT8u-^@96l3TQ+^zTha`0p2tTQ958~`@I#Duo6_Ta2-cIH1u+bJ1&QQHo0=%p0(-c%UpCliJj_wGXQyq%KnrhM72?eDv67OnV+m^vYYwsuP;3v%!=q1gfyr5{kdF%nR=jotx5sy{gKI8f*ABpMP~ z)MeI{B>Q3wa}$)6KR8d-U9gr}h%(rBg7NM3%z0%Xpd+)XeGUx_@l~u?Az!>+-dk;> zkf@^MewaveA2amMq(=P%l3uv)@%AeZz0U@vO#7912N@O&N}PGgHQY`&`=h0B(+-b$ zJ9KdeTVfTyC-lp}fnR72W2M-!4v=FvFI+W8r$`8P4JFb%tj4L3CUrf}DO|)pUlh<$ z*MZ_*ZNi?z%{(Uel^ozDe9KRCy2;7ebZmF$Z~)dyuuSWLe30Y;%|K>O0&Al?0HYK` zccl6yE3hu$a_(KF3~{YOfW}esu)RX}zD;-ug(|sEDrukmk9TFzczqR^RNfV8k{ecR zk27L>4vu-)S7Pn(CEdYY(;xtkhBA=o`TedO&+x+Hl>lHB2}pweoe1*_lDK+39CShb zf!fy|2->V6AgED|2}}Q_Zs=FP4x%(UGcu`UvLK!E>=|_m<-(efmEt3dZMl;K46!{b zL6ue5$tNC@=W;4J+$lVw0<-J8omAgm4=A?tSh+s?kt+>UoB|9hD@UtjaHyqw0W(0_ zyC)?xZ%PjruZHgd0surMo=k?M?`G(OYb@YV84IEOuNZHappl+`pm-mXxu5z$McpA1 zqfdNtF?fZBAr|RHX1;~*tTXL^4%mwICS0>`6GaJ#b_(@lYK}-48sO*9iqwuW1(`H- z&?rg(eYOx3Z1<22SDGd&_d8XA9nS&KD5k0~0m6e}E=b+w^&Gi`-F;|T_Ll?Mv+-oL z{!|jPGqDceY|%K?DlDPugaPmwIVVXpeanZy6$3FnyAGAIUVvxZ;6n zJTEY36;<0nRDxCbcl*_Y$;%Tyjxrh{29bT$lFOj4Mc}TUdfPH;3zQ+#I&>teX1ekA zZ0LZAgNS=>5Ztht_owaQJc!U%3{(MUmM@m`7L{AT#sx@T36L zNtlk5+ekK5eUQ^Q2yY91y2Ucy_AysN^Cp9QKF zxz+v`_Y?bgd?VCCI@}lWYi&FB^HwJ!yAyD+z-pp^kGvsvi#I26UE?DslnXpV2AadC z1^SCGfw(u8D{$;VTUMwtGnUX_7&CW9`|X{!oE8-qA-UgCE5yeoY>;@>th{7!ao4y& zHN|zBZJ-<0>K9PG)-CP??UN)@{DGLg3Y(DK?)66LQui>-J&17&J z9drfdb(W=F4cFdkQsbCChrgPjSsM?{Ym%yIb1N9eiG>+l>dsM(&qK4&HLWky^(R)ka58QIn)xA^I$}%T|{KSheX6#~W?&%DL z-jEg>z-9Kz!&!?E#Q{D9WZYcOm28S485w)m4G$D;*p{8~9*u@&&0f|i^7)6vG_Z^) z4qoFJQNbe**VyNcE%NAM;Y3{!x>^J52wk_lTuP*PF27ItFnl*eHE-&$70R`_5*~5! zadcqM0&>o^!H*7%C-OPIe&7IKfw_)Ba;C!rpcV{WRN>lAJI7>{qs&I4>yU^qQjjlT zZwFKt#240;+{FqXv!B}mViepJfmt3jbYZp~WIRy{6fRZ(mOXxbKiyLCv1E(dqk2xt zC8hXr$h(8}quqSCnEw9u$F6| zrari5kIz<&RN`^|sF}z%xR3*axhOXLe$IUOFckL@6_4DcgdYGA;TdHq={bGYDz^CN zU=Zlcu;a|U#y{wZ(%x&aHrdeU2u&cmxC} zYTbJItPljX;^^S% zJA|_rN7sk7uJ!JylRA9snewKrq4nrYA)H_kt5119(}DiE|J2*f8tH6Rd@*H-K8M@$ zx69hWHUq;nv75zHKzEO0b#~G^jotMS92sE8G5B;nDL(7>?;dwaX>z!4_T@7c@;JnGP%m9{$0F~3ei{A+*q(7wpA#_j{5=2S#44-~ zOZteLzEZcE#O05%sUpMknAjXmLSoQpJWZvdq5Sk6zB2FM*LUQ@Ft*dRGJoHGxwyXG zx;=kqWA;m66Wu&RrM18?H10b|l(QJPjd{>XY!S!-Qh`+aM~>qk92t2PJV~wzM#bpCz2tue&=lJ%GPZXpHk7SxggC_73d5iTm6iuj9qA8}OFdM~a3=%F-IAboA9@loW&Zn&5z|OFiK-Nh2IDwSTFfn% zk=u^+*JKcle%X^%9H;gcsQdj=9V|2lJ))(AA5BT3@mII` z6(qN9wHxhz0^+u26g`nJex?x(nU`3_?SFJugfD=k+@I~4PvfPbYfRw)05Ez=3k!o< z3t{ZWQ;M-9o_y@q}1)F|imi}^m zIPgKKYOIeYwH7{i;aY-+k5WlwWNBwg2%7G6MkX@CU5inw zNi6$Ai;4* zvv3M@RcPtj&oJAk%@lEwTes9>BM-3kTf1Kwp5|2gZKQZA6z#__SVV&8r}2 zM3xST!RCta5S5n*|M0Ra`D7N)7yXIP;oBX^2mT9A8+=YBV5vKh8eN!eVOL1%4B)tB zV*zZXII1aKhQd;8I`0~>OQ+WV2k`-W`du9An3q#(xyL*f=nZ;n ze{|WT!VPLZY?N%PX`7fF>;gS7wF99a$$VFREC^RHJPXtLF^YPQ+BSyfa+{gZTv%)A zi}FD=N(++EFe}LHV=70eTO6(%Y!GPuZ^2g@=iK}WRUZSu3E1?$5==zVEK#*`Q z7-f2f90X4_I4ta?2FBm2Rk82^H;uKL4HA$0{W1o3+v!xGtg{7OQh--a6^t{CBLDk0 zM`?{G%?R9)cQGs4eV?U>El_%~F&k#5o4AbD2shq!9e%#4N;xWAWB1Ww@YgqPf4 z{8@md#(kH^Vzl^P<`gT4V<3BTPmkf{sAH@yI2`tQ9ZVVJL*iVu&|sh>1GZUZ>OX6H}JL%%1Jxb@ld zc^XF! z4S>mt*{T$4b`oPRU zy2+VS^R=RKqE_Zh9K+`CZdW;KTGVJ+9T|)l^76@6c+62F=?lCMNT;q80F)-e_*ELF~^|31vTzb1mFNb z_E>odvTE@zWh?7(#F}4nGa`r)^d7>a&IGyWCT@$g55hW32>ynfkTjV?F9h?mN60~W zC;+S-<0X#2XxW%uv;dR=x2e%%iK(CP7a~$g3xE8GIZ1?_6NyT*%rR71tX9H5eSA%g zfMoROGLViJYQ8ei!;PJBj#JaMX+ioB4|4CjB=qEa%*alOD%|2(P-EIt8KITY`NU${ zz&-JD_2q&GaSlJ0dlBd@4&)PTFcchvQh`k@vEhoV4GL8Xzz-AM`;qmYSL9(za51xK z8k@CV*b|kae$^R4A$cae*7G`BeDE<__=gv_sA~iXZ3N_BPx2nRN_f+g0sX3s-2Ts+ z@;b3kle>)sZfY;t50v?0+NgJWmBjMlSgtqeGYX2@+m4{WK+`Y#MX$$vqJ?V8WdA0ozgx4NZB@jZKxyop`GkRS7o16NcR zBi}^+(Az*G*8kh++Q;bGRww41(3qsDsce%yR3|L})0sB7RBX5nkt)8~RxpfTLm82Qyk9Qkd;x)|UM&xA~Xq)DE&;g=HJ zfqbBh$J7dk0vaKztVxCzuipXDMogmV83R$Zq|RBGprRsuWTxU@DkQ1=Yq_&tF5}_3 zZG$=p`pGVnA;|y!GcWopC6H9^YelU9SE8rcU6;wP?m6vD^n`{t!$BK7GQ+eA!@ySe zC5(mKvK5xxw*7eGC@sU%8EZmF{Ms0zvHW|`u9%+$!6pJfV{t%2mNQK5RG&LgC~eP94fjxd(Ri&ZEdi@+*r%Ch|m5HB@fD1U{dH zq+){Sti(my6{pEjBPy--A!qI`QB%+>Ru7d=t7V4nz;Xoyay2%j5ddccL^kuv(Gn+~ zwxliP&N`stZgYM>ysCs|7F})qm`ZLKf)c5O?i&D6X^`Ha0RFR_w-3s% zxCpcj2Mjq%@He;gZ|I*Dc3A5te>(A;03A89@)$!-G9K}m@fmy5kaJOX#Ah|E^caHI z(xVdkyGT}1BfCcMEkjT}mm?WK$a~vs;SJItdg-2(ty39Cx&#c^0$#(T_->Gn%5@y?j9x}2#X~GN6m06bl43o7~to=n>78P5O)g}G_-Wz1u)8G zg*XaCSk|37RA|4aTZO*pi%UYjq@tL2#5&TImyKyrBb-20`&k2(gm>xz%pkV%}E9If6 zKNfoZkD5JRPVi~yjdd0QsrHIqH;oxiv3S3V21Ik3?*R6x9@kq;a5^8Y%Lfqf+Ug<0 zAl1p`qAxdGqN|$?-73=b9FJ@BniR%HI*=L}?g?4pcp={<>CKWlyh# z70F(qSIE%`>w>B!-1n@)ix)}H=1T?@e%UgAgSi;Je(+wyYqoB~bOfFMQRfvmo=SwQ zpR^{lsNkgXm`f59on#ahieHh~I_j?yC-*PLm5^5zS;P3;E_g|HKMN30V^hV%eDekj z@<>?Q|G)y`CS6jvKMywqd>|6(vhB4FdE>L)!=}sxG8)b|E#xqfYRnip>#yW`-=XtM zv5vUw$(gP$x9}VpUoS^{QP?x!SIx;0IbCa{LuByLniTR^!Q@C%WdgP#C zrv>NrR4@_f(JqgJ4mK?5Zv6T@s&M4b`$f{bo`F&gc6YvSqmh~C*|~Vtr~~@XFa30b z!J}o9GE5~(@Wr1Q;z&(}c0AKL%H?efAp2UGl>KBu6%uKNYRh&2wNs`OT#vC+mCO7h zXz%`?&ID6%IHRveu|OPi6FYH(UaE#|bSZ4j4WA@N#c9 zB_nbc$&w0OcL9&uRyTc|Ub$4;R})^^bLnz7DuaO_SY#enC?MPni;*7m4oj+8Hz#}s zNq`z`drY#pWkFnDEE`oKZ&baH_YBxV)zimp6sm8C8IV$KE>q zZ+@8Kv4;dcytE02Gd1os%p4kvQ!$I!(y5VQ@ZX{*vj~hH5~iTAt`tV)IwyMNW5?5y zL39%b!%EG{MmIND=OE@*`UZp;wWS&df`^Ht*$&f%*`K&M|0Y<2*R`%+qeE^IRFqAI z{K~5DBf&Xzb)_#WrZg?;EDmqCwS?=nKE~0LdqIBP?n@<6jH#fRnvI`;WDO}T)ONW~ zp=VYgs%7B3MD6-}Y#)tqE%Jw{P-z}TN(j^UWuDbar!ylgy5vq?Rmp>0fHEQi^+`?S zjc7O4H6Ma82RZ74p(GT+e(f{erK2SsHbqU7AgucXofLh40@gi+0(<4u8Mi;*Zq0(i zh4oYhbHF(ta2Ot{rJ{-8EMq@oxJxAd>+Lky^X_$Dt+%3P8fqUM(d90x_UpwBGrW6# zOGKh0aDNhdPOu;_afc=hS;j@tn);aV!NbI-*Or#Wsb+~l_(3%0`qZ|!ntuQQb@*Ar zQtTi8f)_VDuqk0OO#Agnd-JS3u5bOpTqCWX>9%1UJul4^AOL}YEXo7!Bn zC3r5Y4SknY%)t}a@YRC{?1Vygg~Fl1xinMC+m2#N1lsw8OStCtWsR@ND7U#+g!DX0 z$CZYkKt%IgmB`V;hWCiNN5vTep4#n*saM(%$0oXqzCrt5V%@DK&vo;45aztZaZ5-; zeKwP#0$LKIa9ecak@Y_}8t4T?8U!eQD94Re#>CHDAnkw@mb$n*#_Q`h;{u>H$L|arZ3EmIBw*X%FBOQ?Tuz zoyHns4G66;LHP^HgL~}D6f=W3Z2AJ*qKfQ$X0-TxKk;Mc?FMAYjyTYA5jncPG!73Q zmueSWs1`^3{i#~_zeJE-?IbX}UlQ+SQ*#El9$UyI#yPTilzme=KL|Ar-6p==EvgBn zRP1K?N9zh3CU;XbSf*=mJ-Bq`9cqetfKVq@64>`5cF%dqA6QQ3)>I@JluC_JIi$2u zE!20pWqBOwEGzqj+Qf@?;T!ye`QWz$WEsv@i@uF}Ebc5C&cUqc_qJE1D5}E4CZM~I z853&J-g{IRzwhFM=Mn|R6eG+Hhh|zuCcv0zeYvaYP^T(bC7yfq(h{R1&M^!Ojf6NIGy@9Tl43!S|wOna#Opthx3W zSN_#l&53AMs75pdWuP;6mqNsHDg8Yc{Jx9)#Hzd@T+T}<6l1s@DtZ~OI(^rx( zSyNFz(jkk$~RukW9qpU4q)_+K5G<9?LSGt!NS; z%9#3;VR~p~Aor%o3OytyOANvfM%!AD!SBPy^nDm&gPT8f3l+XGhb?X_o4YCIkmHK@ z^}y~}lA>q#7Bl$2sQW}BpR`T(^3p6Q)2b^0xacXfkjiM-k<9f&nE7TeJ z-y=GqoW-p)2?$4??zpjnf(^o&PG95XFj{!bkA1Lc4HYa5({P}@V=WcQazbpVl; zS6(}VM%ZLGsiz=NE;65U$=PNSgS2q7*kA}ggMSkL?0OFQeFZA0FJ0krepR||NTd}5 zEpe~g=){ucAhL*a5k^@RFsx865(Gyk*;Ndvn{+r+?6f1wgsaxJv=)xBb z!s*N6SC9`|0Uufz3Yo=;Rj3()HTVHC{&HC21G^wi5bXilvz}BVIEslS*G}r&;_L|< zvjV~YTHyk9Sr8r4dfAB7T$17oRJXYvs!LQEVGt6GQqM(^qyph!xc{t~$-39k!HqqG zY3ZSkNbr&U!S{DfvG{>8s?m^pu7#~<)z-^X$A9nr_})wL1yp{8s#T7w<)GrDjF<{R z&_yIAr5GsCpdTiE7hpzixv@`|f*q<}zx^SC^+K*cfz7M6e(M#@E^$lgHK+g%2&?*Y zAzL~QFRp-6DpYXo*SBc5e-f^&31}4Es|axU+Wg6^N4>uyZP)w*k z4k6MqEb41wF{%zxs8Ck4#9qhe{GXhgXi%0JHIq=0@;*LyF zP~uB#-Q2!d_k#@b-TrN9oXP_!br@Ke0{mF#rRLKvGUIPMfIGZgU5B>!C@#YLKnAvf z1{6*Ndcam0Kh3@dsZywJ)Z(zst^e60QiKxp!Pyh2V&I=hBmHk)!VE=3+QkY@aE?DS zdm6j^`@UnIew?Y(eq! z44yLeAizM+gBBJ{vwSm zu<>7x7kV6Em`m4unwDRnK-kW=T}yb`#&QDG8#Ji}XaJ2smfNzU>BzRatEW?yiVmCwuO7BcZzMn}9 z+om{{l=ZSvONCh7V248tULyqJ#MxE6TUzDpYSMn@sYgQjgLF-Tv>O!kNUO--DJi=GCMO|S70(@#yXEaG+l7Bjtv9A zs0x~PI(vu^a?Bf8=?*1D>O4k&(S8JtQb3ZGTp+j%2L{j0{7Bv7{jo7!x{d%~(t?d0^j_6waj<9op&JmY4JM7F!owL24QP=am+pXaWH3*J5!`SFg zdfVG*p4s61PIGdwj0K@g8g`pmapZ@90RFY~^j&-k18s_~$p7ytXK!ut&nP7wmyQ>r zVaw5a6z9zB-_@1nanAN#p-MygJQ#Cu6#P|MGHG~mcil#ZT)q53;5AJgd!n!GInUqj;x0ckjpYG5M#MB zaI9Yl69{nsllcoEck;_=S5}a4pPNf|5$3wOJO_`6fR@Q6Y|*4xQz;3k6#a3TH#Em4 z53N)!i*BSq1t`*Hr;Qwz`2OQq9MRl@S=r%$6L0SywM0cBeQ4{+x~s(1^JKP?L*f`u1=_1Lw}~G&{`md?km9IMcozcO;O~uil(bbQzs{;p^Kz+x!LkQP$_Gf#+ zLkOF*tdh%ywzIH2Y=#WWYJ>A4jIP+9yxny_y(1lIAKeB*Ksdi{ zeeA}D%mBbX+#DhpbZ9lre#qSsZax+mWqrq!bc}$!<6-m7gtml;BJBQd%KLh9||ouV-lG(h7nHZq!h^nAhmw%`>^ z1z4{Lv{CMDI*>p2o@TBFT6)zT3&L(%SDFZs;&pkZSoS$G@R2W?3jU8C;IgsIW7X|- zkO)lOM4_PqUK5X+fr|#&52a8{gCKm5ZHwqjeRocxne8T1Y$(dIiCGwcJKv>iF@(9D zbZ*k6?BQ(U5(GR#;ovtM0RSOloXLN5IRmc3B(h58-aY$Kt_QMMi{3FO>)H0~Pi7zk zX|HfzAB3=LgXBjbri1OfqaWk+#3?5WgTL~Pi&m9@{o)$TEMS72geQ&uxD1;RS7WIB z`d@9HfL!-H>=QYoDbwSTou2!JKOS!9mA|nl3gFjaaf{+9*gF-ER~wNkdg4~&6!aOM zF(|}?FsH5feOEeTs!S_to^xAa06@7s@X=CGZMt+i-Mg~nQFdKf$ja%|r>vvqigtIo zP7eSmF5}kFp`J2zeS7>%Q2}~jT!X07aGi9;AH26Ak&rA7k_eTHsX|NOp_T4t{zeqZ zV7GONH*C8i5TKw+%E*VRWY6k<8Qzty^ogZ1Nv==JZty`kYNdUeEMpF3dSuO99hcx3 zIW+L#Oiw&;4B!B(pvi3F+{~!-FS=TZL_js)8$dw;HUV}n&_h0y5j)VXi0l?t!+KkQ z`T$oCw`q-sT3R#u)^yBO39E!Zent~(#R5*- zk)uQsY3&mt&9;ZjLUT|~o4;WXy(bbFiEg2L=@#R%!TDWBRq{TNrcfR@c`jEG{6+ci zY8wb;_OMHs+!RI>n{&kI38^?ZRzj2scwRU?6}6y#?i&ikYD>7(*y&D1cC)?d*<5b3Nx0B;V5Ar;CrK&@%KCqd3H26FhocJK#!wX&+9l+wBnD{t?S>i z%jCvzmz*~3 zq$ov{6cq5j*$ah!_}Y1yJu0Y(e8Q9x-dM;f(OaV*`JROg=K{b-_?9YOVTfK9qC=sVdH#6f{x zn5{Q~h$Gl|W|5X<`!9pWBSRqyQ%!5lPxcOMx5yu-6^~kVMR!&*FK8d8WK9km7+ucZ zAbPmh#d8YMQ9B>Hy?M`&qn~78V!>0NIi{(W27(cs7U5DWyd+Sw-xVNcFCGU0^}D=_ zI%A8Rm~FxnHoFPOB_$}z@+5$qnTc_0cnr`+J4SBEej>LI18+{w{T5uIvq2Yller4d z*2;zpU*iZET2`IUG`76M8aiNkWI&Ua{DD`7F$}(NP^KYBaWBp!8?^u0 zD2e&{>JpANJAn&3LU;@w%k(7_c6T_yu}EX&JaPMJ`F=%Zg}^)j%`Obi*#mGIriots z#fC!!11YH^WL~w1mk-@$-E8c6Q$20Nfqy6_V2aKql7z{K^7f3C!@jYN1p0E-;V_q&>C* z@b1Vc0ue1;?3~ccFBi+n!kBtV`|1+Cs#HY%YpLt_xg(_qhc)>{`?%!amduSmxt@hI z?Y~B62S_n9V~>O*a8*hv96N8t$J+B$_~6ATLMpN2(whTf40*_s;acLWHSFK4vbQyZ z%9sJU*bp-yvaQ;{({He`r;tOzYHn!?`UU%&|9h0_{?ae5E34`n$@+I0l&ZxZ`X)|a z`2m4({h&h0irjYsMb*Qe&XOQlcxQ4Yc*<-`XAbSmw=;LjP&Mc{ep!Roo!~Njt_ri% z<_~qVY!ZpjYwA1oD6jAb!R1Sy!g1U{vWVr?6FF|aZqny-$l~)CMcZ8OeF=&h?hYoHLwC zK?kc=|3m^JDL4sWv>cLswklo>n7l2j?&jC8h`|z>*w?v}gI zRf~`P4r?DibwxO|Mdx_RAv3|JretYwt)x`MyF8Ire=i*5wQR~S@1W7lU5!e-i|@K&S#pn;@T|(>p9MdJT*diDWWP z`v5>oQm&7*sQ!j-<-G^M@Dj&h>X;q&q=|_^Mu*`YKJQ<(;!wZ7=CSVQ3l;+t)Wb0KI|1*l zUURKk(|Z;syLK$$5`MJZnC8J;ph*HrCOGv0Qx|3zK*fI`H{HXVFO6^AhpAgthqS*5z-_u#~sfO64)#lolJ|v;)xvnAZpQSS;eW&J>T}){zcI#?kHc$ zj$L~>vT*u~tj;+(TZp)POe<5hr~G>FD8P1n#Wq@Q21xFB53!+(3E>nAxoMVKp=viY zZNZ8LFKM_|sfs)Zam2cG{>B!Z?5>@wmG^ zcgS`nGhl=7JItIX>qOxEYVHu0z3yujuU>d^uHxn~P1Vl1`u5J)QucqN+F-`mD=` z=P!NfvQ$OJ3y;hT5L^4`BXrI}ROy?uPMVEj`^Urn83`p&@-sb5;c>piFE!C|6?0{u zs^%O+EIYE7k(0e@rP7o7dDhQ%1ra1ykUR=JMz}gqysnpIYj;Z{SI60go$FzIuaE(j z84~S=zGFjXtd&e0g)Y=O(cY1UTgy|7j-ovy>TGf<&Ychdo&v^vQ6?rP9PLOk8cUXW+#AEu}Mjky(yDj{6WXq98uoB|H_U@6)?ok znYs=(pv(Y?;3WUR+3%eq9=E@31&e^nj`+c|ds@`*)j|jc`1~m&-MmIUCus7 z;PE8jPz44k`vCPGC7(KHGE-057xz98_OC(xZV4Tx{8c+yko~J3`r)t5jw<5pG{W(C z{%?id429#GrWZ~(`?yhjwt8l;b&z3ETV5*$RplW)^gN!af5i}oyl0H{lEio4mqDv1R&L)beJft8EKAc#0)st~5!u~~0F*f`NR0NArs?QAB-#ra%q zDj&{kThq$r`O6kdvZYzP>68<@j9P-qxCI?jp(%^p*2cMQh{gxbBV;wg9w6Xc)3dYG zB9<7brHe5Eqp5VI)snEi<1D!v1nCgKr3Vg&3Rzcok~B@G*yqHG(Kc|UmK%&Z(8U+q zEt-p2kMx#}Q`|$?BG%lhz`NYh@(3&S5sHkS1E_cCy6j!Jg z7;6YyCcqa(q&G5Ffw%a|Ndh06ykCMw|334)Cw%@wu_A$(3>Tn%R+L~glRmWe5_rL!y=u;vrvs}gN&@JzWfMB5Q*47em8ljy zcGcj|sKNNDydIldprJ%{UpaUWAuHM#X#`rys2z+jRrOyG2_2C%x4T7^)};i7hNyUF4~q9y2J;rt)h((6aTLnT*ec9D$fRqR?bdwr5V()pL% z1kp2{0mrht)DmgdZ;cr;6f>m-GhjMl;CbQ@j4UpPXKmLrE|s~jUKG&WTAciONA6?3 zBa3jP9*F+WR1-H|fBj?8V$9b%6p5%t2u*%60OaY#F2F-P@MwR#!-1Z>kW$=8BtI5z zaKkw0Milo2i`%epBO-Kb&#sHY%ZMbh?lUFs$`K^nRk^3f0GD} zjwD+4BJ5^orTwzC;nB(Dh6BX{Pr-@vlCV5!&n&q-nnw+}&KvA@wRhz@QrsR^v(Z@D zF>k{5dZv6t$dM#Djj>k{mzK70&s?Vd=l{;?nYw^M}tIc>R^YeAC2mK1IxrU)zVw9r7AthZB_=7hX8m54lb zLn6NF?BGuxwV?`6cCMUFS{@heoMxG|p4`T=#Cb!tJZJ_v(8@d=0u&(SidkctKG@tA zu6y=pj9B=P`$!WiPxq%jCrN$UF5q>q-Co1voOL2lVE;=u8ZRoiE8(+_1fFb}+nxgzUV*d~1qe`<}#QJNtC z<7t@STZBtX#10A~jeOLm=z&{w`Nbe(Z`F*`_N^mu>Dbu zAI!@;ZM&~ZWB)KZRtO-`bZn)V*d5EGqZM~}mPWT`G;v~nn4ye4+$z|S6=BT7+(xj` zVKY32M+=wVzuzBt6#L9~1LBcaJFyoynpHMA2aG`_=B{E z6aWvgKE&tf0KT{#@HRL^izq!8{Bq5zT=;NU)5{3ZDrW#jm^IxuwQNq5YqohrT#(13 z*@~@Y?JVr(!Z)!NhVN*USeSmfhn~GDZjAplUfGHx(|<*)LJLG*W^M`Sd46kewUS%8w^NZL7m3Jb#RD|qlp{9-8 z(a}2!1I&QYJY51A zIg3!2T2MSVS4AHMswp=0Zr>^mhA)LjGd|=k-QH#u{@a;3@z)nS`&q&9O?HL=ykAc; zo9WwvOAh3q(6d49Jm8s(y9>TupS4fdl{i)MY#^j;#X@JCzorv$F*L4=BqDHdba|Wv zR>PBoFDz$ixM{56_~IO>ID63!?60h7@l`SOU&j=xWK@q;9eTd>FUR|L%@^1Zj)TAj z$P@TWA+~vX6!B514~@=v0^__o;7gFc8HrfAjH6N!f*5an}F zro?@Y0pR6^!Z~fS{wz9fJ8_RCxd3pS;+D2moEzF_IN<*kA97+R21yn701)4&%+Z8Y zK1rX(jM~>R0H>9Ne+((2@?Yz<4d9oTgQX`QZAbk?6*6>MDAP!Ga5_s-4?1B{QG-wq@{u2u_vbI&HF! zN>(sVD-!zcPsOL`{I(nf%5qc@q!(I|D*%fFbEaE{tyoq50f$0iB$y~GF&2WJID4F} zBi;f2bo=(3WOE#R0CuXtuhA0G{Bgum?rV1_X|b^XiQ`D2K8*J+B32i}Bd%iP%(9ue zl+rHh_do|Uf01vyl^FB-6%ocpA#gPA%EpZ)7c&5bpIC!!ofGVZrWj4UuK}sR9QURx? z9vng$(1aV58sS2es-1I!*Zwf>rhhGF=-t8(_WfCA-9;Fm35q0k47FqGzniWoTHqEMZds_!D+>*Q}0#}u; z(nH;%jSNAEu`JG=YBS4Q#Yb|#OGj$eW#Ivv$bLoz{~)h;C>DgzT3^TI`c<8!TWVa7J*EdtaA*E1^t3cK;hNUBZHfJgq~i4 z7MY#)^kRwBXkZUTVu3yDrWJ+vbsQlq z?@qi>W~4BV631dYhK$XB*WSlW|5yj&ooitrBvACrd!z!r4A7;1Z4uT}syh|WQoa@r zId#EYgm+_;#h~hz2&)rujRs7y>n3Y>nI>a}3Z*^5bE|V}w zo*-&rT+eEH_tc3R_Lh+&%UeOA9F&VvF)lW#YPA%3&4nrZK!LurR;@V> zI?Fku7!C|Z=yl|CbS(x|qGZ}dQmMLqz%L8+`7ukrI2V+#d|P_jV`L>jj!!1hG+@g( zLkM*Ke<56VOI^9Y?7+zm_?`IuTj#rJ*(5?n_BE^zh7y6-Zrz&tdt7sf#Vse(P(>w< z8D0QNYoch*a)BwjD`+`$0q&9H@by)(Ci@wf<7SOO2u|0A7{G8sjv--5FIjwS#bm&8 zeF>PgF|U1%ETFsRtph>vTD4TQQhxS*B+*!+sgoGitlqQiN~k${6Cd~xVmDmzJaVNG|U@Wc9HbV{naT%8Um1lM{PmT31*q$ z8shKf{$SbpVWks>_Qfe>p0j z3Si+Z^}n3~Y&5X_nuqFM&e_#|sozE{hyoJY`jS4bZTt>11z`y<$T~>J=+-YOQ|Fz0 zS7-Ib+Cbbu*iQZNJ(@OZ6u#jm8(91iXd3mVlIL{K-v2JU4SszLQ5UaTuoxL;FU9@P z2(O!r7r0}04$tq!6$uVejm*+-HCkrx+|1e|hm9;2z`;T!S8hXwY24%UZz8iY5mZiV zSl`@wwWJfN8Ub5&2f#o9L$7UjM0XACD@7z=3sN3y4Pyq1Nbi7MM%8vOfk*|UC zTotC=UyOd!yE+Hwgfa_Uc0022D?e|gXP__;+jx!h{eep$z7hMjA-@ZtWcD^DD!+6^ z*FDTn8lsnTA6|F%RFSjl)HGPT=(IcEC%Vhs#vTpP5F({WTr9iW%9^pc@jMlQb}%>x z7s?6^Z8j0n?0`OQ6J;vn0_|@i4+BI!xE+Oirz#7j%Sydj`Av=Y6|_F(-8zyFB4@-e z3dktXf1?#UNuN~Y$!ZHZDW|H$q*|9y(hS(AZlM!>k@XElz+$=0~j8Q z8(fSDzlHy18tr8t5%JP?6sA8D23iTJ!cyVAGfSf>{_eON1)VZd`AK^&n3_9)oB!xW zUBL$SGKF|dt&Vc&r`8997Y_I4Nr3o-KH1N#zs{3Z6kj@&#VfdRrd3*u`4y8;=4o|i z1Fs0hiS9}Fq=ZSHwsPZUu`1Kq|7T@-E-`eiXeHku78Yg`iH3+g=D9Z@WI%sqP}!`o!(G5Ne-E|hV+6~741b75 zVt+%2_j>15LO=)sj<gG^XXmPJJc88$6reENBQz;pAeiVYSn})kz?Zg zs&D^)A?U3<7&Yj9@%^aD7U7j>cHl6O%uzayK`9vN3YlPV3eg^%22KkH$~>_t3sG1(3E=}{!-hsuh7GUUnYbN3=|SzuJLURBwO5!6W1-G)04l z>xslaF}9~M<33){FqHYZ2T!&h^}%h1VxYKaIYc#HYqu@yyZ+<8_-hl0Tkt`Jk$D17 zmPoci#~$6~4(mN~ifp+3!ZZ<*x4Z?XM$%@Evk-mwJih~mKtsZ$b4|dpNl3=Bm;xj=`h5A^)G@{eikMI&#LaBMhdqgp*~%Lz}zrBFKo9;!Ep$s$ZbAq zOkjgCb8b}L=CI0Xb_s<#?M6qN>6y$DK;`rCrt!w`*eyhR+V%4=rnPWH#l~rk1t}SrM!T$ z_s~A18jWs;X5mZfFthHI*<@qI#0iAQ!UfH_m9yVo$y7!gq~ykerb*I;Gph6-0QOna zj2zJamdHR(MV9Pxxj_nRewkgS2Y%j9es0%ZgU=ADSc**dmHbwN$$wDs)c-cA* zLxI;=_XBZm6@RFhEtG;USrJz=#qwqHzHsCjqnN7uRvo!LG>vCW7=qQqYR#Mb7_NnG*h`=dbi-eCy_sSq@onb)Tow2 zRu$LBKzDk5ok^`RHp0q*7&l#YYa0tR21eMT4x>Rr6ssb0%!m%@1I96^cIaSbI$Mw8 zu0XSu*maH1F<0CI+s7n+ecA%~Qf9=IOb)M7#cd6(iRLsw2~dCNFz*9NJP`U}6%3r8 zs7#u`wT8;g5612k%$i{mqPXLW0mYG#!DC8L6S|mO8xiWTD**{|-a1ERe00UG zHPHeUYH2cDPmd|$)reeC~A!tt){VWd@)sWXB<7j(T%E#G-C0*9)u5oeE@#vtM^H> zxng@6UHgtZ`*$2D5cdTNX-_*j2q0`wO9d8XkA0>)FAqPV_#oE^13mK{4FrzHMwc2J zp5s(9m-!&x9cVG%{H}ybFPYo#W$!8K@_E4f(yu^@ZnAr7!A~Av_yAhqoaa?1{FrX! z@VdYpc{WiXtTtK4b9z%=D=^u-7qQ}+`P<*oIk(E6l{I(K7FanDpgIbyTc0##Z9O>3 zQIvWHGj@9Ex|dGAq=FJZB?~%!kbTZ%_?{?cJTC5GJDD4Iaj1V6b5!g?r)J<5ektMl}L95evSi_@}tn9F^vo6tV$esfG-KwLYXZg;~@LG zEO1LuC*5;PnaNM@uIhzpJe|M-b@ds}Bp4Z8OT9W&1(nN0ldVD~^YRO(*i<70NmCcy zq=|eqkhJJ$YV~RS?LH<3zjC|HVLpeFr{u&GjMolHFzIs>g4jwrQ8r~VFYg6+$6?{~ zmSQ;I;It{}oQ@PouB3IV3llJcDajD0E(H8b%y&|pJTjrzjAl$%}s(gMK_lYKjf`4z6-D^3K?k@-hRtS;T~ zi=Ui;r3e|o0~|}=#{^@v=2caGaVC(Rh>YogzIa)dT{_~B zQmLB(@n#4i{VXd=#=fg(I0t@qAG=Gf0&`36E?jrH%kR8^kt2F$r zd0xBZ+DN1cj>F#|GxmiO-fgS@oXY?0<}1r%w!&{opzJ|%JF$Do_LV6q9vyO$92#fH zPYssjpUQKp2+~eSLoe9eK zyZIf=8(Nr4$Y)qjTesjq3h|yAKE&BCJxkCV&rIl|pu$4#I<@atjY{rmkxT|4`gYUz z1g%MqI#Qd2NN(LM)Lc8FJ3k6Yc05HL4UB1@rNlg&GM@vN@bD-=@XJri9czUszg{b6 zF=C_twl_~~IlIy=tp4n0pKJNaVKJ8SfYP@iWA>OGNX1N?)`f(S`rA z*@gXGy6%XA%(D99kN*9)?WSn@s+fBO#qKu^x8Qc$M;gK!+VC}+b-tLAyw(J43^$%0 zdAe?QY*q+B&>`~F&xRj!gWgt)zQ1Qx zU6FTBQr~dSw?0Uqs!h#tF+zGcBkx3Bk>QT=0f_zLrL>V{bpM&sxKBq|sDbuyAQNKF z3fSae!dSQ;462T=!YR+@yvc6D%D!k4QzVXt^Dl1fFJ(i;Wm+C}T8vThQ(v(b0FiJ7|^$pq?}Oexspd z-ZA&0HK6b{f<=3s98)0$w8(ZXuxqhu8Q7R@(;s6$e6EbU)oFITL?=_Z0P9=6NhbR%Oa6>3SOi1!WC-@3`l(*( z*P$B7+a*7gpqb4AfyA6K%LbKUMc-@|I}W8sn$p4jBx1puNA8^-G;ks-d??xebRFi@ zOk0?OJl8&fp8K%S&`LQBQk>0O?4Z0^|7>@U z{np1Ssc;LoGHX!OvS@knzh*!${GR>JIikf#)+M)cw|QI&CWd3ODmLof;s3{rgat@3 zA6dxEz#sE#+eR9ppdY}LZH35mnL%N&`oLy*SipP(xtSQrx_~z%4dx_l*|n4+x!Fwa z2OTK)*}rjv%x|N%R+5v}Jmu`JI5%qITon=JmDEdMjIMJba0pzG-$+!eRg5U1b@+~I z7CwBNwK1H_ULcCa*i6#W#0_1wj+C<_0}Q`6*8kag{Su9=*gVW=rK24kysKM#hlY$E zcf^9dua_s(XYpM=;v=!==hJ1?)2HKk2a9k0mNcG=L+CY zXM7)8bGKX`%TGsl6`oI02H^65Poy^zW?l-PA#fI64mRT5Qxm`1Mi)BFDuTk{qFaTl zZ%PB~C=Y&~xgirJfiETZFNPV5}(5EK&NGcqo+N3njkShG1IsQz{m4At zyKU`9AknHsm7h7SDiE(ZC*7@WXs93?M*&(d%5I^^9G2SZ3|zQ#nFtE2?eP?-u}FiY9sbe)zZP~y&z;GjgfbiC(2_5kNJnaBd5B zv{jB zSs!~fhZ{95b}W1?JB)=wG0GFBgq{r-%`pV!S1Lofci~w?dHp6j6ps z>drZV?1W5T6wi-hNxjDc*i|uDghGe3(K;xv^|JieBs6>UADV*O?mjVP3VQVr!c3UT+G9vr(=yzWo7SitK%7`exDn{$K<8l)oXIft~V|v z(l5G>yGH5@oeUZQ^ihKS4Zk)|gE)CUZOa8?@e)OL;lPSoGoG`uWH{EdmR@gaOjar# zWJa@$?d`(BZu#c8>P#?7PkF{XNzt2J)${eeh%grf-F4nQSDed((&R8$0Jh6)KTFSofVtCW zaUAAUQ?Rz;_-yLrdJo+N91FdEYX-yDQcLaCC(``wD}Srrks;{sPy`ZWqOCPjW8Dqk z+vyj(D0j{v|GLcqS4DG2&JU0QPTjaCy?xF_sPN$+iEFD}ZX8ymhIV#-em%XsWq+0# zA&NID?(^8Ye>Ql7_;j=;7JW@G4sVYE_X7YVXWtv%9B0V=7+*Tb7A>7ey$p;g7JgygAj0L;j#pL@VDc;V zHMP{(_JLTIb1d8mNsmnKDYr^uSiPAIY}EPB#FhuYwTj$G{35|I`U>)`Y>!saSqIY7J92NzON%S-1Gf@hW>TRLYbCI7t~120j_pqkfPN5Z3Y$T|WhU47K!Q z{1}EX@4*vN_NA?6(P569dI;s5*nnS49oc^bwgCbGaIJIWz3PmE{gr;yy`rW%NF9<*bN+1Nh@!+b}K#=tPP_2rFD`1j)s=phw^ z>OOw)seIof9@D$n_HH4+;=ge>go-jDY99l=7zh)E?X3R%zfP96huI3gx8;zEsnx}< zg7y(0mCKDhH76gmgPO^W4IWPl4{zeH0=gBJ)F|e^ok4BwsZ(&vyxR)u%75_Y`J$L= z`wVq@kak@;4ordrCEtYvtTX4MYr3Uh+dr{9Oww$Z4(TS$w6idd$G8K)cP`DpP+MYK zX$Z`W<=E8?IbG}8gn4~cQI2o0iY@9PPn$42gQ$yzUt!c+c^*g2pOGFI5%N7nEM_kF z`jEa@O)=F%>VbiAGYiv@{S{`jPAuR7 zz=2I;KxAPI_2V>#*E&m-js@Yj8SgnIFD{y31*f?z4`wN=QD-=wcj<7f9Hp>L&UfcN z-!&`PpC;r-2Pq~gXpjp51nYiW=V*%mFt>x9ML+yx=YwGs#)PUWkPL~XO6H{JB5+uu zA=d7!JWwdE_CYnOf;~HuZS|Cz4 z*E>Wp=p-%(8&G!D%cpEfe5-;#E-r+I#F*wdC!hgjV@fXH|3H>@v9N)7#6o4CrR+o45e4{e zH>R}?mU)u?t8gaMs&HKm2Z+U{VsY35A>Lu96n#T zD%X^XXS%u@5ZD8x@{y)QTwg_AptNtN?rHKt)n?*uLYGC+E)a#gQfsTUq<@OzxyeGHm*>vFpzMH3u3ps>7}4-KZP;@Mq#( z8gIaCa;d&zkv*@4q9y(f8dTesF6D{Rix{v3U>@nmjT1FCUgmj7T~>Ax9haN3PhFY} z3EMrYp@aYQpr0$}*2eV?FZ%f*JMsrVRzT2+L~?5zH{sU3Q!UbAVeR$Q4GWiSSc#?6 zMho@o*D2M{5h&MZc9%P>$IV- zF#pZMBeZ=}UX0YhcaRPdM`zmVmiZoRETQ~$iAy2SejCrdlPa}ZIh3Tf?#`5U0K?hk zv`Vyx4D`h2TG|p_Hac8;X!I!`*bJ`JD>lz70)cY#z+aY!H}Aj;&h?VCnEZ;nXM#WY zu~cJbA4V-5xtB4shzw0jXKO;qr)%&`xE%kG1|s%HiWcwA%0fNGG3f14cy&k|NY6A*vUB<-mcC= zh$>?nYwF~8O{pDkVeY~~cGclR!a!TDWJ`EBu<$5{Rs7vJ`ETOiQX z+0yRl1kD9rp^y z-#u?xdm0fuR_rVl>1N#kaUfzM6>UwM!KhU&n`zP^HJ`WmgGiB$cs#+^^Sun^T@prY z8B;Wo!JGaT3#e$lWE3Y$DMno5CC-G=G8+^z=@cZQsCV%y?@4WTPphOnjoVBBso1Tp zk-mUkmJU_K#s|vkWl|3Xf1Y~((9xzw^vPum6#Cc8mF98BM#r*357iRwNXfLW5b?={ z!alFX6-vhs?$ilD_*aS_^6R@>+&1N5doapzFD-asA67wfw0@*>(vRy0_c8kQ=}+S8 zB@iY0IwWY;;hjpt{US6LH==(xF|^QPQ5&Nx`A{L-6IqF%4553;!*MDoznExJpecBt z%K5UTD(c16mu{6}U_UHDzWczm1pY7X*Z0`yimDv$3oD9i^;Qqq$(t2UIlvc4F&d@U zhidYKSruCm|FHP2nDLC|8>y7M-3YM)Z`y3C3jG-UYAn^7*_h#5N=Lh*f{m6WUTQWmn$tT(=sx>v znGs)7-D8{|->or^Obj1u_81(cKv|(HAAKH!;I6T^2$SLSC#)5S*uqfP-XHQT7DNq& z6)=rqXGDtud3D--B#z^ZM4(-?BK;2xQZ(0`Q+fy7B(>~~2Hx&Th(JnT9akLwhZJOo zlE7>I@5t|wc0Ec#E)OIE;=uaQsUPWfU2y203c%C1Mii9^F5q=O;FcREN1~M#XjF#B zvAB}Mtcw${>wzH^A88{US-J21m9iMVBa)x}bo#&9D+D~p&yz%T6~pl98h8GiUK&b8 z1`nhM+Vi#n)-y#X0p`>h5yP7p3LTgAtsF*<4)8|I*uc=0imu}$ zojrKGaLWuo8cYL-Ar{!xWSvw9@EeTgkxmC*XkJ@osCV<64Qczr*0|5)Wqtm-M;o9{ zodAHS6!XMo!(3}~BZ4^d`sM*;rQ*oIy=WnkmRwjn|23^z+;#>0+QNgdgLFpe&zV86X04omp5H$cZm-C9Wq25zC zYzWB-Vjhi?h4!cHei?M9N5;OctZx;&aXQH!$fQZ{|6sRq%CZRsB#1L6AZz_stzBVs zO#vhjK9FEg^9Z=f$me?+^OLY6umjy)BG%piGzLkqw+J1js*sCkiZW~tYMa=;;o z4s-s}3xG#CO2>?HtuaGD-lVPxO$Isx0Lb0@P1fuCMo@HjdFj58HUy&K&6X8yQ9rC= zQh%e$GFlb!gm4lAx;0N4xV@fh7;VV>rC-u?H=mpT#r>O65r$UG0*@)|Rr(n5l-cZN zy7cgCzk&H?OUpMwv1~eyKtQjp@hyd*58_~a-&i&fX$9GvxHY=>WRuOp;B%rYPSameUuk=NkRzr&H8BIg%nOfCCncF}+>%D|E?$Vh#1PWx}&Gk!i!8Nbw z`3oNw?c8se@$(D%2ePL8E6=>5zj$$H+CgGdH0)@T3# z004!>3J7m)gCjFWq4lpD*KE)Y|3~>|M`*bs&Lj;x%OC*dp)`!VPRJOnl%3HC#1;fl zym6<1HWGU`?(Z>FqA|lpK_^fs8-(?8D|xm+1WMmn5ci&;qT9mGKSsL$Y4o%l&ikhK zjf%TLIW(Wz3)H{URgDG+1 zG%iD7hc}-L+;BVqI&ZZ$3GNTF;g(o;W&(6x-H;`j{JwO&`>}@8 zpPNm(y?_`$b!Rb81c5Zs6GgI`A--Z$u(_l;I4Eu8@AV_4mnGd`?rhxHM_&LJKs+@! z(;+uh8X@WUdNf3QOa`ADt9q+@kPJU=m2MkuV>E-8rEovlOGSX*T^YarqWz(W2}W zk9piQGd6ZFQtRtxJbgdld1v#?0P1Qq&|!wkUpJTMK=*KqV+wcX`O<)=>;C~p0Lif; z)Dt@wI*Ik<@PaDr2Oaf^3zJw^Mz%_C4@g9yW`mDr-Ol;{5kq#!oEC-@F8hBH@~VCB zSgw&iLB3O(DpuV5+rH? zQZt=An5HWoWK>IL!+7I7(C)!K(37=gX*x2r+;tT-yM4HhC~s=9DU}j7I7I9?`G8Fp}F$@BS|uJps|ElscGPoi%~KHur}ddd=h}g$1zkC#qSmScZO<{;r3Zh&lEkaae_4rb*p$&A4PwWzgdazxE=Q%^WNkOIH+n|s(WEI3$3|4I?NzDCt zbPo{&Gg<2W!2RlV8B-tcyQT_T_s9V#2XFueCc*Du(9;-+aY}!rkPpuAvdbY(3yY*rs#sXs}`I2gPnB4*=dFV;8#urlTu}5fGI^@Jb!JQ83RnXF3*Sv;4ogqDtfKI0|V`j(6#580TC4 z$W*3ZN_suZ+9(4MRjt3a-eb7mSQ=543czP!prhzb%~B1$=XQWQJC)Ir7g6J9IDe1Q zoK`cX`7a{yZ1`heQIK`VxuS4+Po=B4$1W_9kWa&~R<@=D*dIh(?|@V-IGz^u=TUe! zVrm)8=&(0nk56^TzLd?xi$$4+E(-VFy|bx;j<0K+A>w=x^UMVyLnM{1A^I`>v8g;d z7TVuj*fD7==YhzBLpbx$<>Zagl~;O3=UI_~fW}m-{Z913t_2ghl)Fp@Vub*8sDeG) z;{>U#Ci_FIlE1Zz3fZkQAB>e(1i*2J_{kfhZfpKzDvYdbM_B9JDUDP1-jEsVF0c78 z6#?El!XK$sQ=i759S8Uo8m+EONoaY~!aSgG*~V0`)RB#J8y^Xe)y)S_mu!zP{h5+t zOHcp+08a#*t>_m%=BwC71WanO=Qqt;YuEg$v$8Ly+M?2J&xL*%r`C5Fi>fGrKG)x0 zwqyVE!%yO)Rtiw)wTxhnz>BG6vU`#ESU)Ntew072~d1>TSR?n0+Uw|3cI zS8NP}F$9Z!9{-p~()w=p8T~J2CtfkPbVdv?bPXDt{aUO1+vWn>c@<9VsIUCg5fC+kmXG zQIun?Q6xU5{5X22fWnQqwh$jqRr!%@xPGRm?hb*Tp^G7s_HuUOG0qqB64WJYIdoJ> zk)`3VMy4!tZ)(=|@UDMaZE3bFqRlCCY$W_26diO^i!~p_O*zATPcWJBD()#mb_f9A zm8+}!QA;liGm$GarD8_-gAq&C!(%8Ed6%o14)1in?nMbh06#j$ilxn1tdgb?AwYLC zB4b?-Y`+by3|!njuR`^VMSLZ`NW#77u0EC~28@f9UqlYcKIEvsZpI8=WI0-E=g2v^ zZ&O6}ZSJ?#FJA$JlV{KhecJ6M-v#m`GwN;4eOBOY?_c@CDD53KUj`(fvk4A}bZ9>L zYX?nbpL6IKsGspV#|F}QO02;}o>MJqE(r=D3;2%LRLf`q{1L`2)-j9}cx&(`+Dc8v zfBZw?RgqZI$H`#OL*M%+3?DJAR~p85V9Y~sx9`L8sOH$0n(u&)AS z#$>p5ytvvKzqB%yvB9Cy?Qe}BK+%VeR0Vd}4p|v)1#z=b=HSSldi7`e!htpUXLr#? ztaX*_Ospr?CpC?7)fS|McI*kLrK!Z7G`yD)){WRP>!-=`s0f0<$vY2=8 zz^Liup^;@jmq}is3l&2DFRGrT z<14!VEeuRwOi$_KEwCFHA`vQJK91V{rskeEu7=dgZ4Yp27gamH?Tz0#8ym`+EKJzU zlPacUyEg?7$!wf{zTa&0ITLur zYfRj%Z1>^W*?;?!36T-HP&wGIdQvf$1qLx}b708Vog+eAwt0{t(P|MfBy&;_E>baR z_UA}jYp9jXuaUT6s5Mp!a3&Z-QH}u5A9EODsFjFb?m}thALewbxXI09L;w4_AU7*t zJO=;bh|M7O)SQkq%vIb8lwSTRkl02}Gh6OGVQCvtWBb1^I;F)3D-n3&ZZ0wXi8L=Y zj@2K||C*IVptxyZcEmV_d+!Z15JPBu8g8#)w|f1RR(7MwwE%;tMuk{8hpvo^ z!1wtk=TipO_CG{))dL&L@0|ETe>9{%rcl**< z?uZ4>hBgG+5<2U_2KlT@!<>za%~_eAXC#-TnOPlrn&>k}Dg!0!`j7|-7!b7O5C+C% zClV)}agc$~1KKRfKgFP6Twx63Y;{u{drXK3xa4E>vz#ho!Y#LYG%P2ozSn{;aSQe| zSwuBN?6s4)Nd^Po z?S@vom!ZJLCY>rl+38bi0ejir(Q0yoZ7@#alK4P`Hv*QJ%tvtv{l`3mgY&!E{({Dw`M8@+ zLq6K;SNlIaJii&3FRjCGwR?BQC|U~jv-q8Xwl0XlXud_@-g`g=qLM3J7ZQvY=d58p zr-QkOt`u(=;%rLHXV$KokjGOj=7J6yu?8g=( zX8vl|mvq>di{k|n*}eFy8q0X@{X@WB{=^`zF|l{0q(VAoNnpST<2*_Tv7K$A(7Mds z*0FR8yMo2b7y_a7tml{}uHC=$c2!)+NR^%;7pIO`!u0ZfZ#q>r>#bCYr_g`90v*?? z!iY;9?6P4)j98U%$#y%PhMp;rd*kv~2rLe+p2=q^u~spgDsc4ya)lkBZ7&diN(K@Z zb{DE;7Vvh?!XeRUwOsw0ogyjmK2^h|#n+<@{X(_#q*6dr-$QK10005NGn8~RRF|!~ zsz*e^G=U4*KXw1%_)!Cfq_!5p;N`2O$^1701_Ek<<-v#|evl3?G){!bki)A3i2yQg zYZw`}-Jd?J<=CoK33~;r@s7B2tI6;l8(arl8jv9j^oU>D7OQpp5Kt&91;t0i+=?0} zT6By+3kHlPZE%pjQpp6>(U1IL)khzq{pZ7eiS;q}J*-I%vxs7x81h3Siu5{fd0`IA zWam2aYiee;-urpA~T%)qB|nx{&SMyw*x5(Ct$ z%rVXo$XOf#=E-q9oR(7y+5xwvCqDZ=l^fx2p9WW5A4W>xIwP{TFM*;E>Q zNDINV2DV`zZOtUl7%_KNX%P7U$IlwML=h z#k_y61#jrU%{ChWDtMF;o*-Y32RrvlIb;Nj&O9+V?M46Y5TmREz(0`?w!j)xaKCkX0M!dKmexO(GG-#x43knP`lJ+N zgdSUJ*Sj5DN;?AAR%o(Az@mJoF!9cQGeCvBgS`)YQS>1>7I_26rB$YzpqbRd<3Fk( z4eQ)T>oxI9P3qXa;5Xn8C!==ZTH*B8nhW$k?dm{kZDX8G7ZUsRwD`ILO*%|@l%OZP z;M>#tbj8xv(j$18%DjgxC#Kap3?s^CY)`()u7G_X;*anA_WLt&d4m;;;$*!jCoooa1E6tnS@mR`w#TU&i*^6ZHGl;&r90VKL zY%QKRKJj6jLY16K%Nu8n_v_@gy(%+VK3kJW_cd&j)3+8N$^^+@wq|3yh;Z`TZA!f+ z$A~)VjO&4XQw-4evA8Iv+~S(kJ8qm;<>EM)f{d-o3}n$+SRuGaF=w)}v*e4`0mIbq znYOw#0zC2K}~iwb9T|2Ld(Yf6z5REJ0a zWnBkMv!|jwQR7+RU*d!y=asG&9Al0#HC#>T>g3=;Fxp);IL3HH|8{l#a1LP47;miWQmfc0I5r&8z z`T0Tkw;RCM>%I2rx$(kF3k<4QQ^5kvi`cAxj!*J6Op;a1=?rYa*_fpLJ|d%N`|O1%9kNJm@1>BK9WHM3g^IkL23-M+WJQ*Hhjaz z&Rjtj4Md_1a!f9xbANr@$xc_@!uRJDQIRA0dUe6|zcXBCGa2!e$_OwO;e=SbhFop8ah7D>Xb{ zoKAK6j2PT_IM|yIw>`H80p1ECoj`*O-FiT0U-u6!Ds4~*72l*XKfUcM4|lAFj+Ucs z37RgsDB{>tr@p^@!l7$48;_B3t>!8Y-CXaFbtwy+xoAxvt96prr*F8+rRKmcG)J*M z`Qct|v1{1tvr&RV2PJmAgGD4YNO92}vPYa6+ws`CL>b!@?XazcB>|hBHMCyp_n^BCzB(qr-)&!Oshe<$di#s;`{`{c7Rn^QJNAb4T$g*w#qDB zg`~;%P+|bBYhInnGJ{U$PPSpfBpZxEv2LukV0tg}cNQJU<||xf6uTGeg3$2K9N7%| z3KAt6nmVAB@lt1csJo>W1RdUQ+Bft_GM!%GkwD=}9e#*~Ae0MZIC357#`pgb$Tj>K z1#QCOf{q3;klma;PAMqj1By!1DJ{KaQGC6ht61gA zg@MdqJr`gFmuFKFM>a{;v0)V0$KjFv@M znHd!w$vQZNo0);zO|pLA$Q}j4jMzEd{lGJ0C!v6K;*gbJm5lC(p6Fcz2Y6B|FrQg; zuM9?xjVr_2`{q$YdK!NX=wW}JF#8>`Swa~A7<>*VdSHemtSX#X4MgP36;}+nVnS;h zhd0bowT)bbWji@Z*z+9#QTNF?5K4S0wJ1ArK-j^0+wPB9TUh=Ki`q3eOkTl(fFvdX zMQrPke&>?K4ysIM13hlFdOsY7kiU`wAkb&DaCzuh8S>c>twRI=K|sF0&_M*IKS;T3 zpGXE139!=tsRZKYO~nCVx86|@D_D7SWg27joggc~9s_*-l@%rIC}+Bnrad+q6wi|< zSZLxR4!atfM8Yb_GtyFd4Oeq#vz@B@Xe%<%dtc`8M-@KX92W?6a(RuD%lf(cj`bVrVGUML? zXhRbCpy(+D`@&+zR3>z%@8goIfPL3^^MK&&)f#yvm#Xs)hs?SYW zjLwey@L&Iq6ApO)mvUGFtcCHP>E%e^b7`zsKzl#rTT?xp(%Z|u$@wa~8{rhX`mLJ5 z>!-pvFaqap(vfh1C8uyQ}Bfl@N1v6r#-CxfAE?Ujh7< zKJ~NX1R*ZtPQGfem06R9l}02ppniV%eK#j=+G~c=lbQbC_K1LE2#)lDAR9X{3sf79 z2kqmLleDi)M&Cj(9C>Is5+5}wA+>aN`6T~l5CEZ{UaIRGE_!Rni{GTck?|a)!;NuW z+!NUGt1|kdK%`|%dT{VXT?6%>--VD)Mk%^<^Yj&fLNew-% zUtfjts4mY4yQnoKe*HJ?Q3Mu9qYLsjMoV2Hq4@kJA!44|JH<1&nkYg$h?ojQzt7GlyHRczSb6{UVtDka+JidSf7sVmi!PB|;L7qO`F2vH$oQNkkC(wkzW(UPn#YrAm zj#vAT$XOQG@c%b{OHv2gCKBBU;$y_*J@>?$ISKzXNlY%S+zK0$D1hw#dI+ORG2MHO z=bu`V3qhr6_AF=-uwrnMTg4^YNiuWBUFC~D1>?1Iw{(y)(G}_8d8a@V@tvuj3Cu-7 z<0}F0w2(Vw-;2E;HK5JOyCj10J^x_19gYt`H)NR_d7@y}L#W*-^_}yxySoAnPLF@U zc?t=B?>6cdHg3XL9%XOeuRdFeVr(QV30U$wli6mXv;QmYgR!)_R3I4t5xqGz1Zg3_ z@#tnV5K~8B5ff1l#P7mO>=}kPLL?yA#NUO0Zn+^0yY)^+)u|1goYlYBj+)ung6Dn6 zqhGS;Ku$7nt8)q@d?{nB{e7SfmGEBYr2~ek{C$&2qAhytZB1Amhlp)y#aNqpjT=zO zG=GQFm}M^;lFp<0AYE2-aLJ_f5f)Jz91wuGt2;an=wo8LS8h)=ct95@QRX48sT*AE zQzT5Ys}K0?$zq+VXx~rRuP}wZZEurYv`bFsT*C1*l*bQjU^I$p>$P7?cQJIU>+>rf z>`vb!sdGJ+o)3{omkfa$ibCMe6qEoc%SJQzGXfTroyi1mLoCO*cHZgxmm~-ZSY$E) zXBzTtsk&>FSQcf@tboXl%v|~bi>V6P+Pn^~J8Fbb5+Hb5l5%D#W!mHJ>-)fl-B>Wl z_XWQGL&yX1IVip1^@e<#rj}1Q0-3$ljqRgn`nWsX%cYU9z)M2;AzlFoWm=ixyT>$%0DO!O(|3-sAmQN-PVuZD@ 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ÐbbY4HT$i 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 literal 0 HcmV?d00001 diff --git a/Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp b/Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp new file mode 100644 index 0000000000000000000000000000000000000000..c217e67c79b52934392b27f8abe7e6cff3c06d72 GIT binary patch literal 1131500 zcmeEv1#l%za^@2=jhLBvG-8Gk(}s{OHe{JtN?6~k< z%!}@qmHkRxUD;n|Wp#g1k`oga-W5awD1Q}@Rgz^>fd&8oSiklwFu)NgfL~fn3jCM$ z@#_QlrAY~kQvXu_>GS6rc)!a0lCq+ZPyhfBPMzfn!}Ukt3Z;A$ArYAuaj{PHeV>$U znu#dTMBd6`z2ctv`%WedE!&waoF@DCo%0l?S|UAQ3w{(!wV3bc)OnoR3Cs1C*xKCn zmTjyoOsU$aZ>;r}iP|gmYnYhiKTa2`^~2*Y&MR>lkr;u1K5lsbf7nXj@yIi5wud6O zU+wq0k!5(^d-pIkYbUgz>h4FV@T!#gS!*U~>4TT&Gu8h1h~lf+hqe|((oc!eZi@g5mb0XE!boMNkKIX5-I zi>N+!DyTXmH#pXJ&Bhkhfh5!Wip@F$ayf#Rqd_2$(dvSGX4;NO&Z4LRx{xW}?UkvS zV{V7gh{h^Zj|I7@vt>_g1mg^fI#n`S^Ej{O8tH^5T3b7@?5g&Qm3)}P` zS^ZT?zVfLSrwX$ANugD`G^p5b+(xjjJ(=-A+^%@JU~unW68ys7V8ebWWOz70*YCIo z%_M+VVh(j^!3l>pUmCDJ9arRYoKbOI`^=WUKnk32+#**diy*2kqC08FI^5ajrpLP~ z9cWw(q}40AN%i0HP|;c$nO~RV#VfHUme>B039O~+2Ws4CChj-Q4h3_CpZIEQWk;UAPckrHty}Npre+oadg%{ATx-r^?LO&l&j-jTM!XKFtkhv(K4Q z=rXsskmO=@L6%)!`IS8R2Lv38?8yfb_|3nTNVCk9$%n#qS1-*HQgku5mBdOgW6$RJ z+E~!tQN}$gof*l+-oyi_6Ak1t`HB4whlP&e8xR$xM{?ok#DmrW#xuEwm3Nd4tg5S$~kq)ku1bBQnXxV*e*^ycg$!u{7sbOTqnH z;V%aFe5|(3Fp(JDOTjY*MNd#qcp?W4F1$jcDT zv2O0CadAi@H&(9VMOBz6>5>U{cZs2Wo+r1bZ{j3WAVJ4Xx%G{XX6aT$%7=Jas*nsS z!bWVUP^O1S-{sCkK(T@aCd@{1s7QnnA@p)6MxJ+sl!}fuf>d!&jfNs#P+W;ysZWro zwPSa#l82|LVU3~Mh6*d7qS1Is*AugO zm-wX^b8A{FTIFQgrF$$U>$+i!p@xMR z{U%B$lq4}0_TxKJ4lB!#it>5VL^`3eT zNrCCyS5?Q$9oxTl8mQE& zc@rAiys6>Ar>E=4ka_t^3->;3e5T}T!|t|cb5z%S$FS-7e(-zy-(i77>0fyi0Ra&J zQ1t+m4n%bgb_~iBEt)HtpO;HSAjgJS^$E(4-UGSSyM80~7Q!{h4%xtN&o`|&tq|Ta9j48|AS(8=AUFPR z6BdHo&=IbB_=pPjD~OLixfzRry{I_%y*va(2la$kU?SYr2)`@;w#IL4{NMJJ9L1zh zr(c2aeIYEup(wQk2inVp*;mDt+liwD{{a_;((BGw=pcntrCFMf*2f zXah7YVvnk3{^=1qn21Jdaz&WOG9QyP-6!^c#HX9V}C`tBeh&;(Z3oy;l47hbA~MXI%LP zR&Ov5{g!r5`r`40yT!5Z^4A{jO&%xBP#ZFkHu7t$I)&asBJO*$Bb>r{d%)ih=Y?71 zAK9kX)m%@_1n3__6?HoO%6+gAaJA3{8(LsJn*+})9UqbYcx{dea>EME@!RK-6CMbDE$AwH+kA7(WUh9WVh%Lo`(gM>B zdM<)c54cWXM@2Vme-6PRnK$gn@}#CI*Kz%$qay@S9`BZ!KVl|yEo7@!(o%EOs%`wL z9VAwmS8~Sw$T;$+ll-;L+W7qW`t^@#0tZX;>hFBwZ(`8z?CKA2{+(<7o1pbO^Zq># z`90D4+d=8?CpG`O=cq0K!28F>(qGdC^qr68L)v$eL}M+F zD31wh$54dcaxZ5g#FJI!!wW19>w6h5xRa`$T_G%u8Ui_#T87Vj1cge@NW;rq4qTET zgTmTyKbh6XozvG$tX}fNu@6Z$r7sus*;qT*IcUrRU4ZR;$10J)S-yJJRa~&D;&v#` zu%;?EE#rJUzBBpZr!Lr1Q8lS1o@UX|QPF9^n&gWui!ybE=ph0lF+*-+QNVW+g1LJA1xUiv()+oVeq3ctj$~c z8^`;+@kv5qTr@3Qxi_lMSDY&|Aj*InSOi+zGtn+S!0P50^(xzdWj8f@TdyEyewB?g zRf4iwo*f3v&R`WQn2qq$KsPWHeW^6-3|B!vFsqJPP=+QVFB7~CX`~6x$ zcF2?p$VA}v%M(@w-Ary8^7teMQp9S!4*G0RDUs05T|eh1FEwy4s2UnGTzm{;RSBx} zuph23v)SIkOu(FIy4C}5NY`Gn+8l<>T}=;mi`&NMwfd5;`0;~@A(A8{SV$RMN{<0- zhFYf?df&zSq(m^3GBFdij&lnaM$>!P@5Y#3D4aIDqy38#MTm6uO>R`10$|NIW1C zVivNXDSy7T`-&z`!wX^Kn6?Gb#aFG0TX$8LW8i(aLfHk^7BnGg@tixt5DhoCIS0tr z8Gi*Njyy$DuW5IZmp>6j7(AYn1~n5#!x!g^)%#1Q1G?3`bZPk*60$HPWMN5*z(37` zK$!vg)&1)a=#%shKTN)nWj#r|U1mRbU@V@9T8w0UXUB3f#2*-MgY5F0akzNSk7w|% zy-N$jXh2N6s$z^ao z5qQMQH$BgF662(!-Rs*WUFOtaT1%Q=Lwa=_c`EoR*ia%>C+jPcf+o^@$fn=^7T28F zJYPH`3V_TYuNbCAgDJxsKKoW@qWg^BBqgd2bN0I(bl<9lZjq)bqMi7pLd^HJeRR4z zqY+Ow1^K%s;S7b8=bLYo2jzujO3xecOQf)YOH=f@q$D^GrxlSPPbfV(_SS-r{_Ell z=Es0*1e9$y$1FOE?x2eV-pp%K>uFNqn^b(q?kAf%n*#c6L_b1E{wgku8QY4d4buH?A`VN z-<8-=ijoaiVdjwE1Pqn0SMezHkd7j{s^u2P4I^L5$nk~a9E99jw3SrG3>n0u^?iaLLIen(tIaVsaa|f@JiXG` zJ_}qGWH){Tr>)-mwwNO+OMNY31D%d5X(o#ZN?;p;{>ZGM)u?N3YE%c;nK+Z_E^5tyc9l8>sYWl-jli91Sv zqV}9XxzQi$N~ez=nKc9M!q$!@Mgd!~Ng{a{D__pJm`jTEZ{H%+S~Ain^f^ds$}!DF z%YRDnsz9M93-E=R@i?I>E1`BXJW2G%6(OnsfN*379uE}vHA~%~XFf3b__)}Wst=Es z*Yihf21Sb!reK(<=x=|Do=R?gHs=@qvOb&Tb6O(9PM&sC5`?~mR+!c3OxL~E;=!(G zKIs#sJ^L0AAIP*Tz2vae+Z`Pq8{GrDT#8hnqz%5fYq<)M-T0ED==Y;3aju#T3lwnx za$;(BjYsF%oSppRJ=}VILciR}9CqeBX zl#&3T>^=tq8SChY$~pVl?rYxW751+f9Nv#p$V2XL<&mF=gM_il$_lPYunkkVuMZTN z<#IB+=kyXbPeKUWLYJuo;|q9Sn+OS5*0s@ZhP_ZsrIEAJp2O=7b=5~V^DN1lYcJ)Z z2Xw|fpwbEzR$3CWCw&c=k$UEgSWQ3foNbA^2Vo35+C@K9eV`fBNf z`5g?g?peR1Q7y`V__qXntiAR*C#bd-x`}Bzm=mK-j+;l@B$}%WnG>LMvF0CcpHt0I1xaA&cpH+*gFQBe?XmHXw z0(~ZgJ!{JhY%D^Gj7gE|ZJ4PtugL{BH5M>|#c@~2dB|duCOIge0)(V`ctVG|V2V>Y-kJk%xUMA6xI2HH*?MS9*urfAc7@mvw> z+8aeOu0VoqlQhtPeSo{(?n98@x_qm6;fY3~ezsNAY~$@Y?dq*nciWn1@5IYq;=Z8y z%1e(lOl$OAuq4F?O29e=Ej%;!u4mEq4hC&hdI)Y2k0ybkC;DPRU~AV;MRld_>2J3~ zSN9R~+Y+M3cZXY50rti&4?a!6N`$410(VAD0xh9gB;elVf`XCR*u9=zU@=EKZMPKi zRcCq1e#Ck>Qe|*-qBefd0i~XtSfA&h1;$<66_z=L*&ov&us5)OQfqtB*XWlZ^{>E{ zWbzQbYU4a>^mSI#SBn^D(Vhrd?Wt2s{B+j zS$h`N#*z?hQPD{JgJa2I8v^UP(`^FPU;#sxaN@3B%pER5xx&Rj^ygf>tB2ogO74AN zAIVw4!y_Xoj$Ypi<@r}CfF56dkI#LsY1MweQTKa4Wv@cQs*uUx@vU&T9mZ3`G`+qv zt4oFX&G2TCcTEQ>8qdkhAV07O3CgL%rZUMFT9s%Q)(Y3@4_>OKRcU4gwumq5Viw0f z8wj1lgxYCok>f3m(4wc`MW_<|KiM{yYbgUeG&uJk*EwrI&95C`m(1hV{6mC$+`D3%59m>Fpgs4mV?x%a?b)YXP9vs(+rU*1#|{ z40eF)K(Q{9KCrt_(c6p)BBbc70I4T-zOFP20VlsZlm#Sbh2DgYR3e$Ys+2E`3{G7d z={X3hRpM*IWC8~crH01^hPkmF&D{>z9nti_#z&P_P-J>DAE#C2o|1!-EICDtrYH@c zJuq!#&pg*KN*PP&t0n??HaaKtg zhOxWBt%Sv+0ndzi$^o+kemRdrb*1$*O>B!%Q$e2^5S3tJ)_uirK=IkmQC1=C5?-Ef zCOe`&aQ2z$Mr~!{sZiF#%A96!@KmRCGy6Hyd^KVr^QtQ?RD(oxytvObeUpvYnN8iy zNjb(v{a%^DqGqM|K`YcB$(;)6Om62+#Va$Go5P?i9eKobOw>#cDF(ZLaK|;ASj}beLHsXI4kLE;d_ns zXQP`pph321lmLTu3SdHXmxC>Hx7e!5|6n6xbQ6kUBB|;vou6cCf490T)z{8)a8vJ9?tlgUtqZF7-4 z0MANu7+eQf^)GouL(BLCZpVx+CL9z3R~>yp>B38(j>P(wH6=D6?s#&e{?zPxHdU#R zHt7-Mf3qlF*38X+R+fg5KKt`n@GUkQ6mJZ|W<@UBE#{i#n{9{YTRpf&q0Hf5T zrx_Mepj-j7muUPag#v05xLO%@*5N7`C0j^B8|%YxLS4NY;zDCU`Oaf#s8C%+QKXu# zz)Ypof2;z7FDu?(x;Ujy5siLUhNUp!5wr*axlU`#=!0%3v;cViqGfG|j@d@Xd(2=* zSk*kbF~>keuwI)4ncEzSRIIz2}S6q2II@Cx1o= zNb`t?vPmYhAnXIycjZ50l!?O{_{hlou|}~;O|kFhWsEf^cFo-$kS?^X9x79~r8+bQUv^`7x!%?I+6Ri>)L*(NOEu`nX@Z&)A{)ADc zb+XT(6wk+CnUq(Jao<7Usfp{ddl>hHyvwVDE8-;Ja^CTKb??`t4c!c7x2!w2YPL*fZr+c}puJ_3$+y1>}S9bg}Ct>p?Cp z;4Di7^x!jTW5_8jLH2cb-;w^R=nsICXU&`j87}S`rT(Cv^?V4cGfeib6ca;0;VUiK zg`^zmNx5$Sn@yQ`g+*}jP<@6VZ^^d8V0!NvSvd$jXEQu|MQ zT1eV|KGknN?f(>;_cx#RH=p)5pY}JO_BWsQH=p)5pZ5QCV(_2&w41~Lz$7m4zw&7_ ze(`CEatQ2_Z2rur6%FgS1n*vk+5eDqoVQr1qYZhT8z+7p$hFDjEJ$wy)qao>WKliR zkYC0dg-BuoS&?t@?7CVTr-$=QTtJSpRzY~+%5x7srQWMPO`MXr$3c(w(v;$p)I-Mr zo1F#iC$#;o{#r6fZ0!km%0nL=xE0l@wn!0@2fp{e@sq=yF`8h*A;7 z+>)s|ljENbY8?OZfrf%CGDes%;A1mjeZmgpK9DoU0)C!wLxV?Q-*ZFNtvP#YIqIztkKa+P-r} z;4qitFi7?*MZwSK5X=^Q2-~1Lg|Zb%L|IV8!}=(B>w@LH^+GJH)$&yTdC&g3?+jWy zWk8HU8an9mvd9?z{Xh<1uIUPV;({9WId=^3bJY|Wv4BSM3(JvN%4rJ`ZwY9t7K+KvR> zVgAK~-4^(nC+y!^?^mmdz9A8QMeW5aK=lJ6ka>2~!^KPbWARJYdRzvnFF2!|UrPuG*B5M_ z25zf4#lLp2`p^oxzvq(>#t#2{6@T#xX0WJzi2DqmzV)jsu&@z?O}ZVhO*PE$XWYlj z?+-RCeWSX-fTdn_nn;=rwFR`Xafb@`nL&o7{dUUUl&LW32sG)c;fH z{jL7>K=j)hzpe2vS;=oO{075sF#I_~|NqbzpmhN6@4c7*{FdmyB|k0DBj!Vi&%qpb zTcBE(Gs3Oimh&nfCtrJK?se1Xw(u&egRUG%`c(_O)EPw6W*0wUZcK+zE0?_4yVqnT zqtU#mq*tFGRX;Y4dq}8;^D@QP^ju4*TJVkAeJ?<4`?fmd`s;lwS)p^F1#^}W9vurM zeHUp`pWG4a9F>vb2QGTC4O2yMvn-!ksH^G%Ch~rMaIC1#7d5Ry`|4X;cmg6r-AXMf zp%d0OxsQSf#I%EVHNay>G~1@l;{`3CetjAM04)c&DZyt+*9v{PWOR91gUz;r*KUtG zRAcU8dd$*qGd03LIwNy&0%qUap{lUAZ{dgnv?J3DPPwfe`XOpmHM(2v6b{d%`wKv1 z!;OCx9!|AK(OxB+y;N)vzKc-%EdZ#8!$fsF({ zZi8)$kY-=t#IxKmvnBZG2sHrMo_=*LEi!(FdD2NP{HYUpO@VqJm7br(T9Timm=WU+ z6Zj~pJ-{G!4|jIjNU#DW5ME|%$rs5pqp<_GAXjCT;!=b@ddg+?;-&M`%X%X}3cZ#0 z#}TZ2ue`Z6mGXK1N$--&PYbA}ng z5Z~;F=2hi!?*!ko>JXlbCx%{(Re$U#monV8+G}?ay$(A~LuL!p`!Uk)!K>A`p}JL! zSw?NMsVpVrH}}wQA}%}Ko{_R0`;EAroF~ zk}^qTVzP63`0T?D0pToBzI+Fl97mwfG8xb~KX!PeGe=u=sH!?F>_Nkx$+!<7;R^ z<GZWnF#PuB(+hB!ps8PpI zaYbZuS;^u)hE)Gp(Hm$vl&u4G-P~L;C4m7Z^nf;-_S9s_agT?D6rW-D{#tHP6 zva78I6YV0Zv-3=n!i;#_n$SOgJaLPPC=cSO6>sBFj}b`jFon zUt@lH8hN@~gtK_iA{B;TF3M1W4Yj5E{p*^P`@|V+%h@+h@`Tuxk&@LD zNVDZDeOGX%^q6JQ(H;*7iBC$SuihVq>ClFSpU|eoOs2=6Rl~k=QJSS92uLMs>`*^+ zxuIy9{^XL~9-k4}rC5O5tBsUD7^>M9)Kz|K4ccwTB9mIkKkd&l%e9rrjo|2(YJs&v zbR|)=XH)TTI*ArXAXRR^qXX3;qr`yW!n>f3WlVF*0kLN6)X_?}#AJTTe_E4kKqtZ>wSM1dQ5*03#f>$(MeaDLX$a#y#1j_s z!BzoZuf6`V;t-&Kdz$rwxc;#YD+#gob9=4OtESp(oGdmo{EtwrsfPA$$Ut~vQ4r7G zYa@Cj%|^ra7_^ECSDo?vZCDfh?1@GB;v#po^H`Kz{_*0pe>Fa_Q zQfk|s&YS9OuPZ7r{4m9?)pj=cY;L4#({HE!U_Wi9Eoan>193!L5{r?_fR zazpXiQ6d)b!A;Bt2hp3G(LADfkSZ1WW65(Tv6)X@o62lz)nCSKT*#-GNNF%IX;lbI zV{{N%?9#QCQpV3BKxUcpUd(H+IC^x_%f^B4EYpx!9@D66=o_-{oUZ0&$bI|IlVg%b zeE3qcAQhTk!1)lb9Hnj~A*96J*+va5XTr!^o2yIU1CItY6bd&L7?t0}XwtroGN#Ct zwrn2U5KV;VIaGy_WxBF^W;f{6w$J54=G~wpwIvh`j2wx{ITvKQc3fvDgedwMI(!=v z{9G_~ZhF6g83X1}2W5>b`Rer|e8MkpKXa`mIZSV?fQpr>}{unwWcxQQu~|)=lkF!zV3Ga(1DTrdA6|UhTdk?iBpRKEuJ}!`qAF; z(8Xg58XRwl1dz)*X=;7HorOIDwFnuDpbQQZcZPN_pJ%i+sF>I`cLe2g=Ubi60FpyUkBM-Z?D|6MtR~y1)R+hA$|8RLQS=^6sqliP?F{s}&Y?+vAY&ec09hb|B)F zzNgdm4u~0N?P%oG8IxGqC>S!Q^XIUipK!NFO>2u;F``jE16L4 z$g-8eWp(*rS6ad)6@U56J(xL}JDN83W{d;qseG|2lx)Pw(QJ*vcN`xH_^>LxH=~CAH10#iV<^w9)WBN9qFCp7Y>1Dvq=H~hIHjKGL1SY zBw~+)3Us*TP_A)9{G*K8Jw1Fxk?S2wo8c2s>9s4=OZIOqU5Gna5N{selOTX57}&Sx zF1L7kbA2R~@*m0)o+aan;$f1@67>!?sn1y@CEiz_dMWDE)+33&RK|B%qwCo)LoDYF@`ffpisg1>Fv z!#9MMz@PZ-5U7e*UEM>i0(CsQx;NCAxTn0(s8Yx>PT6z8DLP4B*9-aDJ%~822Rc~Z zaCmK|+Zzi))|tyGly!zr_Smu6n2Dsx?zDHRH6MA^NyMK_gvMYF328L^Ql?%4o{>HC z){oF3g>1Z*S{w03+$b&NxCn~^83t#StzFd$#Z;(THal2$g*^_k!np!}yp$Xsgta(b z-f)1F$GpnocSXBFDc%jy-23Fbh$t9x`P;xuBc^?0^C@0yi@85@B|TfdmsqJgO>8h< zqPJS8(ORoCXlv0La{cj-|NL_j-T(_-P}T)uT6L@R>;b!5aN@T5aV>~1V7optHUiY$ zxTx`%xN6f(onFH(!U#JOzIXQ}{np`y=}DTa5LKAYhawwGyXD@qN90j-(`TYsQx>TA zxGfpDPPfG!gAR-v%D60<4=m43J`)BLS9)+e6N@Pqf_C~n@+ei5XkPN065iBdA<~)2 z8#D()os+anUlz8=-IwMwom>IVoV({QERj7Rd>KiKi2X2&uW zK>C-e)B>p^6RJz^T0z4akkKS(V5w9HZ1d7TfEJipFy-90eXdoMgRDt&=rf~GqVQnS z;8(}_?Dyg{M+T2TdnmK`DmQElQFFRrd=m#V97c8pe~9$GlT59su^S-zmccW7QPe-0TZD=;3zw^#HefR&x61iONOXpTZZ0;pbH!*c806Q+g+|BSWJ zDd~CHGtrhmp9=~Ng5XY_XMZU`Y+3|X;*4UO0r0?seqDV6ZO{IA230@sfcDJT<|wt? zOo4~U4NYVKAi$??yc0QqB7te3KYJM4m|p)Ri)1~NYD5Xl{Saz2hT5x)UaM9h%7ZZ5 z*Y)a2VP-Ijnr=%10{^0B`CY+Z_G0XtV+f8~yw0s-y~aE|Rxpw7nT3+F*=3?X-O}5)2jl3ygds zrKIe~uK-}!@yv2Fa~GCqg<#kWf;%iMqsDC38yATTDWB#KhAmTR?2|nmbT)(eElm1* zT~GZo(Vr=1R)#?$RTC-w28a_5Q=XwFl1y_PDh=%~bR>M}3vs6=4?*>Z5=H&VD zD_0&GrL7bSagp?%_lVLA4t_tkt<-<=JGMXuw_hI_Hz*(2Fpde+I|=QV71DNWmnJW7 z-}Q$xUO=YJ#qBk`D%x{uXf;k1aBm@})TGsagj5)thb~}!8E7T4SCUWIxKbQFvvt`K zPI~Z@HXF8Ed8cYhPXg$R`${a~pQVhT8yAMaZB|o&=Mnt`0Bw&PV~Kt-Oz0Lr`kbDv znR3@z=*}68KQjINg6X0CL1~@c8}ej{b4p46+=S}+DZZkv;&Y>(~k>tw~QdyR~s>|S>hHI%Aw@s zlleXd4PKDM>yyDvVB_c)O|#>CpZ3|S2rYw^3YK~|6Sidyr>+;c`of+3Lu5#)SmLGZ z^qtht)Q?@=LeXt?gwHEOBZ?r@OaOn5KR!eh3{BaTwJGy2&vk%azqkfVJ9ZM8|EQ?) z++P3S4SrF=!1W^B2cGdBYsyx6M9R{6nsiiET=ImW?+OsDsjqsBt=Cl3w(SaHGAItj z7;ET=G%D;L7m0wa)a_@2XH2pU>j^W$(P!Af48#~^>;Lx;cK^4YC;boq@t^q6dNluh z(%*dO-+bub>kI#0U-!oSxS{=L5N@AZZMU*|FZPhJ0?`OqA{_|S+*{|+CTP4=Jo z&|8jMgHP{Csvsnu1MR&2Ep-8M;4S#iYO7G=O@;D2ZHJ-5yz7tl2js_}M-EP?quk#o z+KkOUymwks@X$O_EA*TY9=S57YEK&`6Yp(4#ga)Te>NEUEqb| zv&HK zie`_(KS%$|JKOvBqnSeKbdG-&*$AvBke3!&E&L|pEq-%-D_Ksgwym)lFra4`d^^l*sMz?++im9hH}LUC5lw>;`#qTA^P=?%@qm|yMK2tWQzT9wh@Nw_EbV- z=z7P{NUL61tLpdaIf;B#A2eQ~@BNYn57CYKtC7;Fc=2)*4LWp~3Pc)H{?{>@2+hs{AyZyyQR37cgVb@ zakC1V>N^4c6ipPEjLHyJXocubYt4u_k2y9LVrmWyeWvGPemt}u#-+LYhc z4jbQ@g4eUQ0*kcI0)S3e8*)B54Z+V`a^fG~8OXBG!zjg+v)vh!$B;aW#;Y{v-yhhh;WdEl9bA&@k7mJqnDtb@##@ zW*znmF#e+xxc?YXiU0Oarvi(-uCtC6GWn?v7)j@(Khuc#G&BtyI8-b>Z@8nobGN{L zjx9#ej~bP!w)HchB<>}0gzxY57iKgmsSSPIO|-o9$IChUK&hS8?AT6l32k_loA{=O zOlv#E{cj^pNvo`yv~a8F@;H?8i8$QNxOgH;TYWDdq5ZVq#kO+~9@;xWfghwz6rWB2lCB&FwH2EP?Y^W%1q!xXNCyEo|Pu~CC_&86f-1omf^8X&4f8V3u znfmX^%J2Ew@44##<+-YU=dY!b54`^Q{mp+%0@?rML2F?0o7QgP4|q3wYC3-vf21jz zB`6Sii9s7eHZwZ;fT?he?AsrxLBy3lk>(VSBWwmgIA_I<6hxVB2C=5{M3_y@)8vjT zXJ3SbmIDh^xCduQeVr&+|9ZSH#6BxHNL-XU|3j;oIIhqXxfepCdW+@+VCZ?`6tp|* z5_aw5$}6|`90xKb$0R{7XuiPqQ z41~ycH2Z;ON#Ihu7DpGE`4zHEX`rd`bCl}0q)?}-%LPryprmATc8jKKH@i|CiX$}g z?rtLUnNQ!`oCN03Db?+n4N``>3|RI72s9Q<{fzSmn=hJ(3DE!2i;jI#CnFT!7rP2W@>OkqbE*_Q(;1~ew>j}teO%xEM4`h2*H5H-)Z;J- z7)fqm@*pMgXxE#!IUmiSyZ1ZF9CfDNv2X{G@7I|4l&Wy`n7GDc6*1C)h+m^KIU`G( zv2F-*v#G9nZN#UOA^$QqE>5}}pQICYq1+AadS4-fAu}8ZFE$1pYTQsK4SmS$496)OfsJH|liVR*wXA;yziDWgatS64{ z&SSA%8R|tjg!uQm!wjd6rK7Sz)NM4K7`u%f!K0~hj6@a7;M*OJ*OC*r7&Fiq;%v5z zG@>q1?VnF2O zn_dbyv9a^$wrIOKPin>p30t=tlG;RdUnXPpQo0Ax2*XiJ>dnH)TS~$)=PFk**Zj%D zuLX=%wiK>aVu@$9H#IU|;LhLY$4|uXV81Y4RU?+Gt0i)oJC#dAI+1qH9Q7VvGFnctJ#<2!T9Wv}i}KVKO|Y@vH*d$tlbuk%={nz(4p zF=VbKe!P@j;Ifs!IQMo)e%be#h_&>C`A-TGt6H|CCJSS@XBX&qIy;hWEiDSVIl(c9Lz>nX4oSSs zJQVnVQh-yUHRH?>51oKPcKhQXa?n>b^Bnndj&oMJ-w=s?c@qhOx^6A0_T!YQ)-5c7 z>0ffK`$q+BCiUikLP>jtB5)ER1NhttTttIOtwDbo&r|Z0pL7Vc^`@uC>uI9yzgHR!#p#@RvbTb&U*+ zzzural2@sL^C@?qL>qAzIH4Ziev2q@Xo=uB`@68bdV}#5kyE%?eZFL}sNxrW4u{Oa zVPartAgR!4CLFq~=o0Lqby8l6N`L$D%@_xDUDFf-fCENks%>UFR3yT?1u)o!yxmU% z3La}I9NX}+@}Fng(RQF&08I~5>4wzC?V8|EWe~~R-LrQ5`R1#vtaU&7(FEMj9ZXJ~ z`_y5a@N$W*Ge}p#uW(gP=m>3zbJ6jX+SOu!ASRwv4Ysv1u&BIGdmZ4}*~Zr9C?z1L z7&r0z(?>ue8Jbk^kV*OA-+S&}>QuX{xI)DU^4-|x{d?lvlsC8bS*uvl?wDMF+Wc$_ zk*Z$hK1CQQG{SCX!b)@xZ!GO?ES`f=3?)UTRHPX*}Gte`0GknrQT3{l>O z@NvZrgj~7ESuYM-sv{G6E3zOsT~=#crEE*K0~B%zzBQS9iMm3`wT3!`cxwsJ*tS_+ z+mr4KFdMp2bKOU>?MdGXn`z7Wsesa6tl_bhE&Rw)3Yq={d3niH#mZvrS%;0_HugF=6yFT4&LZoH|ldBYkAOp6l0G}O5XP^pMZ}No!oGW_X5c; zF#;dIH<>f^ux&J*FMv#TfrpUWlT)h%+JG4=IDa|iTTPD#0#~~J4|{I`99fUv{hHZhW@ct)X67+7Gcz+YkD12yn32OGqh_V^)HGif0_E$zy(l0j z^mfvqsHx=2fWE&}P>dPvx(D1TLx#knapm=bG!N@aP2IRQ(gHvAy-c8gzWTl(!af)E z-8a-OYN@3;s?6{1{=CXt+)URfbq*p}bL1E?j2SF*9m$!gW`EpcAto+tL6ncvS$hE6E5RjGP(au}D&o z%dW90(Fg0WYKSMF-qUXWh&V>6zM1NqAqP7e@Xqq185yKNRmTevv`Q)xmgLy`7FkQJ zLwi0w8q(;OcV>h(xnY8j!RT5W9Uq!FSq3)R+VPXn-P+SG+Idp(i>>N;_^t_Y@8GWd ztDSa^YedCrw}#iLMzSBuhQf7|<;)g*3;`b$WZX}8KV8;nzNF8oL(J>@O7B~r_-{jK zQR$)ekG9W>67P92SVN-rYn`etrUc<1NEOtWkVkttu+)p$YhFVJndZLxIaVDq<(LpH zPV>$k1j*(LYu>1HTBCtm&&z9c)K-j=60e7$RJ{lg3u~f{w0vs-nccJzd>RG}6crD- z@PPh!mMxV3fg3k$1U~^O*wGEYOCmM1#2;3!wRbBunDh9?&!BbbX4oqfb9KlV!^V%L zdv8vzDg2<8x|UmR&NBJpYqkhT{r;8`J}-pqqQ!El5ymRyT$9H)HGW8$Am+wi9rURT zPLU-usFXJwO)>=Eu@9)go0zEZFpYg;0Sn(7-|HF9clMlCYQZx+dG7m-37{)t{kdot z*<3{EGx)o+m6G-*Tn9@vb#0QRFER0U!bqLafS;JG%Bl z0r=f_OtcWF22 zd*|=T?i{*!&zDd@Pwn6?6JKdn54MeADfncX@q*0q?v+sDf|UrUVXa0sAtu3TuR4N{ zoI1=GY>M8{hdSEia3(i=+;U=&;lWYAu-)LPh=p!gByaqjZ&Yh70_NQ6PZ-!2hx67z zFKx%Vz_Ea8)>#s)=}1!xUgeOJXWD)qSa19g;Qr}xT+?8jw~JUQ!tM4I$OP`O^{#K; zpCe6?U~)G!P!lDrFRw~((+oG|yHvDQ{fLx!0mKZ2f4D-bTV7^%Qa*8LCCXO18}rFw{W7C zeat2rjWd91B4oy(r(!czg9;FrX-w0e@n1tI3*9GB$bYm)odqfoeMh~=EelU-bZ8U?*y+qxDz91N=`#}@aCy9Un_*>? z-2;Ci5+ku3?G_W7;0U9fb)IyqlbGPo!rg0`P1X2bx@{y(tGO&Qfl2qmbm1VX(6Flb z1D=Y-pn41eZ5B*hqM8a%04;yW*kwWBup&%vdevFZq;{?ijju>Ba=Ny_B19TY5YD^@ zy=1U9pYvJn1nWT37$-1V{uGQQ=i52h>tT(VZKgp|++GOnGggKuggHmRLodMYUYm;V zYD(48qF`GRvV_CrvB;;-;PtFlZBxMLkON$Bzy|V_8Sb20yO#uw+PCvH(O!vQRAZ|z z>71+5ElS1T_bV5+m$g9XZh8Q3Y<@lJV{ha2`t(%NaOXgj7F0de_liVpVLe+}IJU;Q5R-~8dfkj7_J z|2NU!r15Xk_%~_%n>7AS8viDZf0M?)N#p-6Qulv*I)5XL`w0O6?4AMtZRO=W8InRG zLVWor_emcy~BU+9e&tIaQ(l2 zf9O#Xb$>wQ#eN)>4!tY^zSurIAlWYLIQU!d|yfx$GW0t!f2C_Fd!ycG9$4pn#c?l!G? zTdZvkYF#BpNnuj*6bE>a+Z7o^24IpP|MUxPPA#Qpgwx_up^r-d>~4 zND@}}lOUD{7kgfpN%$+!u5KA*Nh;jgWyW`k+Xp0H$Ls?>Bgfi_0)cuP(wnY_(sU*TV$d`c zQbnY)pVLS7NEeCqq1Ywt>4k@oxMl3w`VW!%<$45yv87)BVBEt-RKvgzA#o|6yhw7g zNk6*j<`6Oaxp()TE=TlZ;+`9s*l{(gZ7m|gdM|x}RdeBUEW)mX9Rbom75PuqhAt+N z$#gkZo;Rjk6#g+3z@3z4UnVuZ2PHkzo&orBuk*6d!?|MH7Xt4i*&GBt>^gzgCut{C z$JpILOkn|x2roTUlyfJ3jDkETwMm^v7zDl>dZ#u1Sa96c&3`raNI+Z=!$YF1PQp~xm7L;kie=jsH%|J;^6n$H zcv%5&+o{GKUU zI-Ogk^DI@-S2)bj4F>5jf)QT#I-pjeU~rRL$ZTw6pEL~X$%SMwuSN|$BF{?$Y7VOCU)kMTYUIrY572f0 z!hK5K+}DiEg+k|YH%^X4$%VL#$TE?|AxV&Fo4vQOhq7t}$O8@q2CVbvqsikEG?1zw=zCyhGS2Ja9J<(-^>4x68$|M!Gr(DnpK#z68e*O z`)#`4rVI1>`>ynl&hU@U|93iqui9LWEJ5OD0u zKjL}V_#?*Qhv@qPo;uWz_gS{gjiM7=-q5t46BZHhOcX;ECd8Lf6NWnkvTag;+Js4( z$&D(3b=_hvS0gwA5jp~oxWT76m4}PC1_H7&?7IA^E@}RBC*Y^jZ7s#Iuo>i9!z!?y*yM_;_ozREc)V8#$HPOh;lB7`Npy@W6 zvnJP_P;anns-n$=N0i=QS|Om&izhRF^^naRYK^5rRqBmASbX3vstZ+>Ca6uwxoXqH<~MDRD?Dzo`pGiwsDDap=>S?O$x zd&v>D_a^iZ^V*`P7(}$*Ty`|m-H4?!$-TUu%<8pV_fY@O2@DS8c)cZl?p{T+*s7(} zxKnyWQ@NbR*CG4|w>v8j$nfj5#<_6Hg_$M1oAYOj{U{&u8E40ZMQzhzsV(Xk@4jIPTP7$!cLQI z@fxv8Pdvd2ZsTLBU-0OC!$PcBk4D$D4h6Z%85@tfnpg#%@b$&=84p<@lXuykS5ao1hdC81qA?$9fW(c zdm3}>P}4}a?#X_=yE3-SS*=Z7XD)eHaYJZ_TH7S)}1uB(~0$8DTkf zZ`oVX`z&62<#Il86(CY82To|jGyo~mVJ8wsY|yWK$D{|yQO^;-^JBM-azy$3!p5De zOo!Sv(BV@Qh2k%F`(V<3qFBq*?Is*kQ`!X zB?=Ezyxt;E?d;RT$>}h3FSAj;`Jr`Mrp9U0)9tyDDBlAW0G~p^OpH&CHNT(s(eE7+ z3=J-J@&JTB=G~(;=x{qL6yE?_;G)nwQkM?l(6PUF*@GlzH41}5Uf~4p0-N@1bhg#L zUc`Ns+78a_aJo`JOtZG~_z_(mLfnKr+Fts0Ax8O)eZDC60FyG3&}{QiC5NBHNMW;? za#O`Xa?z9~FkT&w7is{AX4~eDlXW$X#t2h1!)$1pZl%Jx_rsvBZzT*3?w2m6!#qoR z*1M`Q6Es5hHrx0@*jEB=F=yK}Y8#3;1)BxzwYFUB{4ZOcY(?h#*QmH>`5Ikg8S9{G z$YE(gYw4N-{k2FOL|nE;qo&#~aAx}1P1n^BsA+ECs@I`tau+%&@-zPMMrJvVtXq?s z_07J_TRzxuK6^(fHmFjs@JrV6jh5gK_Zvns4lESY$jg_3)MEd#5pN0i1wGLHbON>& zTZyB545xau`$n!Yn+k&?{Z@iefaMM*Ur`J!Jz1-K*OfYlJZe`t@FP@4tykt?oml$Y zLnBImokJd(uG#7tDH2QYF|Je2`4nA@mtTn=PMGaFI8Pk}Lv~ zHt&I6h6-McPA!Xqw5iNZp$-}lPu#U`aAPVBQPbJ7dXmx`=`7{Zg{?>dU@P#QH|!rq zpp7DO_h)(QC&vp`ZIBt;^0T8jg`v}G>^>GR6Pr8l9QBQIEh7N(Ec6@fZ1ki^FVvrl zjdf*#NXjElPi5;g81M8{2iI#gl(olJ$DZcnl2akVjs#})%>b~Pp?BE;Unpec@>SjM zTV_>q)~%CN&?sEo+}GyWWo4!2X~rLmp+crfLY~^rXG;~F0p|N7o>xvmy!TPw;VistYeb#Ti6T^qVBXO2(} z`Se`t^`zO2kI(|0k$X{0PI&grxs8{IpzhNO=Yq`jPJIO*zX%oGEN{a6j-$rjmE_uJ?t%+g&t^mH*$B^Ye&~1*DXaL zh)Sr}uaqS#Q<@TPBqc>c^mD$5MyTXz0M0;Y^&05(^3DSQ@#ZuQmOnjtn( z%uF9oMl^|6fv57+OpwdT5Mz8R-N!;F<73SucAkY*O98+%I0BVIsw%_^IyjidZNx5! zMSpi!XWONpM81)v>ZC-v-BuJN)X_5tcAWH!;>rJ-3ZTAlIerGb-%D;_d&!*;;c0?; zyaeFGcK04-ww`8119*N4oPzdxi7Tt@3BADNgvPd6E(T)T5W2Mwl4xh6@rT~Xxt+MBh^%$&-4&U4r`yE~4 z>v`1k6bmLfU>wnfnaCNCqsRSX^D;|wwOw#zIfBYkwV8067`WF*9`G$aLdW#HW)@*AU3)m{0;a2ST{hsf4XaeVK!ND3rAjbBY(eAAPTG>66MM!crkbUA;{hkSNefbh$BqIarEQW!4J($(v`KAaxEj%}9 z;O$`JwV0bjiu6VTWd-F8o6UvMT-jYV_BKT>_b@x@*gyBR0uKb!8i`{x#{3`VDxWp0 zzfoS*Bc48-KVA1+BV`}EAx4ZHBKk+G}tEPXSC&A zWxHoXZ&|R_7QkJV^GRbVLiVb$+6;~@Cw`i$k1Fg{iRVV!sQn>xxR$iBYu`lim&M0p z%&xvHu%1ck?9#`5w|wxcqb=qd*9pS`%lAW9srr!(%RlBPNlh-8fF_B4`#F1cD-QX}y@uoDZg-l91L*&8H&oi;No6*vu zPG+V3(01&iW3-#lGjpSh%t($IE-LVadv9KA1n?@%g19>Qq^pzDTG~RKy2IrhCJq!! zyPjdD3;MptJh@_ofZbilf8&jZCp(;_OYCjC0bSBgcBb<><^ZT9C@=Btt2*E{?8h== zCGTSOI2Z2^Z-%n(ckLj77W9wc2x7DlRD?g?@V z;<1qjV?rdt0#N!}!nZA|Xoa@k(Trbu4SdP2bgW-5{o$%Fr>H@CE@RjR<&iMWW?_l5 zq%@Zj-lp{RqBpy+US=g@$OZYHB;RI=5bM&6RI#>^A+YQLLt@=}FvZS(8bm+BK7o>i z#sK(TP@lMxi$0(k<`!vOMa85Vvg}Nf0lAF?3O=R^X%PvG{jF{#Na^c7w)sUX+ZWLR z+XdN5D0e4p^)W5^M0W|{s>-9dF~NQ)B6r`a?doMM+mi8KEGrEZS7%B}JQ;SyL0j_0 z9aHVdrH5e(b={RtQa@+;@oM=~H|3}xpP6xyN!ko6cJL==OntFpBXUZx~)}=?>UhJHqZNRIoiFSxQRVSEW;6>tGGDW0%1%&-&5@U z{dEH2Z}xaySS1bNdy*AxADi-b<&5V*xrXv04}Lx3#MKNrX0fPmjlk>(9R!BH?WswN zs$3J*O5|MKR%xyb$zAMHvFbijnxTEBjrORdgq8Z=*|(+()nf3p#;)+g>l4sdw~-qRQU>Fi**-q^!UrS^41CI;Ln3d^tFHPf2 zPuMg*ko$$tAm6B`N(N%tz$i|!chJ(V05@hu49X1fV$H&cJTg;;PdGKs$!bO=pXG2S zwLSH|G4o?<*0u8iG+}pQccY&qTv-n9gLiFyAo!^acn4NI-Y7QAzcy3=>jonLn_}0s z?*dU|At9Ud0!Nh($t078MBDG0+(kxf({JUmVG9NEPWTLC4-m!VLD4Mlez@L|v-kl< zOzUN{`a%K}E_yh9eG#;EqD;V2*fxaU^y9YXtxzj(xF&BsRVDQs_?A`jl~4R+XW*5# zh|fzq-c1Fe84N4pXt?yx>okL2PxT(|MdgX7ncyAmE;U5@SifDw>nIu{oG!(mG@bG6 z=FJ`Qrk0}lht$wQS=4SZYOEed4;#g|M$})ysaGrw`l#_}wQJqIL!-x^GRC^g{ zLzIe7qP0WRlzI5=d)QfY=Xz!`Tbn1ZTuashNc`L#X7lN60YJ3x?vJ zdo|t15lFcjr;-iB|LZxTa}PxXCIA8kibdS7-@JO^#tx8}xO9^(s8p^i>!*BH1gW-= zdMOS*O%!WQ^3Jzjeyq#NLwfdu^SJM|SODL!y78_h0(*zuDql9&6vIR2ALA~fu((Wf zja0eJA*}+-XxGc$Sg!v%=-HBj-B?b$qHx$NrT*<-{M$3G@BumfcE{hFG3791Gy6V* zZKuZou@^)XqzI_W7lUs6n3E5zFqe%U{PlGN(%@1zSZgG)V$x^SsR5;`bKwEsjO#xHn(Le zgE#L+R;5X-iMo8A?fONeU-KQCPy%dv&AKJl&~R;OgMFv`+^wYSj@{(k*s~a4(gc*w z8Ys2s4k_RaUx^0>M?!4~@0;y7CO(g&0?>!Z@b9e3%o+DOf`T{wO8Hb=kU96`nH_(E zUgbCQ`XR}e`9_{b7e>h3JCG(!_fbg z`33uN^>V*p(Cycb^lJ24 zmuda8MP<*0P9sneunr8S0|8<`5#A;c4O@2~ZS;dV!FRbh%gOndE5FzJ zsU*c>v`sqL#5-2tGP!2&trMdB$q``@UC99kvPr_lYqgiIA1cej0V9 zy-+k+xYtT|iE;|V)i+67;GtpEd7_jEc@$Wg^Q((4$Z0rK`N3Pp4DE4y#0Uh_X8{++sY= zfZDt`7!(7mbdb#@icUj#T?{J(> zr(6*U6@5Inx_u?v=yJDX7bUB^m_spX_QeJL0zV2h8aUEhSoC^mSRQrksgF6|DcjAP zskjDWzsVpPR+c5s#b5uzRzI-H{)hTF`9rz>l9}MJAvJYv6>*FCMz}mou})%Z4@c(Q zj-af>(zWd2QhxbFmkEB|yccI?PaYncKFWv|e!E#oKXjD1;FMAdP~}8~S*v{cdm-WZ z55F`oyCAT7Jx*L#Kz8Dxb91=~)|cwkzuE9_1^cU6g$aY>C-~ZpLq7JkWrjdlnE>C9 zG5bFJfV%zksdCz*Sfp2GzxU%NY%m1&;8DxZuMqMW$K_+H^HYv}CfV+?cX5u2MeTk# z8xxHoiMI@=Uv~XXUPoj7j(=60cF0=*4dv0Fn)zgBtWl%LJu{#O)hyxT16)6-NrjN|73x`Hms0%gCx83 zi75L;P|B?wY^`KFxfoi>X5yj$G5+-T_~!RW@AsJZKhXd0OaEXD@%z^3597+;_WB36 z%k2OFA2&Py)J)6aIs%^{+g^$j{)>S4bGTlS+O2y#E18>a+*i3qnvaNaAiTx4HiGB)_StWINg7S7}b?6<j^ue>qs#o3nu3zM?YXWVo{ zjVftols;2G8@g|w@69NsmkbA6(g146?H*P?{n)jg*LVps$-B(1?A&EhvrEuEJ^N-xQ9^VEt*pQ67F(NR+lIY+nbqWjStJ6EDQwqBEU7l7JKOi|o=qe$gB z=LI^o(AvPerZy>Z$-L0V8n9x&5u9H)Hiold-5IzyPvhMS!NQJX32(LM1#77+l$pDb zl{-D)1vvM@yXGW<;bJ#z!QlA_WgQnp1oH+g((c#zMl6$k&k}*-Bc{KFLIDN4J;uZ^ zVGDW-kki(gY0AAe#zjLb%6lwszSe@18*L z<@$MxlQ3uA7J@HcBIXHLZIf1=Qg-%Q zzWYHgzU-R;e8Cf2@;aN;S%O>m8W%^(d>?j(m~~8&G)emY=zx>MeAwGG=?C=KZd}%K zb_vZd#qfdRSq*x7nJ0`YbMp}gB_vFJwUYBn>jwT&%E$z$5W6(M(H$zAJV1v4us+d8 z8H|{QwtxL|+cpCjZ7L?Ap=?CkSWhyb?hH^o)2-fmy7^>fFh_st74BX(uVeD{l!v>H znpa;Av?Tk@>;jasHdRF*);$iUdU$Pm?KAI2ZsKZz_S2Dnc<*=|} z8)lRiaKoWjhK*9bnDR0vr1AIgwJ6h`1o z_#`Z+O^I<~GDbp=*hnw$C_gj{8UW}Sz5``bgUqghcZvzrD56un+Y4GpDumOyi>21= zyIt4YPAh@DcSd+s8`sqMi3>2>RW$f0=ETmWDrsi-qU8zX)mqD|iJed@G0h z){1OEK*G&QM!%!A#KWQF#Vc#(&E+%ZP6h{9}|Eg~6i?W{(&S`Q#> zSK%aCohasH#fTH5!-1&Aj6g>qnPlpjXx!L5hI^IkAa0u%$sAIEEC3FV$Ztf5KEo8N zZa}?ElPD*~jEZ*7KRFaodxM3m>mu+T1|2=4ri^iZYC?Himb0m>u`STxtkeHT?mBQ# zdx1}Eg(uWmV8*j67AUl*y}~`N#9YDoe8)mn=EC8z^(Qf-6>p(r_QYAnI!TkY%yc!? zFC@O60HRFLjL&J!WOznfnOBx#_(^c-5=7cI&;6jASiW;aojnHWr=c&c)8uF*fnht_ zV9-&zpo)j~AbJ#r9)U4TN5nmbON$mO4-Wol6j5_Y$j)ok27q?)OYQYTV+;Pvt_Xou zZZ3caGcV1Qn{Xc(f~hm@DDqj2k1maj8>ZGyIwjp}^$HQ@M=A^u@jyxP(qTe#uEADt zU#9A^1?_j~#9@J*VSqpnZR4+YL2xYNUD}f~b9YFPVU+z8QK!f|F5#~LFa{l`0^nE4A+MMSu6hqb)c-ik z7j3nW!$Y!O@LF(F>mzrPUDM`5wE-C|Shpa?9xwxw0QOvU_7RqS9re&16@BFEv$A_8 zx_dJ}w{Gg>?;OrXlVfdVAeCWVN4-e`A~aS&>VQwfj5Z@ktNjJt@F=N#>tQx^14A$L ze1<@0p>=0hFx?+Csb}5Z7x(YT8f>_29yIKEZ&Q57#eKmU50gzhWvPU;|A~uDV#QGg zs7RgS9iAXOX#Hv}5onX5zwMYCM>Lg)Y+O+ZaC~;y!rEi%*7B=%way2 zM*YtRYDfa(;)$)mf=rnyyKVzc`D0t(wu?52;L2~Ud8$Mrpr&6kzbI|lzlUszTZ5S^ zHQg1Mp00kOBPcT}`98xG4G0H|^=pD$Aa_Xat`Fd*>e&iXsL+G}Y16C*0-Vqk`bZp5 zivcJqse;0gti74dIYaYO9wA=chMtRMI+PaYSlhr#%#3E93A|WkVwN-m2pAZzm7h1p z2J1Y*HT!j{z5dI9DkRM*ID37hATubEvYaeV;qARQ1N_K{x31*4O3-~bnG6^^fh0x` z3P~v!OHK}k{+s);!7@yqcl-8Wiez@5@=sl!KGkSBrZc~nOw#$?EjA`DXsqP&rU`i9 zU<9Q!RsQ3#rfHAII4BX4$>m|3_CUryQvc&UsL|*OKdmfZF-17-CyFxXQKX!zp`)`7 zp)5CTl6V1ZQkmV)K5Hx7Fg%X4(rsJsFB@q$UgUgDT)mg`nPWBCgNKO~M^ZnB>%39U z18a0K25}Bg};0&cy!RNO;>!12Qy*>-k|7(Pb=lwQ@C*a zO??P2gu#TsaZ0)DN=)L6#P368fCE0nSI|sa zRHgf60oSb*!)xKpmFwiSW^u^q=1()vHLhpuT!0`6tIWsQv|lqbks#hJ!?JDcqSunA>r>D&CxJSup6T{$s>!RH~hcjMzX_CG1hs2F4RCD<#@#V4F+umv`@*le6|a7e9SGI;--ou@RL&kmfQ_*^}Y3`r@9gF|K$;Q#Qub zFq|`xBhaXj)j2GC>gA-mG0{GtQJfz$R=&o`{dCRV1ob*7-Ct7I+7lE`7+20K1BUN4 zd3$e*V?fopLAtk=ndo36Gt$EL*HgPZ&sgDiD!OIrD^u^>LUzNV$gj#zDnMTri#(E~ zlFEdX?S&`D0==I{H4*kL6TzWL8eaBao~&ricpn=p{9JrWLf`B5MB|U7oIS*E+nryN zr>6s-tXk^F{k9$B3BUeKkWznn$Gc>#(1OE;e-V(~jm3)=VBcuY1tNmU@ya1hG5Vs? zGbN+YdcA;OxO{53a|1-$I$-fOvAJy=vty#;M>c{asUb*A>a@_mQv; zwGMbv3&S>*t}<>DQcv9$dpb7e9cbcjUg_3Oc&;m~W`2q^F$piBI!60>9&No&r?&mMmm6r%HztD@`Sb|jn|aKqG1rXvL6h)<9X zJz{u?ETth61ZHlzg>#FysB(4URJpWkOo-BK^|;<^Ow76G3C04h!alR)1ErJ90135( zv_$bu1vwG4y$pCW)S#X8QhRy}r`J%+laiUKyL_ZecAreJ z>2^sG6~_9Z9{!BQ#VgM+lIYs`Sq|7(C}}Nlf=3Z#@y^jVkQaD{SfwyHNtsA*!Q{n@ zf5YGe{q8D%w7x9+=4_?=)TbE_mc=Li(O`%YrON!y^fDi%%J6meZu0b3FkmP*%UAI$ zK#At|XHGtT$Clnx`Lz;qJgNB_AwQpq-4x~(%t|1=oOW|-EYf_Yqi!03qbZ7n5!&KjvX7IJYie&{1$Ju9)ZBdCSLoWB;e!Xuv;135UT z*xaII(Z;v5l=GF*Yj+9*aeqZ7%55NXnkS3vYm*;*Hj5ooiLH8(9O5{nCQ1e53^s4> zmGjYlT$<1dLzL}wkbDU7#p{PBNwj24&|79lPD2-xr5ILobs+YrSvN<63rFPiShlK8 zubBq`+Egf=qGO!ukdpH(12DzH#0%65Z-M7qm8Lm}mcf3RQe=)ZkwiZ?a?>@NKp~tC zPbRfc%WhnL1%CuC9&o%I6wbU~Ur^9+E#IA)pPg&9onj;; zLqKz^WuqL)=YSEnlR}vqbs{QrbE9h{^aiH`ck6*;DKB`XQ zAntx`^RT$-L4Ix{XCdCLWe&Q;6|NE}ZUo_=Y??p|Uxg9&#V*XT^$3DNhi#^BnAPCmXBve6a*GI4QB@_LDvBz?*9^raHg(*?RcXq z^#Tvr*lDzw+0WaTjaezwMWrbC&4$$TS=h+(y);qXK6NAB_mS#C+lwJyYt%MOq9><) zwZ%>9^NAXk(}tBu0zYx$0?B%FnzgKnGJcnfRgWNnVo;emU8j>!B~=bA$!g$y6sUG2 zsA!yE5*ptv?~#m{dL)+O1n)sFdh;&7889Gw(cq`EnEpI@ub$yHe(T}2t{N5VFaySO zc#oJGpj^*lnw7DbuuuNe>t-!6swJ(y&QA&Ud5%BM-K|labmT-f7I3EK88 z7>RDp!saxKihP3|A~908=BDdZ{^FReTEUp+v-w^;2I*aRRk*Etr?|{Sfc+Uj_Kb<1 zR5o;^MqB79J&HgHG&Bplx5@f48y!SHF_ZcuPsDInt{8JptVxllgqU2D+)B#?(>BxF ze6SbY)Ld#oKELL=WER$fXtfk~HUU1@M!7^$m@=1A;zg7dT|0Gmy?3cHGicQ5PEPMC z>Lu{!jm`=3!r4L8c9H^gi(4GeEJ?DVi8xca4uy4m0kaZqgk%0$ermD7d<_dq*<+V3 zKZ6kY;WciOQaVCms?%V~o*0Ww3GASQ^IS2??YCcdj;^w7&_Lw62zV7h&$k>bGLy&@ zR5=Vh=<+RDA|x4OuSzzziH04PeUz66L0%t2UDGIy66BH;J)zS|SO%0yFNvRdATt33 z?X!7-G7?0XB_rP<-7|UycHTH1HMJTfNcZ;DE+A5l3}HmzC;5|b@q^3Ed=w{m zR#WAKNrh9VF7u1+RpyNKQ!r<}eulDV1qv8CCTMGx*2lT(Ck5_#kH!@iD1jpZ*6 znL%zqjYVtTQDt0>h4ZG;qNTL@pZc}zpZm~X*kqsIZ1TS{#s6I=zu9D%C>P~yA7`k> zUGK2j6tF6ckuDur(a6>-Bev(0$|BBlO75G0x$;*C!BfsVq^fi=c#S1AH{C?%d;5Z{ zWKY#Yt;?ZrjjCQUgZI3YG&UxdHx;Dq z$rB~Wy!6V6+85;jGm~}f@<@2m?OJ5Z7xR1e+&iQn7m4igKkjbI=?MEB{LgF4)rdf5 zPkESH<{y-M&EI$%GMZaGxZyozezVEH+2r4B@^3czH=F$5#rFPhPv>uJvg$83IgSA6 z-_|3}mM$tJ%*S_Fvilb{`2%37w&|T=Q@ze+?(SfEc7#_FimO#D(oT*GxTkFzCAn4M zC93v?V~j4RrU;hxt+5TV{F!6UuyC`Xe zY1S!8+wL-{-U1wec98aox%>h!F_(oqMr7Ui+{$S|aJXLE-dMWd0-&e zWQDf5x%$Vd$RZjlN~T%s&Q5QgHJX;zS5@o$)5u7uagIOxwtT+9RyqGgX!+bG_iKbB zA|T?k^6!N$kY0JSO)%`22V@0Iqgr42tiS9O?uqf5BQ7tYP}Mz+ ze~@jxwq>|m@F1Ww3m`+7mL8gtJQ8|v|E~E@wYXBk+RO%(C`hEVVS<$UEz`Fph)Ac$7Rt_ zrC^)_#lBo#PT>dM9$;+7qyTX%a9nw7EgcpaAOdrw*gcEwqCmz+_?g=W|jqM^+AJSDI6 z2SzqIL=!9+K;5C5Nv3C>D@b2yyocQSwM=l=fnB)xV$!FAIgaXi^SQ^nPnT}h;_3ZM zP^@pEg9BwlVrGb56=79C_8aZl^YpNl@g)fkwG3e~dD8bCXqLPq_;9 zMINEik4LIB#8*q6j%N%m5^?xmY}e+=5{Z*AfXkn^*>LInOHTiq<%`mvxbSjQj1J2LhT z1EM9!LUnt@W#hyc)eLLs`+E&J6e;O#rhRD*iHY-Sm#KMPN3`FO}v^HrfePvDZ#aC&Z zyFlym2bN#hourKD$J^Ib)6&nw25cf)6bCmgS$Y2K$9jSLz~7wmy$1UT{0w8e!+R6x z%KhAdEtHYI_@i&iwiY1J2^Rc(uk`4q%bXlM>tMYe@2V+idDUSSiXlkDSthnX(GvL_ zyPYfE6Z?it$hYun{JQrS2RIzZ-@0klpJt6b@LaeCQWc(@n4@$THBV%3T(i^V8HcoXCQ1LyLe)aJsVR{Nh_PisbGkXLGB z`nn9iv;i7A=-eF0Ac4J~_V4I#Yy55uzqRn&75w&uzdhmKc|zoFfRB&3RTvRw03J?b^C%m?iVDr!fRH-@K)>UMBm=j~a0wOkKX0rJmZap! zF!4OEtW)`$-`pcayqn2w-h=+yRpt@h15It_?G`WqB48dAoe@`Jv}fY0%)2~bk6o47 zvJzjBlkiN-Yj}WhtO#4EovpLLz_&f~9(e1Aub(6AblR)5JBs2NYpx%eMxVmvN}OoHma#qO9}15%f(8NIV)sv1b>@|;72s98zEGw* zy-a15YvkwSe9L_Ip#$t%hlA9o9y0#L%VoeiagZCUR}@vMOby=Nr6Q_Wh*q(HAaTq} zyeDmK(#K5ukTDu^4-DAPDOSGVCrus1P_O0A5hVh%U-v}%=qc&RCcm&~z?<%UAY~Og zQd4XXG@dS>O;!g#9Kc1!sOh}T_^hf)w8QHga-~Rn+V3ATp`w?7c6lSp+yqX zADo88J8!-V=xn}}W{T=PVEcF0flUp2fn;O`Oy4M2A_saO!gOcqzfi4CtTOMnBK2Sp z{M6Vrw#?7X&stVk^0gmvkNr_Jxsa}m&s4>jf;Y3pCe#D}YnF@1Yant~3J)LxC(EN@ zJ|_ebjoGbV2!P--r|e^ElN1iY@}ycu?`$HM%7EpJ2p2_9QE1K&F(2N=xZSn-CnLO2 zSYW=vQ{~WN#UAD7a&;!ygKi&HWLE?=&YMII6*Kdmgkxk$Gn%`H`wRZOD0}i98pT>H z#?))aOX6M#ggn;I=t(Gt6?#nfVQoW=_!RVU6ky$9zx;h`sBKnj`oujp+1YDlf3SN0 zU3K)g&E$ZCXZgYQGcfahfY(+8{L#fBj%rhubA{*T z<xoa{ zzhd*l!-|}Wu)LV`gu2#|`cy5SF(;sef6JgxUoFn2zNm733pMbslP0q#n**KjsKVjX zgvAn1jj%Nujp;mGDq{VzHAUXgFkpa!g3o0A3)H$)@%TUNy#;U_OPX*iw3wNh87*eC zn3s~ml=)?LNA;;we2;xa z#MgWn81lZ>6@7qADV!Iz1pSF8Vc;H$c`A7jCCWPS$>C?0QPnIQD;tYnpIBP~bX(zY ze3C{dWSr$SxLw9z$Rf`rE8i*`Orr8ueq!JO6qAT0cM3_0~_PsEBobHL))OWqdS}3g7Ph;RQ z<~U42zH&OlREzp8nS-+K%d^)gw)2(2^&-f;e2NJVnAV)bEQliU!)mrQ{)?&p| zCGUV6-tS_UJq%cO$N0Q(A}DUQ`Vez`F)P2l>W-cSA4B4d!7ObHj&hx+b3@%qjvEIJ z-nX$Y+y%XF&I=72y6X(+o-82JRzJQx_ij|2^=xanxA_th6}OZU9x!SQBuymMrA$`1 z@kPB_%e-r65)7$&w;*UG$BrmF~Kn3!^WUFm++g3i@BoVos)M7JyCIdSzHk;7{xbEgjcu(MccT_K7 zx@!d9-F9Mk)#7|kTi7WWPdHdAWd?P>I;1LS#M-z=h_q_gsrtHtlsc7qK^a1{D-)|p z6!ijqa~fQ3xtkDV_azE|DvjxP1~M6Lj<;xXMZEc{A55|f<8S8b1h7y%xXk6I!E-wd z%8uX|Y!s6jBB%KRlh-~mA&fYH+%Y&>Qf4MSZCuf4H_SDZ56Abj&f zRZI%Ky}I9;t{|CAXY(0ro4Iq+05t*Adal@%Syry3kC{&9R2?_BzkTZ2cQ4`4%4gc= z1Awcx-ETG;ZuEZf%GwpKVyLpYifu9zDhSG}Snf5p6da3zm>snMm~#!Z2wc8&SUlgH zZI>Q|lTcJk;Wz7FI};UKT)nS?nJf^zzb~V=r4h6yf11lIJk;W}IPVDMK-D-quJ+8= zBuStQQmOm7w@VVdtgE!VrzdAastZjAy-ak^WU20QngUW!Xa$4 z%7WgRI^!%2Y{_({P_V56+5-(p8iOKt&R1AXXtraZ)KKx@i!8?_CHaB~&1w+G_Yx67Q4w-6fEXIK}nAFf#718+%SqwiT8{SRy00C~(2jNp( zBBq03z4aW_f_C zVEy}N-9#Z(U4u~Qw@x}${Bq*A+K12Uk$0WkW$GmdNLUEiOzQaL{GAsHbao{>sL~Sf zvD!J>Ur`5JYx>-}E>zYIx(6YwY-T+6>5GtiQWPNdBq;*_Py_{~5MPmT_CmPqNzv64 zoW+>9r4sa)X97D%Bl95WMzpw^giv8o5ATK8F}5u zbr<yB(etjT_sE+p*4^F>T|H3ylWdu_!D;@cv#$6W~HeX{Hj zD&yYN=P+xGkk^vA=%PZAgm)dtp!!*$Qm34q^-&HpD89|d&scsq$6LpPCKqk_71#-) z@I`jJbexveXGAdtH64kVpdYeO6)S{$%szGzIFOt3(S*xG%)9Q&G z$q+m8!;U{o!1pEho*JPO_W7#S?IlwlsCe*-6pWjQ^ZZPB_5u=iY8U&XTuV~IIsW2r z*9$e}G#A=m>`zeZw+R~4cg;3U%9+Clv+AfQOeoI3il={hE2dfHjg*nOCiuq<$d%8aC>6MVJJI8(Z+MoI5ndIIVbv>X4m6=77`P}x{I;3_ zdJ7iK!9Ty)1ACW*kyIfffjDLRhcfz~wS@*V=YV5NgW=9R}BRH2}SYn>EknBlu3&j)H1z^@79!XUz&mUnX zY{T`}?Z3MdMJ?|BXkewW=`Nh!{4&6Pue~C=WNY9CbG^HTYI-nOQQ%GKbX;>2qmKX| z9*4<6gdrE9Smd#Rmo6xB@Dmvxggwq} z2{)XJ*%WnjZWG31{Tq{3XNxXPSkI}p2H6NEbvUdeZR5gF@Ju8(X?Wbh!cTm!Bv<-8 z@2Tn)C!Ul}1^KQ0MiM+m&=)sgXD%lMr|J-#Ho;jVdVIcA`4~f_;lkkVO0&gcv74{k zwdbv9gh~4kF70~2F~rK)^?ZR4BF`fbB=?ccD&y0YKC_m-`i#7Gj9aB563~aNIA@${ zTH$cPkIv2Bv~+IgQB1;oVUmq`KbJvN_c26kVsKGkl#HDVk>T7fYtcZsCUI%5+#y{V6<_$IT465u0QLaq<{YCe)?9#g%hAwtMdybsAY zaGu+Nex&Kmez)mut-XC@`AD;y?U+{48$6A-7WMMGWCBw{H!U=N) zjn3OicyGCZefU17RP6;vy@MX!W>5IAzx^)g17M4I;h92JBMHE#uHJ-vyrv_Tb}=#r zBP15z3?m#BLdqGY@4ogVz>uWoG?P6bdl}grvSirMj^X{Zg_Srec7T1*=PqO8XW+ zcC(>q*v@5PwAdYYFVdLm2=#$1`H`*E;~X!j3OIF=RvN9abhsO>+CQ^0rR$))e{Qr& z)>&;_$G{+SK3l3Wh)le^sKQ}DU;qUC_;+!@|KA?x7lr(bLSCLDAn#^rFN>F8!J5kp zu(hPUr$~5GJ~x(+zfA<7N;Z_w<|q1kG$M2Y+XSy9J(>?Y|0QI-%4Vjd{2E=3oiv=| z>zA0w)ims~7SwRWo*MQ!@(Vrr4_nxKT(q4tNh4Vt0h%8J&?f-Y@&~+HaeQ9Tp~wS< zKkCoqh~`dgMzfvCR1bL13S^S=Y6MazvP7&|qmw!bJXRn`G&{}Xis{^&;WugBULXMt zir37rPVV%|Aieo@H3DCA!h@-GVce>*+=?>hfKQ^?rF0038Lf25G}^QrJ{ zpTu<$Ah+FKnDpD{HXv$C=`lFEEfCw zz=K6dJ7vq@QV1{a=Q(}W9SgCX2go9|{+aHjUbh;r<}!j{1n`qPZgx5-a(}*Y_bi~-6adN>oa*%wkcD-_{dx4A{WQSpbs8xC2ay17RU&mp01Qp?(& zB;q1xMyS8~p+hXagyw}k?t7xyAOApa@TUZ8YPebBayjyo8(tWLM5z3G@W3)%5R;|(R5C3~$h`+iSME<%vI^~yCpWNaGz$+yRtu{?NDdh&G zAD!Vr0hBkg>_n6YY#T5WGT&xUUN5>>rQPm+f4YPs9FD5?B%7Y+Vsef!jR(m{G8AsS ze@L3CU_U|bx1};f9$H1wYYUHe#fE7HKrOz8MbcbsV1HB+G+huw$IhvP;pzL@& zTP&T)@%KRV|1Ou5$2 z=I=DsWVCXmuzTr+{cRSbD{i)D7_*jl2A-#&X4@ z+)$mCkI>x6+^mfOWL7rV>dK_RfhV4g1vgacl~;Sk9j!_C!$aLv%YU*d;Q;yX!}KqM zR$}~_W=+lJlfmRvt7Mj2Szvs&nmVYPY1wFYKlMGD%~N4k-uVY|-JDkEPLg|g)0_OH zobwUbKPT}0>F?9|SMBF4^Q%FAHNmee_%#rI`^NEWF8I|1_a}S+KRyn9|N67f|0n|5 zhov2B1xwZ#qINn#+9XhR%j6GCqk+u%&lo(;MWddFh@}6~@rTU9U?a zm)U^>uzU*FDv$g0EL9Vj03YPdC7?DV?y-uZC7>k;+Snr) zBkgum0sd4C9;OMd18@x)tMpKB$xpa0$I8e_V(<6@P(UCE`!=7C+k&u}65Hqu4!@T| zj;ajPKq*~9YHK@xduccMk-j_pG9z&WGF3+lWX1>5wxrR^LzvPa1_#<0$naFISP2_* zWAQq~jsw$|GUaj1@%a(HEblu>d|=ZAwv@Vk=xlL;Uzj^dtot+WcYkb1077{V%*zk6 zCn?E~Y%Gs-LeD*whgyfCDm-4_x41pINK^_viuo_3S}kp^n2yR`;YV4~b^DnMF~+HH z$WoR#6bhg1tbmJU72UPkz?Un5eO`4cgT@M+8JHxrI`Yu8CAz-DxSzifv6!BcOnjW4 zc&AFkrs)!h_ANg?5IdjuXtpMaItpD!ss*2d(O6Xc(6ZnhR}BC}hz&0!!wV|!f)%3A z0z%tcCi=uCU=rTtzwjx0e)B~byHWIX!|;ipeDpmJY2Zdgsg(YJNFq`pfUfp(e^P(M z0#*r^n`4JV)=MfLF_UZ)&>6-PNnom=jMuG=3O-Z9e?BkuNTl;aVX)uHL6>?BSV}|a z`*eha)U_;V^+dX+QiYs1UmBwJ;~s}dZFxtHO<-}Ssg5NjmLD7-55tfCL{9>c$a5a^ z*=%B4*jWghfj8F?h=$;pEZpk7#2Lp=E{71ZwTSeeYIt~mlmm$SlzI4Zvve}x=WagE z7?r9Nx*GwAJYK|PA!cIk&z5zO)O9=8c4(kMyY1n?o_N(d)pC}cgrLA^1(}8Ou%CQ2 zMtT@fu|urr6XH2u>S21f;0Td-Y5Jz_h9b5fjWFEPPPQ_?zNZ$T;EK81UBUn<+A{Y= zth`PHE?snwLcJ=fy9KcC)e4Bu`&+s$OG14J6bg9&l#F=IGr{WLN%lx_9Z*sg~y`{Qv*(HU(@B-2R%GI_er({lE0TwYS4+jP5r zp7Da=Y?44IHspA-}@--QJhJU4aF#rT81{NLQ<<)A)ACl0k7`HIdHpAM%rd& z0+Nmq`~rB1FInH5eSfU0g&Ya<^=FrBfy#c~@?XpRk^7fP0N#{HYpf=e9u5-|EX`2s zO_;<+fTd>Y08jTw!*~Jzv5BMfETJ}w8EUEUu_9WIrfT+hbQmOE#XZhcWr4e_-E!X* zBAUz9t22q>W5n$3wi3p5hX(UDh}ZT3SY9+YY$sK(yVSiKZkfKd7G3Qf@^IOq5-LK$ zC-^c#A7RIz_-lOrNsZd3?)`N-BWsXW%}luS1}x zt(25J8EZi({-KEpQIW0qBJr;}HJ06BUmLsiR=R+NA*GPfgu^_&3x+k2iOGrsG6FR0 z;K%jo$DYCkMGAF>r=*T2M?2s@wS}xR%0vHqoX$1T1?s9|Yh-OeGo?_$#DfHQQ za)W0(@T8x-IJfUE|Fa@E^b?%3835qP(kG7f4z8Zq+Dq^zlNX*8&P*XV4LW3D0Z(L& zYVkp^-2f}pc_2<8sk)lWlxk2W0Z#)n&lExh0E!V0M@P~Hm}12T-7u+@1)W**BO(Ex zBr>y{j+LyT;FTC#{37obeGtp7+_JYfI?l0@q^Sd7sny;kow^k4^?>(BU4~w2yK{w0 z%w{L^_MW`BU|hI`=~zBLi!>%G?(M0^@RwD)x5x2%$~z(ZNLL>8+B_#qD#9oK2sZ-;AhhP*DaV36 zR3q|EN+b}@9>3a}!R;9eJ7jxz1q*mGPCnV)AHnr^UGr{ zqtqxI*=rHs;IlH-K@yl$!MlrEhH_M%sB1=N#Dj5sOqwzlC6|3-&fwbLV5;IMoVmIy zOhQ=t+!v7>+uB67>t-t$^}z|F&~0;IW8WQ_R_T>X8ebcOYN)M!XV5=GoHf)$hZ$q{ z%&vRBPUC3-ATzq(Y_jy!C=}uh+O4_#Wnuds+VAIvSmIhBO${}aDA-SEr>iYghLeM} zQXi5GGw1<;Z=TU+&fB`m2MZL|XW5t#$`0os9+h*PUZEm63*#-< z%rzliiUl4u(oe4n)%_&_-*sJ4YE^4u0bOj-By-{Mv2P*-Vm~=8=DEx=CL$HCVPIOv z|0C-XbHolyoe9cCD-geW4h3kd)L3xE|6L#l(}~hg8^C+fZ`0_&D`Mgx!@#dO?A;(% zD3F6Lo#zp)=b6-ewOI?T9x>XvB|lT`O#Di}-8wNnTwzYerWO zn}(9ymkLqU7-j2dMJ_&RU#Vrm&?+Wo9I(TwL9gg|SuFJkjt7A#rY8?B`SPG63BU0` z|FgA%D0{`rwoQ{1%Q{0@9+C?jI95_2dgl;)90Jxc%Bbl%gR0CHy!o(nJi9@hDh<{fh;i_m)ZG&qfz-I)AKuKwG0W$z5F5ta&l-&Wb$C_{Qz}UcSQY(QJf?7JAW8dt{~eF zyHX_^dL6v6d0Ndk@#vDVK2$7toU5Co2ldHmNo=Vpi3*h@2BW46ydYdD8!TD9A4O!_ z6OPP^`|Q#R3IDiYlZEx3S5zOp6oX%B@S^k_$*aZ8ibdYAAzVX&)atBjE#ed5PMaue zZAqmglA>^OptSST%9Enwt^|L1bP05u)=b_Ymoye9i~UP{GcrVFJqYChIBRvEgwVr0Rt4u|O-0s&>{fkrj7N@k1Pfg<&6>xK2ckg4&5cwngefZYwc@EJJd<*%=wM`a zZU^Q6FnA_WBrR?K9^N%$QRNq9i5PPyGObRIbnS55tG1Po*=}eXTJ?}L^i`jD2m^7W zZOvmrRo62k`HWCzRV_pP=nSHXoA+xR^juIfvL<8cLOHf|9_8JJa7~!#v`OMi{5$Pl zviuIM^K$qpZzixs zvQjo*FKV-Jlnz?g1I?=g|1U25FlnKk#MXUla&BuVr9eGo45Lxedi{6>m_R@8x`fSv zzq-mC$HwkM?}|~OkI&r-qv$AfeE^azu|@Kyns^P|PwdU$uds_)tk{a}y03H;J7L&r zTtoigW*eJ5BE|6AcPrE8AqEtE|kDZ+cqw6g}H8gCow=}Rp6{jTgWdU2ucjH zkoSBG4H+Ms72p}P2<}Qv{#GLB&A}EJ3(M8x9%5EV5-uHMLxsrU-hw>?V?oCcat;D5 zs^3Q^sk=>Rc?GLdRq9;${wty7VC4=!8e<+X_B*;YfS? zNMpsjiWH)SD?`CKU`m)vX`w;D%2$4PJTOgQLvhjlz$k0NZ?_bM0()OHHvtp|$!g5m zX10^)+c7Ykqd9D){q1fT@rlbEXC2w0U4`PXL$kKVckq5e0o0+beh-|;Z%vR@%5l#C z1>_X-el>Gy+1{4nc!r8x`kNAM;l-IvM?*|>o;$Fd?zV@4xrshJ?+2eh&*ziRA5&P& z#dt+8_R(q757x=yRzbx?MY-kw2Z0>0+LhIJ#t<_9o36E|ajky3uSBwHf^p^YENPL4 zKIDn3BZKNrZCM_$F_erKnU|3^6~|&emS4(|@u}9Qa|s(Ffx9M-{&Af!`ua6#PrCf4 z5t&|f$4%V4kn1!^QrsQ%umn<-rPD25$w4aw<-IuVdxDv6RspToyiAmxuur?-km@j9 zy;@MLJDf4E_}_;JW=b4kA`_OawqGjms7APs@^0hue)d&x;$U#bA-4x@`qrds6l;*oP5YNQvY%!T2L~J-*c%)Z2iCm>~O$B{=7p@hCV-bVA zSafz#UIc%(Vj`+N1K5kXh$l_-N%#e~GwYU7R|LW%t`Mb_4oIG7Z`DTP>9fvo<_zOA z>M5Duwf*h9IhnSFB75yS^QZOCW2g)z09sZLspzC9510osK%KP=-sN6nyv6bnYB4vL zqyX5PB5?Zgt00D;fYQ_${@uFOs??}Zp#Nb3JO=5Fo*O+1TU5+i^#Ney|pOF?VpeKv4gfiy8_jt3v_U*qp z{>D=0{9>v9?cnt1rGVJX@8h;1+`j!9{qY+8@k9q!_ci+K zr2zFhC${*bwDeO{bo#!O$v|YA*>mLufx<@0=R~PKNj6HmR!YlaV(HVogF#YhF!#t5 zCR4=Xckq>Q53C7|ZL2BK37wODzAsArA(k9IVqvvHtrL&DR&ANFP8e>#UlT2KhB?7YS-UbYWzR4)V~>>9l-x;bly&U{!vMVfA6#Qk$ucyBb{k?znS8M!gjlaow|F1{}6cPvHbdAh|T=z%gJa8Ced!~Z%j~OvA zaghIlT)C_r#Xe~Tb-cRA z;W!K5# zf^~QHre{`;+gP{HMqDPeSKh?k0L)K%Q|8`+Sy6f3@Yfl4QAt(#`Gm)Nfnq25+VrI@NjaxBoSns}TM_ z30ird(eNx34wfjqu_`JN>cK}p-agmXzNquQr9cx}nse_}0&-|S}C2h+ay{~sY$MBH;uHW&=*$@+O_%ke{MGDD0JCFOErWqa#-@`JZYlc^ywJ&StS8)fdu$e1qt^>z{HRYIl$F{WAK^G!&`@S;P}g0 z8(?}8=&Ma|%uKxw4YhyEUNHh-UF#R~Wk56RUQ-2|@eX!E%F5BWH!ci>47i?B^rp&YzU_91Lz4~DY^{bg+xw9O0?Jo&Y2 z#89F3?3e4zERB3qWAo@P+EX{f*sk^=wiXVXYqLL6&H!g^GK_e_m> zPrcffK?Bh)W7ln(((!+$)D36l!>u7IvQCGnQ^$s-e^2=1&_yHT{?0!P5Y#=nuDR61 zYaSI-2C##2*9;TN@X^pATCk8@gD+fZ)1d`i<>=f4oB5fUI4&`y{zmZz+(=4~O=;j~ z@XZ6uiW{P zS|B|M?5sFnWghhq-7DJR>y?a-0`to0glJII+zEbC%+nBY3pDi;zZ1*j$nBuTtU2eVYK6x40)D0e^jcw4P4t?rkLMPX#ot?*jI%WeWv-nzx_W^EdNgV zEz8(@%$=Nj4^)9?3)1DxzA=}lD!P@0Sm}+9v zv-Z(8pBlQpIzmY>(!Z+fhaf!vbKU=}NvG^7o)DbakM8d}^%9>dug|R0i9YO69OnJ%A%RR}J$(bm~KQ25*a$g-4`yJXP1E#CmM2?MoB6rjBBFkSl#P@sv*6ed)(%ceE zJclxcpO%q zDDiBLvW^-rdoj19DosQwNO>+fCUM5z(uno)vdDM@%r2MSN6AE)1RlCLR(fubrBN(F zjNG(3Ob7Mb$8hDH*!7FoDyPkd3%rkKN8a52rv^;C*L^AL%KF_7#f6hqY0f`Xb^w%4 zFX9}Eb+5U!>2sfIGD`6#LZm}7a~9DkG&(R^bbj_C-Mhj~d|>bMx|is*t(x({OcjUv zDe%jD8^TRW>vN~kHrMh+eJXFb)NBj9L*LD36J6$hq58E59WJ1tEs?G@sk5kHTqu*%DzS& zKr`ofAFziaB=bq3hfRQ8c4^*p3txXGFVd_a?d9>s(y6j$A?a_}ZqbuDl^!O={rMyz zt>ium)hQ1 z7iBkTJvie2{9{~mDNAEj_?#IK>DR`77Ex7TvC32^&xbw_3ILij-%}0(Ki=urA%_n= zCw*Qnp(a)y=z_1|l$J&t+rzit1m~R@&#UyGeBkf3BEDNJb>Q`Js~FJ!Wv3ZF$62uA zfCZ^-!}AuM&d&wjY^W-LQ9zF!-OI0@H=B4P`zww@y7F@d42ssMc>^iNh|#q+%0-a6 zXoAh-L?%*5;(0|A9DGA}cKoFhGP&B#_w{IS`gJ(^0|eHodv~repeD_2ruPzu?t4x? z$~N`gt_UtHwaZrH=VO=8Jkrd|uF2;NNkFIll!Y9pX0of&-qI5do8NC!j%p8(v0hHK z)(&Px`5aMJQmtmUWN&uyy>BaH_of<&ttcGVcnChhXxfYj3f(cSwT`UPp-;)DVn@v! z;?$=X9UmyX>FXkO%j$E7=-PO14ce3^t{wwCZJaMvUdbQE^%Q?tWzp6+sEv6aUC<=s ze77R^t1@VA)O0}|jaqL^;9IO&)1c)HHyX5F{eo|?^xHoDwYyDLh43tAzwNWLY`2OJ zQyMsRJz3!;*D_e%LGVJ%eq7C{`jFnusK5~AXTK#x)}7J1fCSESH4U|i1|I}_F=7Qq zh3%XU!Epyx@$JFNykZ$7i!)|ykkmIX9Dvg@Fe z4~#k=DyR1&&a_;d$cE~qhOwd(*#0sO;2;QZkt+XahuJmV_YSd_)aZf2Ldh2v@OVK` z(kyf^uQfrFOCy7mk6Qs746ZRJo}Xs%0ert=|1wv~;t)VEc)Opk5PY+(vMk8b9RU75 zZ2lf!8EwFqnp#XF3xBqO97ibI#E0}C>%#*^9Z@tG8wX+awDUB#DAu^BBDFhgj_)CI z8#&PI3C=g>Y~qR%sBja2*5QmrQ1C5#Ca9pqZqL}1JD0tRqZiFs8?l>hO{a-?4dimy~kl%fCA+=(G z$Z*IXOJ2KV2YM5DHk5xP2$o4sUdu7YZLl%r5*><+P{h!-^<{nJR=L&lTWY8h$`#&d*8s%1z#Sd>ACoktt7bN{w-n*)kIGZ}^_Ve+w zV22`8RawuSj%1u9do&ZMYDg$!6Qrt+v)ZCQqm+qS58>T=vp|E}aHDjtdH_%}ahK3s z$T_{oV_)>FV_D9N_KoCrv(hRa7M1{A@T4hf@`Xj{lP@aHN zZ{z-mv(69lyv@|&Dn$Vu_S4}5X(w3?j<4(5ut1+E>!`Rhmg^ui$7}V)`=Q#;u99{({=5gPosRIJ#hSF-2O;!1tvZJH0)gsT&!FLvw&>ed`|A@r z0(8;@;rxDc%ne08Ie+hVh?lLnba`D&<&8m_;X&MM2}^ij>7cxInE%U#=>6BE1r!6B zx&j(ebPapF<0a%(<~k6N)>+}7$+*`+^XlK1UOLdZds|!EQn3YLbBnT>_J~ebV6MUW z6Ac0sXG)b0j0-H6NTs5@>M+6Z8?S#4$V4OBFL1Y8QZS+>`cC#mHaB4dQq6)w6oO_O z0(W2D7&F2zJoR!v_x@0iC(ubTnC->Nj9SB1zs!hfPpC8X5LgWpn5XVWVil_de#3hA8T^b_d8D z291MOEN_Hk{41y5d`(o`B1No|A{+#kCbL#%g+q^XB)`HBTDiAX;g2DcN;@nTMo)TH z1M6bdfefW8d0awoExe5cfoWOi}y+B{_%Km|C_1}iGE z9e|!ckN9+e-WW1%cs$8yj;uymDx8&@P_{3G95L1;oZ=0Pwz~V$(YKY=R%Kz@9LS-Y zOCH-d_Q@2SE}(BjxK0e)9AzDi?d!L<3!2In-=L=5k$pl(q##5#`Wm7Ss-^`^C03DS z>PKnDqGbO5>bQv!L^8dI3ZVi;tPUD;5%i4_SIyA0+am14GOr{(9UcZS6EIUK zfmP4~NKfD7DxA%fh?gSwJ+1BZ)0#+A#=wXs7qH-=Z`#%lg1nS*9I#lZP^D0!FK6^? zY!-8k?g0iD*<1dW;uq)Qw8O1vqxQx0jI-(C1(&Db;)i7kd--|#%o;5uPO_j(n;fnF zQLcNusnYPo0!`g}867pnsyXVB_{yS36P>7$3mXSX)kBr*?qReMfKtgWKMn}Ji-LP^ zl^>wnDndZ?LekQ7x(H30_gLPs>8A&u&{M{|M(7YIl9%Aul%Q~C+0tXQ`ROn(hs$aD z`?SmgT<&ado2oB!(s@VA0(_#nF9DU^X1d{hpKM|6B#(-EYP9R#-|D}8YBE+yL?OR4 zgp{9j21wtz?~Kn8H;>YL`a)g?ZwP)ICIb;Ppizv_so9v-h^Z&8_~<5o*O@oo4IsQL ze3Q>J3Vn`;nslW7*z5!0)$;Wzu(n%T6|Al~fXcSU63w=!vqp%5GHF`9!>y=(QNo4g zdUjg@MRVrE#*Z7UN@Yui|8)KdY0CZH8{`hRUFV-xZt=xe4yNM#XoD%UgHZUs@a;Oz zcZWaudn&%fDvET)eOA7zg0lrtP$i5{5z9WTe-eMbIHLfSyp_m@V}w0%r;qck?+Lvu zYY!<^lr!$<)BJY38ELomvOzu?oRX}e8YTU%^BS2wMJatw|BhstRAys1nA7tWg$lKn z_lXn_8!cHg>*@sEfdfYpQA)|cPwP1Fl;7lqXmkSB3QR&jsg$!PQgz>!TrRT&PhpXW zB?=6kBTkH8N}cAH;yoX<0r6ONniK+&eK08&=L%`ieCh#LMb-kHhUszL(6K{5*A2dk z@k(RsU6FIv(M`gdMnTT;5U#ekTW0+Eqbg&udV^Sc@unc>Ka{;IJ9q{YFWr=p1 z&KErwi1izwI&=cYT2^o4p^GgG>e0PwcXY+s&087}sp82^*dprpjhxzypHr@B3!gjR z?9Tz-*U{6gE6dsfV7q_toMesmg)Jh6;>dUlmzgzLyr{+Kx;aQ$>ND8cX{Uwj>`3l3 z3Ku38<5pd%dJ$c2*$`4`#eZbjo5$%_ZLB_&wI2|#D&pB-@N^ds42+JDMSE|C++><# zS4j%CW7)oD>q!Q%+y2NA?sRs>`?z--#gRCPHy1(>@f{}dOEqv7B#jy#}#guZ=ExJiwYHP1XsTz^R5uD{tRH?QCNK< zvoJ@{7$gN1ac(U-AbOIz$QTbreVzAQVvC#aihU8_wjbOv{iZ3MULy?s3hV6p@VTpr zwBTd8Cof-0?X9cc1}6!f_&A`|hkX>uHDF(AXK7e)auX)JQW@Q1fi3ZH85n3IOc7D& z2g34TiX-v3eM8}3?dyd!FbZK&5jn*qLaI}tJqj~>@|jOoBqiIOKyp}0HBlUHq?!}8 z1^-N>ekQegxJSXP_j~R+Jv8biGSG>rh)yjuO`m^43Q-o9H-B`QCBxi2l?cuEC=Ftr zJgI|h2|vsdMY>H<=^tg#EhzS0w8ENUEj-DD-((7h%41c?X7-eZ?|yC|mjy>dZ{Yvx z?^OAoid@uL@E)#{B(@V~s3Jk9Gx{p1R~!+{jED~j3zApN#{4F%0VyNVfH~9@g-;bY zfn;056D;2ulY_?fQ5S$$=(Fj2aWZ#6+)6$kdtyElA~jol<5%jXw(7WnaaPw-e8Ml_kVyKtRF|wDxtn*oh^&NQD7gRtdi#Th{f~X6 zwzGTi39_!z=wv)W0SxljmvQp@VuxMj`6{)AsqvB0AL=UTh3gdrr6C2gq}kY>FGL#T z`z4)lLL50#&J+pe2Gsu^0RPRb_5b8>zj4}Zzc}q*oOb$L#WNfJ1ig}OwT$)x_M3%f zCc&xp_HI18n!{2RAI8YRRn72M%d^LvfokOdg>=g^MOlNC@M=A36s&h{V`z`w?8Fdm z58QlEm>>cEXRh$SIx^KV)^k(RbR^D zWJ&jI(EBTNor?nbeFIZhp&|)dZi;O=TI%IHQ7M0-6{%uUh$vg};bNIyLId}EQvs!K ztWXiQlEWpU3~=FB!*L3{qa>8HpQA{W_SLD$69vC0b1U}?61H{j%~$ae?)*41)mPdG z>FYh+oh7N$egMaX-G(7)y4X15|GUor&zv^=PfmO4UvS#?Pk-aIy`F!h0?ubt0L2fUf$5CtBRX^4%{TLA zI!Ir5hEE+zd2-)=BCwd>(iy|>J3n3fn74#X0=q z9DZ>Q|Jyi+esRs^Vkk0-AZ>yPlr&JFzMY~+meg|)JpD;aLH9Oro!~CFL1m1+2cPj+L9I{$ zLI)sNG^_t97B1U4LqpX_46w@lX%8@)6cy3$;UPy`i^%6t*${xXKTn$9bw?$!oXRvk z7mJ<$jBK(4^e%SY&Q4G5PZTy4>$FpUf1vRSZF6&V#j5b{5KODTQJeneCQknVq%+Hd zzz)0bbvT#@iN#A(c%)ChFzpN*hfHF4I7ojUaQv@ORLjFXxds6fy z|6zqLUu&9gN*^sfRA~uB#7~~OHO=S7VogA$npKLX=GIEgr@)E9x=xeFnUF_v7fU~b~X^-omxUk_Ssc~O)@`ZwOYxr??a zOPEI9h}T~@8r56mm1D%ceGd$iy5Pq72kPSuf!+a5q?oV(KXYtr#+|;6#{8sm?L?hX zq}Wsyvy|)-9=%&<%u|+~fG9`hs@=-{%e!}{PSxV+&w>}ly3{}5n7mIgPQ8>FAKA`Y z_jk@Ek;tg+?9&3W*HWMo_=P@Fap<~W#^#V;&q5aVVS2c! zczgIkh_^oA|f?AnfT?1pzc z;)YVno2jc*>OE4Xo}{{zT| z7FE*=*P^O_5Smp@EkKL>O>L*s8m-IxwYAZ9RMHiyvB$UsyKGxJ?6Su9Ipx>~{?CGxS^Tpw|Dsd>s^*+OP`3Ynw_bPn&p_$VE&IFk#os!R zKOp%ZrTj5&e;LyK?6Hxb9I7o;aXk-h@gG6U#Jw>WyI{*av&U({~slO?^&lx&b*hkr0qHx_L) zluvUIzZTF+0YA&Fj$Qo#$ksqrjq#3i-2n(BKY1k>`@O?rF2obaf+uape?0ANwYL%z ztz-Z-s;(rYp%PyLW?kUyn0vn%CB)qqd;5dv#;xe6XfcfQ>t-4a$Ox1+XIE*q;?-N7 zM4&tDIi}5V;R7XwgU^fSoUpx?`Y^9Yc3mAjL%=Y@t&10Pt zX96h(wLF!QwFDC=^s=>W%A9TaC(*|q^wKIY z12hRt0xbeFK&$+ZBoHX+-pJN%chf_TtT-;@gHhXhM#By8Y>CiE@UN@jiyp+zH@fv3 zK#=kT(apSlRXK6%pE>Zu%k{JQa`opU+UPM66FrvLN3xO_N`1HAezh*y90!zHDqM|b zl_%dH)$Eb2nr{`tX7tG9MKW5|#@pKVM#`SeUdm!H@jh__1VX2B1|aQzy^rQsR!Sz* zKWxp&CWw|OnG3RFe7RYKzX39+rDcq_y)nrpTua%{UACYVAR&4Vws6Yyp}4gYG{F-% zS3RWW>>4bdv+V9iq8Ba3NJhhk4bNWD&YP5Xx-Q2kOhV+L~ zEiTssqMk}67rhhB(PF6^m0~H!oV~otGEZt8lf%zh<`|7|a`-07+@o=h4LoF+r`1i% z;TO&G48=Iwe*grO;Rj~Nm{Tkd+lhe6Dq6PQ>Er~CKX$?{4sNv7jIfaR^;1H!_WBe_ zD|%CqHh!sMfPm5}d*jb^UZRm{nSq_f`$BKMps|3B=>hd=hNHAyMn$Ivb#a)e<8>2& z+77=-*|pjDjSM}=ce7oc)^pJhp>7kS?BfF)9$$Mue~No1#Rh zSxCM!`FW^jCVrZ`g1KG}D(xdTeQ)$58azwcuQpxz=?%M(p%!+8!5S%avxHTBQk|$8 zVu>86%3l=Fs>s*qHcn2DdSv0bI@3{NE-I2E&Uv6MpWEdIQ3@aLqE=xYYMpG5D$=SF zy#ZZ12K_FZKkfM`vL;C2Wg(2$G_$CFcWNPzl=oXXpoeR!xg8AOGWSfp7#m0it@BN|v(F0fF>MHf-3?0mIrA>;J1r zB=DM!v-i#_B#YTARM<74V_$ezTu~%)n(}A9CrkibN6T*KTOhj!?07 zb^23v)z)TAakZ^v6Xe{>y2cm3I9lmCByo0$T`MUNe}4ku#K-yw%{A#dhRV;95#-Dx z9&3FJjIG7oYd940h^o)MRq4A^kO}yn0|+`! zK=+FZt+Q`hF{l6l(BUnL!qm(Sh^fsBf!G%4)UD;|YtbefI24U4R~lr3n4KkV`{@ER z6xz?ag)5kGXBqI-wQ%6pKasB&EF>9XNy1OI)UC9?s*$nR6`Of3=6IVHu*%g{bPgZe zT?xkFl-Gc(Wb8RcACGqwa$Db33#XMU z@&;4xDP@p&uA+A`Y1W2FToci4&N}Mmv2kgL3aeUyz*e(t(KqbC`01sh!q75<1qM&I zAs*pS^G#+AOo+|wLRhGFjBg13{4^+!@|F{4_9(2n;=V@EL8NnskHH_^K2!@ygbhp5 z!$Qw;V6JZg=oN~7sN;;-qoeUxmQjG-4Vd|wrT;>)$`z5ovrP0<6eT442$5aQ+S8Mx z%Wz+z+xqqnH}-ZLV9h3oAg9s())qy$E=_BGN^#<6F88^UeU-M`;yVgKXnsBRU?YHTF5w)2u9%Dqe}^NN*U`kr3Vg&V1~Cnzdi!;U1chS4ssH z__qKG1aq}(-<1D8P#Ex~HO94nqD{}U4pk-PF;>kH*HAG@MTQexDViOcY_M8E0hltf z3ZOH~(fV-bJgj!p5Y_c%M)vy(VPk_Jz{N+-LjQN@jS_oSNM70m81|9I4h4TzB8!gC zOhs&ktHJ)pWcO?%inb^I^H2Ax#S?k~O4k4=PSkws7pq0l(Q4P8;#;sYn1gHR9r65$ z3WkWnV4%SuDdJN$qv+bW#GMm6S-ZJMZptaI=@&H#&1Nu!b>*OOyOyEC8yk1lZ{~{c zhJ5MAONo-?z;d_15aT{L38-a)sS}y!ctdA4^wLW&UVcqC;Q2m6OEv7iZdWu%@!o_a zky!G+86TEs9&8Abr1G`mz%5_|mSzIvw!i%n3c>))$AVL$XOBy3;tZCdmCEXl^zL>T zms`>FFw%r((rGo8ko@LtSea{3L-HUGez{Pe`@HXCI-$Szdp;egy3y8PCdnFG8iSFR zsuS%${h+y6tZne~x=yQcaY--XszpbOfG~D(Yctw#!rT_|J{*&8#1@6FlsMd>8G?Dw z$z~)1c<{&suLI-01bxg$LQX1;dOlhv#H;b}3b3BDXME>x#);jxu|RAJp6|MUbhKLl zEXo^ON$gDJrmh#ZEAbT6Ae23NOTx;b?$`sS5^+>~=8;EwQ=9Xy>wBUZ&VYD!(bH)) zabl)QnPhz9INnvOYE=Z@!+~21{X|hfYp$$7>Furbk8cX&d&-1M&ZAs$UU9Ahlp81> z_+bI%ipz#eEudU`T6?MmlpQ7;CN+a{!DY>*=3insZ#dTo$_@9gCxJkeZWfXjiD`&N zy#SdmA}+hui1xxK?H-;jvnu4%(=V$p_w~%9s2e|J)it4QukC3dOH9iafs+c8x2sAa z@m)R=Vnwtoc`eOWv@Wf<-396-xOCA;C}0k#R_7KmpNlXmBTAA#G)dN1>mtxHBb;@nU_3C!c7Nuro^IfmEJ-9q`J6umYN9>Imcku$|^<~ z;O|H_AC2VY3X{U+;zavC^5ueRv-D%oeZ7HfTcVz)e^2W$;ij8PA@Z9N$Rb2(uQ7T} zGjd+c2$U^oj}e4|GgsCYeRZx7BZP%QS2+riPkm|X^OEwBn^S=RYF*LtKJYsq&Kpnr zL1I&UZ)jTj<+n3Xi8f-)JjOj%@Rdm~w_9@;B_$bBu*LrB#vgTrt zRVwpbc6-uYM7?%#JwJqFGBd?Xcv2zj61HHze6f~htT2b@hGU8!N5jwG+&bx_qB($8 z0`rL`;&IK-Qa&1YNqKLgO-$A-7R@;IS=tA3!fhTm8OP#};%^0<)46x}Z5 za3zi?xu417O70NI!yOD8#!uwd9|5F?Jlth9-e{}Z#{-oZ_in+^>fz$=eMXLWF?W{& z%U+oqM;pjAayDquR*3ptA^{s5+}kk4|p*V$J=)I^sx? z=W(pxrDU-_%dfco`W#SH(bs^e9#F@jTVRcDVdW^ytEnDEUkyS0Z4Z$7Hn-L^Tf&`9O$>D-0TpxIv&n z|CvZ21z_+Xbq|L(Vey&;t@lmtau#3Q=%cK$a?7d0i?%qmadk2%oOju&NcSa)JPUzz zHiH?kMwvM;4!;zWXYJ!!(0Tw6RKh;>f|~3!W%tmKAr0WfEmu+g*wlxof%>ujsR%*a z_OQ`4&^wf~9E!c|8n#ErP{Z!bCr*`JlBbG(qz>iqK75+0fQ$nkn?;_;Q3=-^H@nhr z6f({fraP+l%yqv#e;7}M$S1GDol|Im+c$MGK-k0F{2>I0KDA_&Nu!!A5se7q6$-eh z#aXpQrP(j+#8b|I@)#z@Jm^UeI#Uzw^rZhessI3qAudrPg!fW z!Pe((w&`MLW6sn^{a|mlN!4F(-o(TtcfDAtGmcKazN*1xL}Ua6{Q56Png3tO@XtEe zfB5M?{PZ7w`VT+doQ#d0oaMDhGbbm;+fuaYgVq_caF&ta{@lAZ91>C`ebzNlDAB?3 zASQLk`@uY@E%H9bo&xgTL}2rK+2GTJz-uq?MQy>Iszg#qd_UyT8>ihRVksiu2mf!v za(V1}O1Fpj08NC&{ix*0&hNpp1OwwRbUJ!SchijL3fN+LhSk~$chibHdjmsF{p{*V zgWiic1biYWt+#`6V^iw5Uc{wv;OJrUcw3Y&9!?w4J4WB-P=BqAClvkRr~j+?t=|L*3o|V())ZGo5bk) z=2UZfdaZEa;(|Q$m4x4N^K)`!?qFzkTAk-`0UD*l-!=D_UclDLQB82q@OknK@^FX1 zT9|X?1KWoOpeF(9Cez*f(;y!~>2WjREvPte9eg+y=ViFJpyF?Iat|uT%LpGq#c3Vk zS72ezO4uMO&Yy$-)W)B-@&Bw+v`BSKrwVX^7UbSBX*-Yk@^S&!zzY}2_k)RuhzJo8 z5h23E!$m|ygnlOx5s@tZu3mp>R5UCg;%HTS0dDtVc#=GhUSfbAtpGZOfEy4dja2j! z$Crbw*B=8!^|#z4;t{$ZWm4ZyqUawFQ<8Z8U88j~>p4BO zA7KI-=!fi1p}L8P{{1!Iplio5*#0wSjc%>Bc6%%`ovJoh&>@!hl_yaEwgWQB z?%V*jq0_5*oQdSn%29yGJUqhLJZq20JnMuZJclCiM1*1=n#{UZ_#bU7_k<$2Idajn z%6zDclY^+MfLJ%^o?NNkqz%R^)SQn#4kh-|+FWzPf_SM6K>xn!_JmNLoal^bjQYtFTl7f*cYGTmoB<_7iSGuENhr`BDOP=9oId(p z$VJn(8Bqq@@q;1?jZA!MTHDuvLnw@B+bWp8zc6!_&fu@YxFkXNYqvs!9prX$JSYmk z=3VUG`e<2U#r*{td-9XRz+RlD=ObI6 z_P}t>5)e+VglM`-NBmQKNL%Bz6#O58qF_e|_kZlJYKw(24IOIhIqT8iiDmLZ68Be> z2HoP8#v$mw?}lRO-Bj!@ABa@TtgTEH;=JIPj>16e?w2GJ9FPJl$k_q%85_4SswfI? zSP4q_xAw=+Lze)38neytMBk`EEyNX);Oh`nt%E@#`biS{QD)Jq7DC;TV)of@%eIL4 zbvuW=7pCp#{iQ2Ld302PP&ju8*`-&*nr-cLBZRUa4~+lpPAL{f!V`mFreXC)wP>JC znyp2MEOx_+q-9AsJRt<4B;#_VG%LS|TK`!GBKVVL|5Q}eQ^ytbZ1Md$5Lv6d zXvbv3?*4w6Rm1F709oC=>=cP7vremo{{exeSkOACU@vuYEYLjgoqB!oI~(4mLtqql zr4un`wofs&$-^RO*igir+n==oW@KAL~~TZHSd=u^qFB0abq zM|$X}uJ0Tp^^KVK!h+YB3%ZuF=rv6`hiKZnYsOu%2Nmko=e!=~Pg1AKxS$KUVG+s? zR)24lEWLE>(r>W8FArB=%LYFmLpli%FKWp9OW0cQP7tkVmMT@(i{WY4WP3(n_c50K zYf*hUxt+!We&6tPdjbSS67xPHDQ_FMw?p=3OEe|rjG@me@X<}#K=!6E^YojkIW3x>TGw& z5vb~(-sk~yF|SY)8D?|ZSjeD)b*04hnTg#D{PvG6sw}ebS4O(e>TxE}-H=;# zfDyU;$^gkdLcHU~%WwW*-R@f}2Pwfwt5Un;23Qt-HvPerHTX|MrACPw@%m{kgrww>y6)~ zJ#liY&{f8~e07zlG@Hape`M46^;=r&8?Ghap|I*>hyZu40dDi39Xh;9@{khYqLQvJ zAq`UIgQ30H3Ti-mw&_t^R#@TP;kWk2H@+I!ZU1hWfUs|LOyApXx|h*kQ8e(|ooj)M z`@N)eoASYH3uocK%mimsKg-f??R7rPvG8Qbjx%AEbZI`)ED@KDFv}+V@MQ3}(UeI9 zU*q^Rk7O>kkG@kdBok;H3!6p9RkZwI0in%0OFBGZxjKr-i=>uSy|N`!g0S4X{y|~H%RKk~9o~DS zit7xHb+p^wAyiCI3^waq2TR+s%cnYzQF)QjQp!Yt8O$X~Ur~h4QV4rm`X`Lr63JOE zN!i;&2cbv-s(zG04#QevACNyQbkYYF0|NhnMM;zL>U1_E*gn*`i`)Bh<55Uw5EiXf&Ba> zy6?qB8dcae2!2`k!$~|Ezbm)mA@CEB;k6{HvXXvmwpH4Q3!7EDR~`($)A*8`vOQzL_a;?k=On*DxxFbFZFY^c1yohVM8gU|&djc(b@T#P(I5 z4)6t0p=Nmp?y)TihZpF^nts4N+;}1$~;qc*!xoqNlCq4O9NrP>5NB zR(7K;8z1rxy}iPSF}NF}W>0+c)lpiNs8lNrTNW->z~(!Dh!}C1_9C;@W$CuftO%Sa z&G?#M24+{k_qFGwQ485(_$UN65jYC~7X~q*rnvmC<^zQQ>7`N9c%{Yzk{cb+neKP| z3gBD4dFp~J1^vjJyoQ_Vae?}~47@v#4;>Uj*21Z^QL62d(eL3AB4O<_^B)OW$A=Vj zaZA+l)mml7Tc4R-FUUUH>13Mbr>O$jH#_ef7DAgc=ff|U&u~7S5YNV+lrUzUW%YnT z5Db6$1PDP?$IOBqv9Z+Uv32EsLIU>OGN^%7msHqC$8r!MlX3Zp%hHnb!KD>;mXBt)GVc9abOZ z)f7D%Z#yLq<`8HbOu6qTrQg%`tHh``f^zzbz^+Zmv8IB^1t`t?U3}mk=%oaI3^6gm z!m5f+;pdyngYlCKJdkVl=S%8*Zafw)sbJ{U4pHH6+&Nf(AGovXPlOO1Z@&<{!ZJ>zr`D_Ea6*IuQ&3V%M%Lv3rE@-M zaDVhaHceU;!49x%DOdGCsH7s9$h=c+DEJI>Y$))&ZBHR3xWp4~O@T+;;|X)1!6P2@ zfZNmH5f8t^9jtYaMabtn&rxbolqtfA_bs!|V1M@hI?|^KH~*wHX7A-0)_>(gMnW7W zF%jT8p}iNs7b^uKM_0i)TYzJ>O+7nFJ^17-Y9D(Z91+3CLU5X z?R5kEmDMoQQUnOTvOS9QQ5QU(Fr+hG_?cWm`kt3K=ZJxAp+t#q|MQtf&U6E%7f<*6 zdacQjvprKwT2lN&6!RFm>_gu3cC7iBC1MMYTQfmCe)bV@!(*%^UnfaETC)T$uXVMr zFlW2giO-ujrKWU|$$%VjN>3M}D^-+|fvt@vqDToa=jBVhn4&D-1*^IjnuXjjvm{1b zalKhj<;t;tHIBUnhkyWT!Sk14UTPTKb@7+}ONxlvVZj`3nA}marvLBxDMYi5N#CfQ zmcHH2VRZ(%Ma5IBVxPzWoH<3QXH$8(3;$X!LojQgN4a;K*x3|367Ej+<|pJ+4DM5! zjerViJ-v30LISZ45O? zO(i+}_KQvW-^=|Qw-~o6MTdkeYw%y6wSOg&##CHi^!S~ko43uIYIdUY$W$JIov_(4 zluxX^Dou8>%M-nd3-AO+wjbCN4tilit?2vo3#_*M+-u8)QYnF|&b&)yR3zkXv2%wW z$yt6?%L}!$Mw%xGa!$mrL(k;Mg`!=@+F~|C=4^J}aO-tUN7U{Gg((=GSpVfZvI-e% z+n}Ip-L8%al>Fj2QwH@oR{+X_3be)Lh8_ca!p9U_rVR}AJ@`I@igg&nNTKM>rloKy znPK+1!6|Xg+2sB_gNjy&2N(x205%8GT6EYdF;9T8Y?pzn7k>eRjD41r| z%oE5c1(sJDQ2`{Am<@A~<=eU)*}VkJ>%?7k{GBAyFC7h^bxZ`Ad{jnX;XRl2hRfGt zfYE%3{rTKWv%cEm907V0SciTj`lGORVPNej?{E;HTLSiex33bG_;atmMPxDwkrQVbvr0{BbIX0Q(GY9f|X{g zURxSNPSzX}dqfBBTYYk9FPu^f;twF{jhooKBl?p1B&Oz`C#tTx_Q9QoJ3)z)>Ib3; zCwWdd7El8a+An-x3X{95l(~faz>^@jq2Ay)t4aMdVO6=3O;SKWj(oJ^jq4zG1pqP0 zn$K*PeIX?_+rUf+MtPa(#>W~07>AcSje95>)hDMKl#5H1+*$+nLV5J9YEhyMDNmy) zy|PW#UARI>;{JyJG`53M6@uywpl9mqN})Lmpoj2hI;P>ygHWDMEH;N+WfWkc+vWBP zd7-fUJnYw)+7hIU-?s^09tVxrVe*8i0f-R{^GRMmFe~tR4M+m}3%NOE90KAKaJr*c zajAR?n7=K*Ohzs z5)!;w)dro(RS>oyhY?EJYq6?WVbWH2`Ja>g>TOI$iTnl2{x9-ZwpOfDt9y~0@%V5t zU(K}|Z+hoBlVZ;5>vOheOXGFu^hQSh`w0 z;fro(SJQS(_MfG6V_B3`?ZRC$XI|h?hc{u5%-aLz@EV2C1dfvKpz=Fa6jDm-4FN+H zy6BJbH>n-~dlY<0};?ua7HtAQ;;DOGl zG?qv!Z(`|BRJ;U;Sr%)4g~>m}oED>2BQ}Xonmp^a)QSN{51bfFcHJLk0kh!ZzSHm| zsu(cIm>3xT0a9kLT4|)M6j+Shj0e1n-8$2GO&AqzCxg;z7u}IvAlGj$(nmRC>SlbI z!%4FTBr$7*(N2_zL$}OtOU9J1K<8#ga&{TE^^Sk%CYMDUbc;F%$v1H}cvSYL!TQoE zeN%>2PTviQ7nq--RUIFsz1@WNT=FZ8esm7Z$ddI;{3a!}7gUE`X?!jVzbh|UrG~>Q zsYP8|;=b+8^c4=c{PJ5#Nbk_4bu_KD?UBJ00!*Z)QZ>6#Xl2twLqYI$=}18Bs9aS3 zF^~kxe68#l4D48R)2_7Ud0G43SAcHHgWPum$;i1vG8Nyg%f%m`&_BoWNyR-nr%YJU z(DWb&cjD2(n4sBCJM*EKzi2Ok28G2|r52k0*s6Z#+$$fxvc+Yqc;4cbch-=(_> z(| zj|A9xx|RU3NUWo$$XmsSY(1F)A9v^4yb7Ecw;%*dg!U$|0rna7fu=?Dtlcu5jQvp` ze2BUND)B)f27+COaqENFCU}v;AlxZrlcSrRTz2xqo(FsAZMmK(#U|Ufp?V`9-VWLC zzaelFFaVub5V)N zUnC|-vPpo2*I{wrJR=1;xswZ;bIv{4sjSj$XiqlEstp@k3LT5vLg%)k$1{$wrT=fg zqIr`8_MXDxEcAY5Wr6Ss63iT=>=^7a?*@6cUdfgZx=2d-RPHHoXxQo)%5nB$4N?bk z<1F`7)+}G1ZG)e?0b({>#`O%Uq1^IaYvcXSc-)7y>2lf@PqBa%xTg)*-4WWyT2(af zPN>ZF8uBw8#Ja|b$|-}NC12<2Y6U*aqsEOQ z6K#f#uUC(=sy%@GE>fYCtA&`Q#7cAzCt3@~C6!aC()l*a-Km2p|4|?41aGEV-zHoe zj_%TAE)_AMI#ZlSa~jb%FU5_O-S*96HN$1mv|nI{k1$tk3EIR1mObtnYFHIg9IFSb zCiW~S*+l*)M;ske3h54+9bmE6+xC(t@To#EU`_NK&;0Fal=YeRkj^Fw-1(buCvL*+ zxHDH_RQ^CBqZ7*>PHKHjMg8v{`%hf(AoaiI@`nrluY!pGaKV4L;6GgO zA1?S07yO3{{*c>#LH$x-%bZ_!&73;Ab04YEtN#!g+qtjj!>6a~$&r5(rG@<*H@{T# zvghARs|>|UBp{Tki2 z$OoYQA)!i?-didau>dUh5L~D5mY*{Nk5d<#cCJF7`IBgXvHnkb#MifptM0Kq!}6wk zd|#Nnso`($EPulgpG|kw27S2McOXp2CQMnOH@vulx_1bM5Qw8<;#5GLMMuEiV6kRl zrAQkc#a&K)ob{Lk+l){>^>xvscB(Z(^ir2aCrZl`O#|`%trrEz&^So%k}&zBzr4Gl z@c`@!ikt}$aE!B(khlINp^jO9R6q}6=wwy-f(D}EGZanI8{0iU;!t0cze@{Y0dJjS zfO_!{MKhbT%UV@vgHT1mzdBtawgjPk(Dd?GV=F&|GVplDNc(@c%mcwbHP`) zs1{sP6cxSY9e4 zjg^0hh@%h)L7L>DY<6r0*y&Va zmUnR~r?AODVu##o9EhNlqMJwvTe*Lzd{ns9+~4P#_gmTNF3qCE{hFMd`$JU;Xh$`@4buyDtAI<&RSSDCNJaQaAtrKHtLp|8-X6KeLs& z1823ZR*&9cbu2*~Da^Uu@fT2UCzNMpvO6DGi9V;|@e2k|)g&IB!X-|!Kr^xtZo?d{ zKVDF_aZs~$ef)fhR@O5fbGynmZB%uwt-8Zbk$imNoziLfslGO+TP$xdP}%}L zbC~lU*W#4H!c_^4uib%Di=9k6>x^*3waC%k($%H)Jubl%2 z-Eo^8e-#yPHicbIl1S%sdp`)$VdM>Zxt%ktT!vQ*)oqMk{2m3(%(yd)ycU)5;MF;9 z&*fd$P;b9$xp965Z=ahP&ua}=O7X%AB(2B`xVF8L{24`q zJhkm_J;2AVCga@!3vDCRTk9l2-HlfZx*Q?(-+8Yxd6>VQM@?rPFD=P|$G*S9*W=uE zh8yYQE!Zs#@%ld2wBTpAC_e?vsE%0$3JuM<1SEhV=aeP)nDiNWdQ;@vWHuz2$mv z567_I_&qbV0$PiX!Wpb&-^9l#@L=L5kquOz_hc<`WNZMMw) z3Gk`U`4OjV@;M;1`GMx%7U==AAIV*C*SEO7vD|@TrPr>VRL%e&B^&n+U;yRz?5a?@ z9v(U>C;5wEqbtxwzUiY@;EJV?bD}VrMiAe}X(CiM`kq+C`3+JpPxC7bZ>t9|483;D z;4-{E(C=j!hv_%}cH-X|nS{MyA%uM7QKb-^Y|yONs`6~x($3{L3u$VT`7(_ryWo%; zEYyOLMA9JX&1K8hW`@wZF)B>R_OWz4dz{OyDfex|ov}wBzE~q%O3F^^ou;Y-1PabX z?AVZ-MI;&8yhnJ2k&(K=WFpen3`cETHuSEC9ZGubO1g$WGk+tyj;sNYjXI>ye=!a~ z-E&^lkI2DM@?-4>I_OaFR{Tf0bvXg$UI44YX1~;hs0y0s%+o;3iWZ-Zi32rYUo) zmzk4lKzw{JuUE3;0M^(Z5XX6ss zb&txfM<9ECSj2t)8}{U9skacMj`6iKS0A{}B$VEan+=I^p3aJ9lHqbx-Y92pAJ`3!g_R5BA z%&dPE586+Wz2Cg4I#M-$?u`cTUwH?%&+y!IloSFy=I|_ zm#(rfG`4Kcm`wKpz}SDq4>E2040x<;(z`bi+0==lCe9H~aFi3A-~cD+pQ8PN%Mf4P z4AZ!rQOrfs;R96sw4E{?=0D*dHa<2*C#)1p16|Eb7`z-Mt};H#ZdYzoB6de)irLAk zIgh2|R4(trR6@E$x+La*VjzVlzszZM)lpunacV7$2d|tdV_Q6up~+RbK2?CB`r;Oh zOx?L0ADt(b`+Cke;PbR0xd1i(OwG_$wEO-T|I3t=9s~j#dJ#teEJC67=C((iy8r=6 z(kNF&!sjPgO=3@4cG&h}b77Z&{^Bj-Du!px3XN>4J>G`~FPLyN37k7>Afp?Aly=9* zt?8IHZxIy^z93E*ppsd!?O}m{=20E$7?BG`ICu%2vRBUvBHzBJr!huyd5N|0+STZaS2F{5|Ds|BqRrMNI*gol4ArUAi;5o2?7#8!QJ9e z6$G;!{86JgcEDK>Kv{Ob(3?>o(;Y`#F-|b9>7hIu_peH4wIi#%G8heSKOdUhUw33o zb}$`ejRtu#%5`I9x=t?aq%b4ui-t9@!9Urfn)2Nalda_`5s+Iy7L-9EaV2$Gpq$fQ zNoI{+5UeF|&2vuy&Y<<8izV|A2F{xr4P2Y&G`e84)Y~CKmaJ`qGaKvxw`QnMwyl{F zhgzp5Sng0DUBTOP;;17|uZ$x{9NB`&dh#bw&uh1wS&l{ZvAM(DXSG6tC%P{R znGiCFw0y}gTe&g)_c{u(8Ws=Of4QxM=_qK!fbQ}P8Lg2@L*1@Li*h{)p}zzJuaAK| z(URQhMGYU8ds>KnPpyX%P-v8<6e-kfW*zYnTDu|7*$qJuca;B4N!i!*b|&81l*uD) zig+*%0-qtS2e&a(oi5?7L^CM(0wV@DVGH~Fsmy@p)(;@tNp2~V#en()AW+-;-+v-` z+q03)-%X2yeI?;iCuL+aEm9Or1~67kpM)tQkyk1hJei1vPAcQS^5wgz&nb=^-Yam| zL43z!ZOYX56B}|FORn;&7k?bMI{M^kUIM>`&z{<(<%3KwHy~$;^6%3%8-~v%FPmR_ zN&X-csXlAoJ}iavC4ft8fkpYb${V!4W6d57jnJ$=Qxl-mT71 z#2v_9b|gZg3B&3(M#+%fM8%|z7MBULE5xT92 znR%2~PJon-h|*x4Kg|5cH0C$8B_hFw3KwBvxw|8=iO0H~p|(COmtvSJL&Z zJ&IYvZT)pfB6l;Q39MId)~sVUsHIQX!Ti5#084i2hrfsgr?bS_hkTL;YIRtV=y&cI+C0LZ#z<%D5w7dR7?ULRpUhz_Ml-sTh}23>3}o>wknH0j!e ztm5;*^Em}^SejkV++K%en#8X~k7NGrMN}8dS#R#j328>@O-u4T!)JyUxLG}?2U;2u zZI`)=_qsX;;G9IKI=sb)2EZBT<{c0)-iU-CWA_tajh4J>28^9=qm{hV&M;F^h*_g- zOv&|L7fW**-Hwnz|ymNtUM_6Qh*>dr))pDhcNjJpu7&VL?M8;BQSV>lzE0Z0_>gjx^5o> zaAfca#kf{!Uo&q2gxXEIpY<+7wrJMCGn^l~t3GuuqR=d|=;W>4ZmkI~#~if!=f{SB zyya<6nt=v4Jkaa;{+NS)Q;mBDqbhhyEm41p)NQ6T)?QL&366;5gU|RSvm^~z$k?2x zG49U?|5Z2OZWfNFOB2Hk$XF?D{u4caYmC1!3F zk2k)9MLQ%suY@mP&f%^+)x3~u7Cz(wKr-seL1jXc7rflsJy0EfwG@BID;x!)S}b2g>_#P?9hh2W=`+`F^Z-R^&&- zpZ`LK)HnW+}B`q>U9$YpJ!p*^6Jwrv@*1_PBb`4w(>%6x*{Xj~v zaPS$T^8);|yfzpK4p(XU%4`HNwVUD2>^z5tOWc`*Zy z$)A>%gw3!f#krpJ|MTk;E5tJrZtkO2;d=iZhXgXh|5_?xi(*WUnM&^`~3OXhufdf(=RVt04fz8a| zc;0bCy^Zc~94&2-FyuXH44lh{?N>QLa6vj&WUE3$42=WI>vVJpT!Rexi|nm9zB$~_ zbI7(G&(u(Y!N4Yp$?L(g%ISg-IZVzp7$TW(3N|2K5+VwOR-qJd+5&607$684K0htZ z&vhU-B?Zqd--mk!e24-7{FK?FLCQ>U*?Q1i$CwY5w5IMQyhpglm_>1~ewVha;(Ctk zbl8Q}sly*w{lAAtwTj(QoXd*`4Tm_)Ago^VI$8$<_AK9ccwp-spgx^i)`( z?-unXiM3B{TfciS*)vk2C~=9BETZHwW%Kotnq2P|)N9je1wW?t?GVeE-bGzqbRmwT z!VedN)qY7YhQy3=v@afb1J&X-)BsB>JbgY`KN}jhc3*U0C~b%F2b}_`^8$uzJIOf2aAZDq3q2G!cg4+JakmiMt^a7$8*L6yl`2zErI*1*211Y>{VPX3Nxk4eGhg99SIbuTh6|< zwAKJ_?4Z!i&g~S5kC)YUA>WM(6b`-i3Dr+lPgeSbKwDeebn-43Qkqj=#)$-zNFafj zE*5x~XRJ0`Rm9F-61@#kGPb-sjp@gkn&{9TdxfIZ(f@*Cf#MbaHdHWicu)@e83^5`>43O0>>*2&TSpVkH#{U0n41Wq#s1 zBpLz$_<0;(j_{4xN^A?O-}3QX-AkD;`hyE)j|rGNFmSR#f{vtqZt@c%t;%oF9*cuZ z8l10nV?6yT`H28NQA?P$bD#|jBQ7%|w$Q&FD)N8l?a{xs?{6$`InCeC@y_zTv%K2n zQkt_k&t~eWgh!eiTQM9ecJpPtn1j3L6$5_e$9EaM<%+&?$>zuM(t3%Zo!;&hoyqyzeaUJInh&9qRon%Ns-t06>oe^bd{S zG>P0Co-6{pM7#6fSl%r$oHm-pwd)?Z%N@C)V!<2un1gidG0xF6m`CtTc+!fJanH$} z;xPxc=E3QYh@Q)inO>S_1C8#S{d~pFLQm-PTDgu{Wk=R@9{?J^|1pDi;;(tbp$mRM zhW%-}@AsYQ-;ML`8(aVXwxa?nW9S*MJup$0-XfUvFJuJywLudz#)sARs+=XbiV;P^ z3fYDK=?VneybMwX=rEu|Qy@Kr`u3kilJ$ORG1VO3q~q8<13^G+(Qg3Jk?7xwGY>$@ z_&N`=s=P2E?mv9u_FY~Q`Ai9U_qB5tzN!xN$gRjBl+Bnr&qqurn6*&xxWy%xzVkaI zMRMp|p&X28jclF4(F{cZ%DAY+vSy34w>=9WLy)>Qx|}pJhJP>M&mxLGC-6o!@cj~j zUxvjHfwo_r@%%b?gzlT7%iCUej6cCwt;?BGQ2sK{9!+{5(=-SB4^kdDhXN z5U=pxt-GR$nEx=_->isq{uSD+c#6iZA4$m=pbROHVo@+_gVI`$#0w?Tm|Nqt?PpFt z4hJK``rTcvyFhj1#Qbor4RuR!$Zn;@vKlSZ_R^NZCxzRh6J^xR)*1K0r%%7ew5%xa z)3pNPgFY&QM(2QABS}Wre8{^QiZ8ml=xI)_ZoBPVNBnZDz;%HNc%{91yVE++*kgt_ zBfHnId82#`gZt-yQty9o1BM_uP{7nxPS*it!Q0}Iw>x+kJmv@#H#A<|3Xy@&WKP2}HZ`o}}>OgtOs!|~S@iYH2IBcpA>s(`OLI9P?TpULJqVWFb?Us^!8 z1UItdegiBRds|ff1k=uVd;_35X+(rzzr!b(jq*FPq;U3KZjJQ*L6BYuY1OxwjQ2hcJ%5ghM>fHO3pSRh_}gN74k-@4#B@RvIp z`I+&ql$M%`b;7=<$KP;~w!Xf+cu8mu9St?vxN&o{v&sTpU0qqxWDp$#^pBNZ$9j&3 z*)==0J@8fKwc+`kgFrKbJW}Upc*-DNEKsYw0m#FUgZUYNnI)rB#_H>vj>L6|kKi!88$mb>zySo--EbJ*T?p*DzpV;{0O$Il<>`tjMaLhj zj{U%5A~sun_rxrX+Pvd(!*Tr?@rbbXO|n=ZX_lGk4NGAq5$hG>9I#j{IU?y~R3RZ5 zM8_%|WZv*n?h!KRgZixHqJv_F(d;QkecN88qUXU3In_}KraCi`U2Jth=VyGIQF9@C zKvlC8$|!icXF5Y^`0csx{z33u791_5d{DgQfw2{MV$vW`A4t%?=@D|2f7ywt@1Cvx z(0sd02i_>2e{yJ~GxsLpb9-BqplJY3yYz9$-Qw|Fr9&x`AKp)Dip;+u3S_zIUhDe@ zbYz;MzHeV8p1bt!YH)|1`Grli_wK!fQ%IFF*D^S?u(pEkZWijwj91~vABOutKy3uh z{3V(bM1D%^e!u-lUMG6FrlnEsW3@3ipd_T&?WUNmud!tC&FZ@Ls6)NKo(IAz!4*jxO7?JB)c*=JP-{lMHx$S z5@=Yv`G+mHDdFNXFJs_C;G@JO>X-}D+i@n?;ccM4w25iV$S+r9UaK6^&o ze@>?F4BoXl2;TPdNJVtFL_Bl>u2y}hd*L2Qzmetwol3sC7WZCReXP`p9pWF)_+dhuNiaKCS(A)=0Ow1F%jL>SaT+}lRhoO^Ww zQW#-ORg!c#c`uzJ$HDNA&#^`yLxJM)5M)y7Ht366S6O@gF?tUBxH#eqr)ZKOUoYrXLN=B(1<0TiV9M8!76fO^j zMt};$q&YgC$Y`Tl6m+kJS2-?8nB^#3wlndI#Ce(TlCKkhydtZ1FINBLA~0R8Q|BP|Ww|GlgWa%s4^&vtW<%A=cao#X$Q*;|#3@xi#&q7C+MOCUutSo9A zBs%lMSBb-!nhi-{$^H&neSCI+;b_%o#>`4*cC}I#C1E$a<{T#nC?W-oh3KC|qfY{{ zCZQA_->MKEGROlAY|&Q}>df=G1TOVu{wpiaoC_un@cSc(BADjQR5%tG!=du)sV32k z8Y;u~H=_ITqUn^%3TuU)_J3fdV09sKr^r^nn9sH^p@c*{BK9*XVwO z@_vcRL>g}2AM1DTalf0_wdD`m5v?rp@Z#_v>q!2h>Kc2Q&VAj~s}loSQAWr@Iw;?_ zp=VWe-HMPvGH`f8J=LE`t8ymj3_?V_!XYf_Iw58~_kvfka;|3W;CGv4+O;{S5C4?d zr$cV7Ht$iZXnSp~ z&)C#N#y>1S?rKn)dy5J-s!YNqB?`FvKyH9IK&3O)VDGKcD(Xn=3c!HsBmHcd{vO!oj18ZY6Cnp#;`nu9hzFE|@@7{fpUc2x@d%Bujh7Ru(?dewMwLF=5(Qyz0g^lvV!07!$q4Br4`JS_bBeQ2;v;+fl8=q*3xDGh*dq=Ap;{6Qeb}6V6n8X&`!2H ziZH@3^T4iw6E`T#!^EAS)w@%ZEQ6L218;CN)o`U8{eCEO-UvR;Stvfd8Yn)+8FI1# znqO9+-ItDz4&T2DVLHXJR2<2cre@oO7s-=G8+Y>-tS>&@Js+5o^{w!h4`{>*YvhQ6 zv+?3s0VA*54-bWl4Q5m&Z|<%0<5AhO%q-xeg_tsWHc}><4G;aZ$fq{bc^tYMdSNRc zL+Nt)<6sn}fIBv_vLp-#c**yqkr{>kCZV_X$F zcE(*ys`PV!h)yr3jyq-ws7wvq*nG$F8YI2HK$M(G&zJc~w~V7q0CTIo{F0(rCTJq$ zgVVAWrq|PZh^OjMZ_rC65$BI`4}7uEGAI$a8;x|CUC#UD_+A7I1c7>1yJ5{w;?)j@ zoj+4xnbw^sot2-U$7KJAcY=oiXhH%KR|ss+%~)TmS)|t=QkkX01cpdYVAE|M zAnOFPc#97h`6hlFnlM&M4DZxgWdb8_sdjSCh-(8Ry=)^z{T~~WJ^_1+~TszzPw>M1&ATV!k0M(WDS>(jj|0YFBil4JI@bte|z`mh%25pHlEs$c8~Aepu&3oke!!9t`_IRL;1 zgylpWdo9PY8~54#!O3l`>AI{2rm?O%?m^N7Dk`L9(S{+AKvj6fIr4{82jr{Hh~Y=> z(Tm-MRX2M*`~rY(IpMFtA%e546QwtA%X%x?@xeLm!m1i;A=Jhi&<=m&Zg zE<+#!&6_H*DKT37$Qf+D_XF0MF&Ce$6bx7%fQQr(&{0xi@U&l!ZU~#)0g&X zvKqi}5V*AI;{ut>U7GPE`Pj5b-+pi{f~3QFwVznT?DOZGzaUMVZcblkEsU#+?H)N&*>pQI#BKX;Fewa++t zs!i|_G_c%EyZCtgMV>JIdYH*Iu2e9a&*K9ZB*0{TyWWSya0g(UAS2=7z z#C<;kr*J?GAoClW`QzO_Cm3*rgn08YcyM?&K(C6(;S*0;FyciB!Vqbf^&Kjr!Q-o= zH(^j6TEt-~k%$d}xo(sF7>O9tAcje~zUE3>$)mf7d0LdTLeA4&oBN(s| z7d#pWbrwtb#TMiM^UM&U>oiMM{&$x|Al52E1?CSqVy@_9Mjdk_7?Wcq%cmja)ETE@ zO5lQ5ODbjJaLlqrjpeqlauyxGExS1k^)be(eM76tiVd8D>(ioWA*YmP2$leTTyq~l$43R~7N=S|c$I+?R|9o`x1!x2W@5@o|%7DnmH6kXF z-!$f@_+^b68|}EoJ!SH}=4p;NH~hOD4O%AJvo(E6L7R^B!F8%0b|eZ*;c8aqeUkYS zUezBvb>h7dP~Qr{C)cP4y+^K#IdRi1h71eFX~=l#3-ASSkCF74szkq{SR1*fK6mJI zq48`8 z`-NhRvQYCZ`J6m5q-esL7kF){0toK$S%d4P34!3OtY_*!UvVA}1i&c@hxs{?YmnE| zoo{sl;!RRU{Or&^2uv++naQG<_&X@$Lt0@f^3`b0O<2;$!WWAIh5fnxsi6DG7i~M3 zd8ely0c}(#N83PoWv$l(>sS1?)1KQBRpXD(_m-6u@;@{Q)`ijOo&Dt*9~{zgv|1Pi z)z1*%c*gOIF7f2q7UZ-*^^U`MT5r5pmJYg7D_xD;W_UN7X~>G}5nK{D>8;(zg)R&X z0F}|7nTDy#2@lRdsftYEs|6ZzfTpRfrJAFfs@6cBStWI9(L-DA*Qmhkm)H?~_;#@j zR6nc+m5YD(;+61>0{<{!Kl(a`1D)BHdjvS%R&?0*OyP0(WKtA~3e37_q5~5}*vt6# z66Kp#a?aY}_t?i}6or%W9one5@8{U)?O-Le zUvBH96+f`em9X_FUW_6YIqg9zG~4+Vu5xrLSmA6?)d2 ziLwZ6_O1V>!Tbs^?^E}J1!l&{v;3Am#AQV{WOMiR{N@yHPkO36UG-YVfZO-glPWKa z@oaav(-BAYjQ1GlyzZgI=BnMZZ3kV3j^@qa)%&f4p!xUg<{wbb_u{*8{)6j%cZUC) zoIy9I)YJy&Q|>vJH9A!0E51K|9AE`}K3el%5Y7KC)vVC+D?%7}elSwvKVN_ZpNxJK z1YF6<^5Mk93)V=rsTMi|fd2FnqyNe_5-acNTjt0eeoa=5L}r~W%ul9SZQ~~`zV;g@ z`Wraj`WxeX{x^F3kDC;)r+>j7;ix4Lf&j^cFdVt7@dhXa>XSXfMA_t;{pGRO@K2&+ z@I=*g@s0gVI5wv4lTQ+U!&3i(raMf`J{g?R`~}mr$T|uQg`#sN_#MQ#X5O-?`d2jl zcP`wvUjgc#9GUPnQv62|x?lM*iijUtYq7K4>Ry_c*^H?xO!}I?Wr&Sab~-b=_Z8m; z?r;Bv7!5`9k`L&}z49*lC&c~nFX-rBXxN{e{ECf@j!NZkZ1Ip%k}?DmPqCxcGo*&gf}I)HQ#5$l-B$233iMNu9Z0T@}8}4Kk3ID{3 zJsy7Y&_B6hP{nOk%4$b-^`Go?sL&rqQv9iVup5?bVtekYl4b&FD;jjcg_WTiNAC8J zCNRMQwMGs+pHZON#>m%n=mwpW<>xm%5^X#yu~Lf>?aVJd)TxTt3jv6CgggB^)C_JhzH3Nu2({$={3rJZk8lBEY+G-i`B12lU5i z{?7wI-nahO9qW6udT&<$_cbe$^`A=AuifYW_^s$)SVEewt{joK3Vnu^@;>s-XalUW}K4tu?>J^pfO)kMcly+qqiRv2&Q=R-9|!IiiawarECmAQ0}6HV=+$X+%U1G3U*P?Ti!Io^0-*Aq1$cql)^PYbtO!>DPC1 z%duPI^p?rs`SC5V?WCcjM#~NhXIOCRjzpz~Lo}pQZHXt^FTIqkR-y%z{@ShIhl!Kd z>{q!yGS2kfMIG%oCrv#^!jO2cuzw(RGH3?ReeM|C!5O0Yz_RZE46|A$$HvPSec~4z z$ekVRL6B6MYXOEjyIFNcA;n*4=o^wLda(*obbV?aeA3qUa7N+me$d8iK;571CvZXk zfxrod9n!j1DGuybD@t^l?iKr@#n8?vvyPLjtFz}KCv-C~e<%$`g2f-r1+7v}78@f3 zYffMs%;?9Eawr0PSn{*mC()>NMoB;X@o=Dy_oQi-=)oolv zsgxE2!Mmp(88<#0m%Ef3O3h{Pi_kpEj8-@Qt!VXjfPq@!eh>xoGru5$>@6kl*M=lL zX7>G%O`F!Goy*d(t!x5#87a}e%#+7PMKs(Ky2^+|dd53WyW<0)d{`FwW?s=d=0m9R zPn}en3(aSq$`$g%TN^!ZJN;nid~=27F%!b=`qK7?$;8((=a?wCE_nl7C~jav;6Pw- zs|#Z7;x?yKuI{E#l-0~aj2kn{!&*cXbj(pzhRR+(2% z(rq(nYqU?|$zI>|*}z@|>D3pOyS{-Ey>j-?PhgkY@nt5DHqD|XomsZbnh{Y)a=&FO z$?!#L3KPxCtcOFw?5!=o+$ecY*k{d2?2L&iX6$WRKUmBCNTh_x2GOKRY&sb|WBD?4 zHf53)N?2-^&(q%L>KcfgE_#7mY`(?u;5_?-;#O8zef31AdV=b%nIEN_PaFeLeH{y9q|Zlt^``NYhLxN@!~@%@8!7hj)sv94 z&K%|>|7gho0FdV}_EJA)V=sz_6eVWYa2p5XF7ra6d<^pLWv5)QmBK;aa)ZjK^V~y@ zq=hV*U@F02Ur*Krap;YlB}>NM+=tfaa;+TmO;dXLeo$&!kgh0|9vy2^A6$tH4`qL% zs*uk0FCd&ryw4R-LTZ2tUMX%3c*IZ(=f^`bNwGP#Ns8ZDct6!#$={;*CGorAA@DJlh#JPV1*P$Ef+?+fg_81u<4etq`6Z) zR3_fe32ik?_lGBrq#FES@S_%4-`JVc1+2Nhu(HT04Nd@R!W8T#Xm!KZ#53nL{Ve5x zX%{Ve%Ib{l8+?E|laSyKKarV`NZ_j=1DV6OLnwH$^p{zgNnQXTl6Cb5wnt39j*8Ns zuAS{-cEU8xUaS$WB3ww1NiQH2DAh#2+#!2t(7&?IPG6Q)5qxhMW)4`Djhf=84y;m3 ztDt5=UTAhySncFPvaYf?`Wh(X_n}-6^z@a!S8tS`=A(psmvg?zg-p^XUJSZLEXSVP zv4)??y2F&9S82H(;Py`U71q$S2yh^i*L)3W$rq*Y6BG1=kK`5gJ^2w|7eI5(4^Mz5gp;N zZ%0aSu5659F};{BvC(k5%v$R&i$MO?gv@6tXcwJpyJo==WX*C4=`7Q3YOx7sED(YP$H`p#)- z!$NvhirX@-($PU8P7=#3WHBDo#IH^UJ-bHV11E>YDk3y#C3$PIB9yT|)|RM~N~nIo z1Z{9-mrsJMBl_-`xcGHW&J(HLv70WCzD1pGsWe80sML(wES%!02BUnS!i(io`fc7p zWniBpHAPB{=sE{hgNMc7%5CKx>_+0+Sm0ik-^n2{!_w#SQc;pQEvDr(w{W7A{`_L@ zQ2Otigcud3Y(~ndprUOD;evq5Aw{0Q1QTS-1_vN`R_21xd|9T*^y*r zGl!sVcu+nOIUa1iltHNw#MscKj%K& zg9ys45<|^@BZZIKjdZEL*q;R67}!RAXvfS%whW8#OxNmXf;4L&0x7{9EpWlOl%YTyTDt4fIJ_l+%Nz+pQ+c^{ff;0z21ntCT6ZUSt*l?>`7g8)-l0x7kBE_+- z1sj}VJYfr^hmO*xADq9Pe!HfF^S~5=ks>@TSjv^jpIsDFlHRSro6o5{PS)%b99CTa8|)X zqMxc$v~Uvl8Qo_qiCOmHmMPGSCQX+`YA<_;Mx((W7V`$%fEON3cbnOmk>8!;#W!3> zs@k!(Yh`>HldfFv&+gjWM{P5|No8<{3(Yr(m?6kcFdRqu5()FC7y z6`q{^Wn{A})?ptjphF025qHA7ItUuk+p=^EwiV_nT#|AHIoY`BtC~OA*j1(Tu_@79 z`iM~zR3nyJBxlK|sM$ALvqTGhgRrVoL{>6sY+cvDIV-FC98X%GWU3GK2E9B2VbS^7 zd_we-D_Ezzlf32n43!W=(1>^VYoqR&|ffT?E?|*{4`)KZmj9rX?jx6c~`$(+x zVGl5PhdQP#oQ8=#jdCu`>1HalR<{n~GlE!#kTvh~>(ZE!Ded&6E4uPT_Ho%>3%JJW z{Ulu5_P6`bB$Sa+>^tI4Qqsd?W-d%SK3$Fv`+eoUCx)t3|ZVI4_JS zo$xiNxJ*@I&kH{!CgrWj)bSOt?onJ>>rE-8qI)2osddV9aDc~TxH^xW#F`QQeD{k; z#-nTx_+&gecWedUb#4gU;+#&TXCOUTTzfxHS0b?3x<3BKFrYV};nuf(p)iNqVH^$R zc!C=g9s*6tujrD3De*tPXsqAebYqgh?1yYc=gXT5G<|&I_NrjWG)~tGSt??=XlZkd zrxFoPkm*?^J*$+e3atWX%0s2hkz^k?SP+UKU-!Kqg0s+d3<>#?Kk6rz&Khl@UgbJe zWXLpM&Lme50efqsYO=mwMjH7AAX#wlUSW%;o|ZlAp0;B4njCdGQ))f;EVzEw zi`}H2&a6Z`=23Sb00*ZBxE4z-_*ky+QAZQSpAkRsDFBkVjeGZ*V!(!mcg`ie1oa!7 zBSE1cQU+Z;URB-rN;$%ZE_(53O7WGQU3=dbhc6rE^sY56+QD8Gfy1Lwqw88zsOcH; z@UB-OF!qgX2eL{xvHcIB>~tAsf=f>4hj(oa7LlVkf=_BpIFOo zLJr?By3xr#l3>4bf)lO8N;+nW)OB_!<`z1lq4l|&r`AP0#6UqMFy<~{=!9a6L1}r= z1Bb{^nkPnCtD zy?waey3IDFM;h=*SOa+=^Cij=@!#}^dJX`*{Q@TziCS;9nXuZc=|GvczSEK8Md{>i zIl`tnpXz%BJff{Y8qki}9%uknK_q{dCE+0cOb(HJm^j4-E$$>j^=a09%f<}b6|*Lh z4XvWCB3Z@vw;~jk)T*A9j*!XIu*ETX36FDli$7}q8ADxpR`ts+>n)06dUNk;XUNoD z;?lf~s>juz9Fjc!FTB$F_aFKji%tH{V*dw2{qI_MXR+T|>~|LXoyC4x+H`!@8_(ZocGWA}r6YckHj z6pt4JjyGv8qz9F7ORv{5TAz@Mwe}QD>CJsq5XbN@N5^>#BUS@@^C=K7C~X=Y&c6yx z{t8R}A{qGy1GODQ8ix8h7_Ob=^tT*X2FHbRU(I~4j{Ck ze}F&#N$>fOoc0#zDQEj1L?{tI(*;|=YKw1Oc`cH3J;^6(eB;JgttZ5nbO%(8?caJF z1brlgaal3OH)9?kC3Hol{3*vPlvQL)gRXY6yZCzw^(Zt5RV~W+FQ_um&mI0kFca|q zB5XSiA`>P8Q3pu=nLDWvT3pT4;EbRTT!3+n39u-Uuuk?~Cn~JFs!*TAArz<(u{SO0 zzmf_62e@qgi+(fn+ctq&&WS&gI^UxHE;Y$Eh3}+Ebg^hw95}ZYS&}RS&;5X{PjB7~ zrqo49G0yEzVcx=tB_Xw_zd+u9=WLZ_{*K;uhTe_-R3rW=;ftcxYD)|fD3{yTVP#ZpULQdr~~>sQ{GDThhxTqS?g>Hgd0|0Z($ zm)iUf1@FHKB3Ik1McV!G1xcPj0Ec?tb}wQo})Pl})pzhT+GEjX3cXe)&RHG(BC+k5>p zDH#eb;U(Z+K!5VJPC5sti^zTPXkObI4t@+C={rLXI0&F7$)Z8#_Z?;Xu1#TNT@}w- z(VbJp%Iv!sNJFuk_^o7C)LXt7)L^Gyum2CK*Y-3Zm9OFe)NneWnoW~uFUji`FPO&p zR{&m=Y*hB7T@A;`6!>9)(bk4GDwAJH5@#-YlXR@SO+8xM(oO)1z$ZJ7G1eP2ubmu! z;m`tS0shD!|FBdIBXQg@QqA7wzJ`xIOdqKBX_s$+M$6nXm0CK22kv6j$Mbc8h=Mp0 zs52Gues~G!INcc&(RJRB4>@XP%3%UNFe1ZQ9~l7sPh_3{lpe;Qv-#uoI7=mZochFz zLO%Mrr1??bxp0kk6%iZOM9>v=}zkL55YOekb8ht(`OU7 z$0-_1@dcU62Auer5w?`SY|^*6ZL9vTgr@)WitH;Ae&OaDIB`T3_=!#3lNG?ip>(Ui zc;5n;Xgqu#lW?jqT_5_vc&2hdoloN6Tux#j(*TY9@DaR37+#s){Hm_uwf0$0R--d=zBi}IvzLWr>6PJ!-Jg)ns9K5Z2$7pMjn+o|yztPEO#Tf-&&8*DC%2b$ zos`vPtI);%oU-{Xo|ddCM^s93|4mWt-`^`*%-=-({~!kVea3b<4^eSziD}YkN2bfRgC@z)KO{9E`uaTBFg*+i8tF+s_z4G zJ>2KP??T)EoPOZfK|=qWc;O#)_Ln>i@5OiZ{yzOFQTQrWLd6dYa21QpLBKA!!8{KBJKN)OMqB#h&-beg?Q?P9CmKLz?q{RTKc^ z>wqH=Bc~b@QlW(ecV*_gH5(t(>3m`A#ydB#;NBsZ{D;Y`#2f#j#h-3}+CLDw3iCS40g1oG8PzK_vYAR?f+ zY#KNBxhWqYgW{xR8LTX$j&##t$nh@faYO$%#uWfMc0ilUxWp_&nsl^8XL9W2EpsVG zR6J`8s*X7mQpj#Z4(EoQz_U@YhI7ZRg+BMw5#L-i-j?b;cgAaATvUTAvsje$CjR83ogr|0TNqm4m%tSz$oXjgIU6jElVS3_ zB#Ni5fVFnz#h@Gl-O@C z`z)m8qa&KxRkkED>B3aWYY=tcLryPTmS$LBM)i|wCPRYAlo)S$Dqz0Fp5At@T#p=) z8M(yvX2}D%4fG^NsZV+~_BawPz%v6JqG==c)AAaDCF=-`Y70yh;p-7-;*b~iZbHuE zS#2j+-|Qm7uI7$$csHqxR!ujO6)ryxHq&=or8`exi7A)p2?B|mZcHlUv1Pw;8Hu~3 zQp0}s7^Df3`9dRt+Tb}QVvUm8v^jJEi#jZFafEP#G8ll*sA5hOy=fW3#9*51Am+D% zVyHVKb{q%UNIu+C7`H%){8h#q@G4qy`Lcq;Wf1VQXZa#6LZ>gY$OH?(_BL`(z$hb5@t%B@&A53PnZ=2+@ z@nq{xQM}v>Usu~*U20cF6+dYdWsr=%@KZ$4R{F`pvWfTPoAw}6b1GA;#^GRo!2#$9 zR!;IPXwA!$Q{FF^MIRK=MJvAEl06l3? z`m_YGBituM^(YPCg~APPsFSw(hH1R+j_y_hgw{zct!xTtoidtrrZR!RuK+T~s z{6Y6BEON6T!KLBMStQYYao|H*dWycK*27B2l62S~ICn%2GMMBDkdVg17-Dsa8Fy2r153x#MZvt6<<1K8s$Sh8{}xDQtpgu7jh7f(`BpHi!8H-%%yhH%%YFJ6tyEcQlrL2Qs=nK#j9a)VTDb{os*hTsT&Rq}KhVq!>VwxQzB3}T~zTIkLp|#U3w+^yk-X9}-nk^u650xUSQ_&BazT_DY7lc`->J8nb86L z){WY@BjfaTjik8z4#!_6G;*;!95g5p$kyqoUp=c%ys)&39KWn3bjkF-x!yHGR_{TW z3cB}m8XxnIA(MNMlU)VYQ1cd^Z9mTWhAvIiI%FWPUVK;{dvS+xd2El#^#M&nmbtzW zNLbimZ$K?xTW5p?>`mh?m~qVAmY~yNBXGh~T&s-`wQt;LGs&cI@u(Ayy~+4iBi}X@ z6x*)`lUqUnd*fD&AS?fK5T$M(5GBlO?H!h8OwG_7dINpl8PRB}&ZI9G4 z>3P8bnC}`sDA(5@31p=Gu!0+g%as5ey5{7>Rcd+n-*eG)a?84rzg1&49cnBBv)Rjr ze0_r!SW6sl1)vLvLrcC9gpXn|b!=V9S&LF91RdjNRQ+HWcVE?3jXo#14gi3Pn;j~s zzm*JiFpypJ$|SoWXIDA&!H@RZEK@qyH$ObUTAv&#-q4(-T%TFXiBC&8t?;X))#?oR(H-@_f%%`tP|E}2I*y2r2Hd1w{;sl*TBkvC%4wNOslK~_`V?e z`|befvKOtS>t!v6PzwBNLb-RlTgw>9D0TcW{&F85zvI!Und z=B+XZ^OMASx0_2p1WfBgczgu>R#KeOh~V{EM%L3c0D@3%r9uc!VNPbm9xV_M(6Z8e zf$M-1D~{U{UNsUkzYCy9v`ouAp_L>waBbrAnT9UR#(d9hs>_0;gu|;;%vmn_92x&@ zM}!%8b37`hAz#ZQAM(?fv4v`1t_e?8=gHU7b}-iBQ#D5ArwXOd z=RfSa)!&Tlo`y@!j_b)kb75!sVykqucXMy)sIvp#aCae zeDnB$l#jCly}iX$WGiqbJ1$}$!{AAngj5A;7+4H1l8BPS`9R!(rq)w=>B@mOOf43X z9>`n)lRK#B5Nw5SKBwUqFq^AQCf(~_`C|^<8Z8F;qk}mTBnh9v6IEe6%4KTk6}{gh zqPql$xjfI8at6;2T21zPHmB_FDIRSS^i=kNu6sy?TgioT?XxC!pJ*!T0o~EdLApMp zyB#i3Xu_<=!#$wK9ULf~_g(8gR7!E&`09LT9~*E`Fnl;^Kl3SrKYF6tdI8!l1LAwFubV#!!4>6z(pBqhi&gW%kYHg%gr z&DN3GUAmY8i<$lpXY>S=Y78BU(iZDyl+zsU)63*gJ@y7>-_ycIx4E<=>V%ayAFRdM zZ7|~YHi;RpgK!%f#cIelU}J0U1^e#oh>1}lMzuMsmN9+OBJ7Bk+{g2}L7k~{rNwnp zwZ00sX;?ZreB{h1kCV#>pRbMPcA-e~p=Z1T?+7dCmIQ58&}PxHAH{)^@dU};L3Y{g z(2+}^8yf`)v?>PMGn~prm+TT)MvKHF>^W=iQn~JQ6n?;VpvhgbpJQm06F6S-6$F9& z!k$@wKh%~V%JBn`mQhj%IJIR5wLedCyDhnnUcL1{N*%H0Wwry8+*;bcr+qP}H zx@_CF*=5`6>azKIX6}6#|2udy6W@E^L`P&~p4>Z5X707uT074?rtEf)T%Y zN8P&6pWsh)D65pE|0Gdp%!RI)x;sWDlUE`~MDN)bcXl;{RV6t9BNi)tT5S&ATYdAbw2m?VX5DH$ z_G2Rua_7BdS$hJ6OQ_ebVLU1MDT}!HpBC(@AtqZjTGi3*7_yK6a1L2EQt&0Ey7(2f z>^F-Di<_s~#BfTVG*18y<@T3l66lLodZAZWGR%NOgZ#Zkv-Q4`*891?GF{B9PvWaE zT`sT^Dx(zXo_DTX4k@184?PmugpM!{o}n5ofT`>|-7uJ~H9L$f^@1&Lj4ekkN4O3f zZe1hMOy_x#ri8Ygj=@YAKJ5tLQ{7x@;zfC8iu22BN(F~i z9`O6b2|^3lkjJ|4Lk41E8H%5*JD><+aCbx$MLkDfblWKa+qesON|$Xt4$4U3O347V z1%YOjkLVJmztcPBMliB8Dnl$?pr#1wdhR|IX;=1tZS@r|_oi{2;-e zp8|a3U4HazpGV&MAjBjNmTdp6)Kdjeo2S9)KT*i_{`-07|LTbU#7lS5{CSjXP=Heq z0K8va0Vz2#2uJ_`K%6G)b^4pn&~-}1I6@*aKjJdI_=h2BxlA)L!0E#Em1gBb%a8pW z=r8O)itThsxE~pE+bib`NTVw0;G+TGEu`*@oqQ0{=Tc_x* zH*aEMlDnL*)EP!6UtZMUG9WSl0)GB?5zGH~$N9}m|K_EC^U}Y0>EFEcZ(jO0Fa1A* zmrnYX^u!MOFTAu!wp39;em;TS3HzU7SL@?LR-8Yl(q5!_Zi{yWa?IVGp3Zu#o|uej zh({$C;`0REZfLDvc}i9D@en(4K9JY8NZ1PDFZ*$!NGw*URB1F> z{BC}W;I|h1zitTWK|4U*@E367G&09tR-+W*J8L;)B{gW-DssC6p>R0tcK>G61BHf8 zu!BKYA337MP3Ts#s-ZcY3j{`4ovx?gVs+67!ZDeLIrP(aKvYOdz>{af3H~;ZG!i3? z1DI+L1OkD8-yi5-ZTR}Chsx=Y4u)Y~zNtn0z@wgXh<@i;Duv!bM*^3i?IadApb1}- zN@ho2+W%?LcgvOa?+ZJfLJQXq(Xs#}5{82-uLKw@b|R&k`$`0}3;SaE2V34-LG)3v zJ-{O+5V8tN&#db-cm2}L&4i}V#20ILnx@A8Kz9IKCX3VQbiU*-jcptdI*}80c*`=x zWDPl<8DR1tSR*W&loKzm6ErNWc#cMhc(S(Rwu{RFSm{pvgl3xL7aL=mrnaQ|r6YxZ zqr15Jmr4FSrG2I-Ai>9BT~-3viG#+7ezgCSHrb6jn8T@K_T$p{%mxtb;EyT47lrgn zscrRGUlxEf7Y90|4Mm$W1TGHra2v`Nr7+yg$iWumfdj0Q2b#aR6!s;GVTtCqjO|W~ zi9ii~bP7)c>gI8lWrrpdXzrN&^M+L*6-Shmn6O{bQpg%x`>w5(Nn!EA>NW6A9_APW z-tsy)CF}oQK7U^NU(FN)lt{;hy2$%Y<754lpzsxa1CRJgMbg+C2|8e0pUI&AtIM7e z@h03hM|wJo*OtBZ#6+QiQz(d-ilbTk$|Kk)6r%Sq<4>22$qVPd^~2VkFco2ct9fuh zo!V)B)c8^8gQrY{h?7Ag1c3db_YobB))|09KZ6aaenruHGas-0ET200T7X1(3Fo1H zOSD-Rz??u=-E1EuJ>J2)FKGyHD6_HCIg#c_YdqPO!g!?hAAIR2`Ui<@L%k+nphpz> zi_(D=LKB$7HVM7}@|ussq?L4{Lxn^7UhfXh!!R;Z<%*4`k(7rChr8=>e=~zHhy;G5 zygb9uGl_i0v)R1Rz5PF2%fGBUHG%YRrS_}m1}XTm_PuKkZR$u3ZdmMr%56d1zws+o zbCfT>&-=ddHr_kz#+si94$qxHKW2&TN{lS@P!fT$ZdZba5$tNya=V!B`GQmp%2Lfpkp1BCbZ70>a$+l8F&pV~@ex05_X|XY2n!m;Z2c z|4Pq)pZlW+`z@Z|O7z>RetU-Bp5d?dkpKU12^l;8c?P-k_HK7;gUP}dnlY%w{l(iA zV{?^qVE1LxhdNJ&Ma}-_ElRX;#gNyUyOC56QuvE#+0rbI!Na8#rf;DGK3Ko*R*tPG zg*!JhZ)YSe*}NYhAl%-H8aajwIU)x8i5UJBFsSd(p9gpL&f8U@p+Hg~0~1ZnE`s-y z^j2GJt?6h#J+q#3+mIZsEFUCDqGVh{6&(v?{AB_WhUNC0%ue~16zb!W@tl8V6Iy{+ z$p`qzNz(EVt_bG>l%J%T(fT7+<{+6;LT`)ZUIQN!P?=yX@)SIv>b2|fdlH&1@gjf9 z%2$pY_05DZI*Kw1RqfFKAY~Z^RVE-<@giWja5N-G`UobbZHwvN=Kv`ZU5$yN(0aI> z&$M+A34I6hC~#-7F^f@sRhaQ;O3#znNJ$1J(h40rM{VpTcH3--2gVrLA^gG?&^jXU zmT~$O`vx@<*(*8!y*dKFR=qA#kgn*CbZO%~sZ4mN^#jRNtz(0Sko@_xz%D}Nf>h!M z>kEW29BJD1k}zIz8|B)VYZ3Ed4`Ps__z;fVAbRQIh2eAHNI*Im7n zTPB9ou$AMf<#`Fp4985{!Kjj%*aCn8g8TWUlb(s|0d!oPqYRkt;$EfIX1#MzcqfXe z_DG(o@2oi_0nGM^LVcFNf8I-nJ;WEiKstyG zP%g|O%goTC)|Nmnj5en%%}mnh(y`OYZaJ1*E6iJa&>a#{FH-0>GTGB|A!M|4a5fg! z;B2H2T|pFkgKmg`2j*jwOdLnTE@jGZi4kxdJ5#LStdA9y3wT{rKsTn?DPEnp&F|CHZuR+yj`iw`eU}AcRH#ShOg6ijrt`F{=8(;0 zn(PcaG4_O!h0xds3Yk67ShDOfbKo=e6Ib{NwOORCpl5369aq|&`M@~2e^*H^W0j?= za)$1@%w==d2DQ$j(17M>36k{vdkG`k!_h008wynv2CH*=F@)%~vyT5HoTBc^iwL%g z@Nm-Q#e1E+o{O4^uwMXfZwJY8Jvi4^!T8+gL}C)cLRDbI6At+C306LOZ&ZWvA$K}*ub50G(MU?JeM^l z(B%brc;47!z4X77;sLOq?O-{6?V6NKO$wGOer3TAy>%inWFj+2G^DUx^-9B30Lfm_ zV!SqzvH#IK6&+aevi1Zm9S>w5Tb(AZlcdH0KtpsYq+sHHAO&#d>_~bZw7%4AQe70N zJVNy9C-yyY{cPnqVF?d3Gf1HI_4LYkWv?WK{H;|`O;o5Oixu=$ytLiH7Zn!=poT8^ z%f1@+=XJ^ksU+`uBS$75laNBGgT@>K4MI^Sr!eN{Pb%NfO+5erV5e`BW1uJbM(>~5 z>z~8ZA}AkujjrS_rbsW1{uuBGRz~00LujJD0WK@jifqvO;N6|`GOa{RFOAhWBGtBz z)fil#bDs(IyG&P0GAh_^%>GPhes*q6s8>aNNG!vF{YH@O)`;s992JRIrm)*NXZ89S z=f}gswLl)->YDays}^0RlNdhuJ-14algCL%Wotb|cSgVGvbuhJ8V@e-!*DU6*qO?K z`9@_AvGfaqS>H{A>=O}F=ns!{Pb>UwmL!bSpOJcqy}GVJ9zA3foiT!HK&X-ztEUGGlh|G5otr3kHDZ%LfxAfshE|D4d23 zxiybz?kFiJrCTXIcO`>LA}CK4>%fVBUL1>qqMOSCp0rCDE-M;f&j61BgyNSPXRJjY z+ag07i)-7%)_2xWkKg>Eg2E~|C>m5q4oGppeW33FmpO^;%+DwM0g|jpokuw*!GWU% zwasD72P=2`1p`osrO_h=5qYB-Q(6kB%f#3Tq3J6c7>#kqmE8T0cR@@N7A@=kf*Zg^ z%61=h3#EpikX8AIiKo=ux+8)nw0MmezO%Xm&}b6!DFX#e$n?C1?0#OA>KkpBPaT6v z!Z>sT7mX0S-?3KWqe1Rm8k7Xm+Pu>lsy z8H-r9fn$0WmY9dWZb`$9;8rcpEv&Y+F_J<$@X_jz$x^@BNbYt~86a$v& zEMN5Py~l9I?uOi>uQEPng$q8=8GehX_jsQOQO&C$^Hqt%1iEFetUTnD%bXI3La#z! zDCPOd4KWAV_}iNklbv%M=DKt&it-IZ6!qsIzoj*2hr4idJ5dtb4_JWkiU|i6z%fc2Z{;38GyzHg-8h#%T{e3C#=MQ|W^LlibXF!?FrWo*- z8|UK5CYnKJ{8>(NN4Pn3*TLjF`GMZPFmx1U3$YmQuAbP<6p~4`?~}WhImkRT392nL zzhXxk*Q$zhCp>1jfU64;*l%gZzt)Q|ZG>^?m9>-}#loJKj!O;K=fGY{52m8X$ugI*DloG>Q(;wxCZ6zVRY2al40XhN8WI@=&?!dx#Yf?mp*uhqxGDYdM8F%&w4v z!Qzyp&40bD2$%q+0bk3WbjeC!QfFgYl)j@{H376m!1N`Df=j9|cfvQ0&1$LSFxKfw z{OaoHjUj~sYpM6((Mj};lbnbqAOHSiBZdx`{v)6l8fl3p=bXjcJ3 zv&Th{HUZqGMFl7F1OuLRvmRf1vU-rzv={@yh$nw4)fB30&eo{_ZD>P=XYvaQgEb$* z(!^!i=BAy|D}jHPx)!0A9YVGip9YKK0*az?E&MxGed zB<{X9x@;psom-8c`7zm~A@RYWjMaQed#<=TifP#qkAxQJy8XyZpD$|^sR;B5Z8&+1 zV#bA&P_B?0w-@|jA?$MUL07eN4|C-p=;8shf=7S!W!2_pQQ+EhH>>OkB9%P44zTy; zJEl*boQGiZ>5y#)-IVVQXc_m7@SRW8=+}~Hs5W#u5vX)TYp%mkC-BgSw zCUSSa*u5VY2i20#h$Pvomx#deh5WkfGjUv)-m{sE zz8i|07y|Jfd=Wdg;jL48N?G)j^5sK-#M}IZ03;eiUrkxzHkC!D zh0}^o4Sq#dH7l%K6R~q$_FoM{!^*h;VWk?a(UT1r1K8lsxac^9z-NPv@zX(k#V^I= zqMT_G`+!?SjtG&QWSjBG3MYV43uS#Ng#hUvoP&tWL0f;hVFSZ==u zj0Dp+I-CyQM&D`GuEia2JufNQQbt;p3Z|a*NEC9>jjRA<2JhOQr!Tz&w_pUb9wqjj zIQc}Lw%o(&@x(e)K4OyyNvRXwWCMHJc%dG*#g*_41--Oh%5MzgPIuGnNCcbKV1g5Y zmp40(yhyc1|G3@uZgo~$!l}4;oypE-nGbcaQzr}KIBM9ndY>Q+qzzbe77LxEQ)B1S z^_CIbj~4}W3LZ7~Hmx7g_YohjY)|!b@tDS*QNeETMW^UP%ZXO5P4Eyp>A%qM(c6(1 z%oYgJYbJw}yJ5(kQ21qOXknJ*Nf3^9ETEfJZ@*H7P~NAaw>-TcuP&-l3xQ-#tL&>G zI#xO9?byZGRKM5q9!wb{$}}7hh%Tco)j;a{1I?n#w%iOeEE;#%zQ{#%ZC>z(!&wX- znC4}Chx@*3M$R~nH+|jl<*^+sWaIl;Vgg<1#eLGq?pR94+f&elTG|Da0 zvR{|`3Z?Ua2BR6?rNBzPT9e2U<*uv!T{qlVZ|!QVS(a0QWoWGDvJdGS-%x^w%(Y(j zPxktS-e46$|F-$d)B8K%M}qH!sJ;_SiJIEtgTvTOA!^9zb~#AgG%?qR8DX znnMXN5(%QflAH1?h|_7mVa7Uh=5{_*+QhSj4hQF21V$^O0QpK~o9L$vN<0%li3TQ` z^3RqG_VDD0bb_f?I1Q-{joR+K7(jvD9-vmoRERJ?mRhG$DKf(rC?;3~sfSSj031yj zP<=l5DiR^Q5$p9mBtL1-W>?$yVaM%`+urZ0KjAuC+ui&EDVeuX{4Jla#VF0$4G?=b zeyWy{2JB;du*kK=V9V~)N<1*a(V&(ey-98I^;$f942S9rwJ(tc5e^i+o_g($OHT5Q z#|hoBkbm{Ej-K#`h%F7RSL+fBMTDk*dpQv-&zPRuXa&Ql`#^W8j&m4E4bz?G^-GP4lEOZWN1D7^0>cNp3;u( z9p!nc^#eKzr|mM1eXs_?7S8Z;0X<#Xjv^(2E*XfBC%Ml~$4aY*!ovB4$z0<`t1U~T zxUd-=Jy^(1iS0jH1wjB{pE6A|M-EfRY+)Dc?lma$&z40(an#^#CxRPAB5rhsD>wLx z*gZzmpjHMHJf0N9N-5oIzD^QwJzH(NxjfyU=tmFbpmV$^3Q66HCxxW_Do2TwuuQ+o zcQOSm69p{Obg4q-$wKC7<}@+$UnSd|F>d}>;Ys~7QvClx`%etB!ta>R|Lwr@Z-)6d z!~C0J{>?D|W|)67%)c4t{}~K3)vuV(aj$?`fK)f2XCS=sBDvC}1;j)Ia;+Hu%rNgu zZ*&ETK1_icnF64znQ_@a!gG?K!dmxm|KN1%Sir>GB)UNV01xig`*3{qMROT9GEKZ> zzK0mBUgg=IbluQ8xQcv+wYlx}9XJS;yGYCq!f)thefi$n-jEp`)+r$Wp|%n^h>Cys zyYpKHzxCk%HCxc8+Tmj{261)PIvT@VZfyr4D_UTQK8@`qLRFTBDlZLFRu-lz&nHw? zMkp^2i!P<&o4`hN3n+N2t3wa_=Z=+H>CBm(fB84{JioW{nm^Jpceop zOMw^G3J=h*&q}K!Q^>A>`^gGzXD?Lj&G4WOLFp%r)z`g$rK7Lbc2mY16)3(6C7K}` zc1~<`C3@2i>lJy7BDZ(yZE8gjN!}U^YIG5%rTbziHrm?$nRVRO6Seub)q2Dq=F&u? zE*7r6WABSLw0GBa>?R}@y-YV|Ff3~78syK#TWKLerPxw@(f6&PDKASi;94756#ScN z78^my_ZutGHK^#bp#j}Lb?B>*?_OCnza!|FsdKCr24o!gS$V)CM^O_5hVvXLU4y{6 zzGOzNYRiZYg)8+l8+tqGFa@+3U}o46bKnX?PkjFEaIA#KS|enx`d_u4CDhtYSXAr5 ztnhcUKcy-M;-SZbavPG;&LSvwVLGZ8fCDBqD%-vO12X-%?sq9y#tO&OA+(W1T=ZJ6 zjhxsA=1ig*FIlHVOkK27~2-hVWg)bMb0-sF${p#ss1hV+p@MXoZw`rMr7I#{@F}{fsj`(I25o3h)6R(BpMvvk^ zuwylyL}Yn*5wUYyB_EDa$zw=QM2Fj0MxP@$n9KpnXrQ-K!&6N;{EeLdL5E!(Hxwou zyZ-Zo?mX+e-_8*GS7G-!I`XD9evg)gcQV>^UGKb&7Hx}pH^zewLR0KdX(VDRgs!3i zxY(u5u~FiC>%y0~M0ip8ngAX;Up=n%xCz8FWi)k`OE+p0iw1ue?!dg||AJ(t^4=kNi^f9sUA}qJ#`7p4b;q?I^ zmE-@6QClll;BX1t4dUVE&+O{c3HrL+Z)(PC83&^MS175i{%n1eR(crgQ1%#w*Cnz= z`%2Q2u82`+mo|GlZV;e^BJ)7gaxMx)l1FD`6&jK(XAfo_K7N^oi}e za8t}rXcIp$y0n<%Rq87-ux9%aEHz6#0-uw$OL#V)fSyVPFEXU{{ zyro7Ityq%%aFcv`0gEV3_~LCz@$h%=^jDS#%kw2?w$|S}jo-Vi-#gCVyYAl~HhzED z_`@^wf3i{>{DO$z6#V`>etZG8J(EvISnjL%j5_rK{Y0Nn@yt}130?0M%yQ*LK@7_n zERn_!GgM&d7S~GMY+V02Y$&c^=r;EgN+N>pyFQ1TAQJ$~a^)?;ovH%sg)}yOjmL2Y z^RJ`_9PiU=s*+eMpEX=#7^%9u7>9*ek&qgMC`NWA(WOuRYXy;Yla`zFqbsjO49F4Q zqCqG;VGZCA!6?dV!P4lSEt=L|k)1gr_nNT`A2I zikdX+*eFDpU)(G;N0>)f@irP~N+b;@W%0)(wg-AR9{Qu+f3CcajL+RtJl=1i zIt)^HU!R?~Kbfv1ARLey9qF7|oJzWYqD)XJaly_PRG(3$*K)mkGS@sk!x+>!PtO~G zmVui~KbN&@bbL3tH52GWcns)pLrQ;Ab%VpFUka+t(ahgo!zA8WNxYH3ixlr?V`{7p zI-4UsHvu2j&Mx3A#vFGN1Smk)jD$Zk1wYC*i;3rIJWrfTVS2t)kaTpcR*K&I++opP znlGh&>tqD2Z}e+IlzW_Ym(vD_bxNxc^oZB*C!;}TD7LBBTg@?}JSOP=?ClflD7-$# zO_KoNGg#P~$U5;xOCd$gud_lcX&i`6R|cut`{ zbw)a@*JRyGH5G)hu8dFigqm1R?Ap^CD6~$ss*gt@>I6Plj$+-mf8V|kr_qe?uvVR> zBR}%Zs~*#=_hLAL3ySpW!pRQba6u^b10d=F#bag6YNqNP(SrI+2N2?T6hg8|PVRC@ zt&*R8fawL{J6{BUSHde>tJypXk+0mwT;BnA-(*#V;mz!Yee^Dq(QEnovf4X9DJ~Wr z2V&@hQ=1`qSlMM*UY>iOx#2`;*- zX+EDg05zjKzH198HT(N}&D5rH6H-5~D3@m4Kt!5uzX3_$)oJ1D2#D!2U8ZtA^cP`2 zg3A=QD*=+?gc!+QiFfNYzV1!Ow;zTgX}6{?K>E1eOR{{`Q7HhdH=jNBx_$s)BQ=KF z7sZh1kCK&UJUTgCuQyD%0NOw`*Y6MTdW`Q-mh-2c1xLD7JWyc(knU^%(GTLdA4^I;tkFVi{eOmX$$&=-qA?lR~M z&7~Cw6g&b=#I%RXj2q-_%MliAzadn+aDFY;ZWfLZX#%THMWheXn~w0?^xPfFhBn>H z&Pws~*k7%9EYT}OT$${$!l?xwT1Sa_H+B6wH?SoGA|>V1*C4gfCb!sl?8tNV*aK(i znX#|%lI!jJssL-SHRR#VVZ6!*+Xj%z`F+u9cHL5<5;pak12ggMq)m6wN({6KxoLe& zMoeMdK(~OqPQVTT%rz1DZLw^9v#|CGSjVIh*em5WS>VU@$dFcLN53QM{GKrWP7dH~ zG@XAEet1_ha;Y{>p5U05MU9vD>Pd}Euxz(olx>@IUo*X|ducgp1+OaC!DD&b*EznA ztmmIBMBK)U5^^b2G|NMXk_ejz4!dykkF5?Dwr0pXiDE?1Nh3YYx2w#vP_Q~m+ayom zS*?KzfPA~$9B>0c!dK8+`)Bmpj4?r=yK^;!A{N=HNBY@idgr-A!$S_rR*`wpqaZ?f zDN0@>Au~A=96QUME*+VO$-tc{E+_^`uHPspvo|*`grrjdCd46HAT#H6m($uP^DJD` z!z$OCcrDju?W+1Uz7>^O{O`&vm&I!*B`;Xz;Xqk>ZZ8|+1v2Qaq%Y7QD}AJkPRpZ6 z&8h~X-b|70PP(51$|eOl&ng9__L#+!);fh&uv?RRkBS#J_CPT<%6BsFs*QJa&*TCy zq`4y~C)DL+Gx@)eqK83@&} z5#ro1b5*pT8vp>BG!#9qMKcn+9KQir*fzR7Px%9oHp*b}vV5b0Jc*n(lqCUpe|T-~ zG@|FEPS**C<{}j<2$TQTE;v2V4;u%E$4wuj!Y|vO)S4X2ml(*CMmG84RZ{V>2rczTJ+_mcBT%}Fj~=AdqJNhSphI3i=Sq$i3?N*& zo!C3Dg3-n2xRlNJ2kT(tl7QRPjd6`0W_Vw1a!! zAp$Nc$G9&T4=l6x8$6ac7o9R1jN=7G#w^kUc%7Xnz7g||T%RUrR+0JSXD~PjQPev8 z`g=-+jC%?wQxQc9-tokp>5J!s40}hlq{2eem%mgf;Dk1oEiSXfv|ry>rVT|aWT?^$ z#lbwTm_lm`1)#t{Q4oIk!DL3Dhb!Ml{*@TDb9U(UIo=xFMoam?Z@K6rPBU`+^{t+tUZk zQkY61Az8PH%XFKF#HLNoCJ9&=qJN@+RiE=gwUGGnS?++z!p8y|4KV69+Fg?AV}rNP zr!FHb1urs`p;u7*RKzK#iU~QquJlOlT1{7Lo-GF^j%rB*0JCE%Sh#HO8{E@%0L#gp zwjX9+0yC_72Y47lNnWI89FiuEfRQB`T^&mk1qe1{YHZ1w_S1%^K+UkFLNCYwyPPlB z_ZznBNBcdEuCRo_N`!);zWbXCK;CJm@szoCo;hkj&zrbyhmV6pAE6H5yAg=WJD`*M zuHb9iVeuuO#UqmL`RJ?5(vkqOFp+csQo&|jYh9G}`aB{7=ChbtB09J91X9}RH10~d z?Z(hrT@XRTN=3cJD>Wlaw@ETlvB5{cM&1{#!)K?-N0SOj$vf!2&qXemxLp?fG_#H{ zM$xIgFKBRT(ej*@+`8hFHpf{LfXs|z4bCZU-h&hrFl zOE!sE%Z{Nf3X(4#JfZsqvC?N{-KO){1*<-fJSfQS(&0Ko(0Lp+>q@d}1h9dpr$)eE#ClYmM@1sMEHZdvV2VAmLLtIjd-y{LxI<75%X#?yT7QF7h<=3fJ_(M5^DM;Xd)%?%4IKysk&T#hwK8c!x+ zcBN@iruwLDVpWDR2jMh|w*N9d^7O>Q~zZ9@=Fr%@Q=`o{` znGUTnAFBbam?&VWVgEzG!hsScN~yMsw<*wx;ghCx zAk1rT6l*NtQdySEvcA*W4y>2e3ux;3wsPop%RBs=RN(_Q)&aN(D zjrmLrvUD87Bwv8JnKGl{SGq{ydJa#(D8@_~a@6K+Pde&_KAl!C?Eo#Ytx23m2MBXj zQ$AD2eQ^{DKvZzqY5Xd}>L||do50MCb%aXK9Z_-o7*9|itf){-B`}PzHku4Nfv!d# z>PKH&iPjLdxu{WIMor_Zcu*n1UY6h!aIpEouCTG*j-kL{s>M=JQ}~O_X`UEf9kCQT zGPkZyhNoPp6tOwMD#|H*wxHJ}pFq?MRnj7%+4JR_q!<;XB-v}liP^5&_Gm}JnJ~tI z263{B`zcw2Ihvt|d|87B&y-`g)jP4(jU+olGM%#Ptm2;5gj5{#=bh7w-1%L!)%=d2 zi8@b@R?5(`Ote8(8gJ}EDKyTo1Py(j(>-TnC=Asd-?c2|!8(J%0_g&e+Q*GVfx7^C{WG~(bcJ6gBX0lI$fMbKKwa4$168a7?I!3uES2|p}F0s;be6kkeQ@W9?m5yn5__q;48VAuL-J^RXhMQ`@Y11-0GoLyytBR9wyzhl(N8m^#Vq^sZX`$h-2VY+>V87B^&@h@pNCJX4 zlQitOomMDxrXQVTP1!O=51aHo(Myxtr1SkCroz(qhpwN*z&z>hUnw%9&`yk0Kl=ldb3BF|OaQzrm5z19AAyKCsNzPi-8Is~6ckjU& zHOW;8sg%7d>#H&DB1kzL6d<&np4c==E87`u&2#hz{dX-3!r@W>m^nssm8yjbnY*A3 z!yDrx(+r}#F(KOKuEG%`mWi{BXWRZh1z!ZSWLvaDnmAP6;IRBp*yg1;eBu6%1A!NMm!ar}<1e~L$ahiX-b zI^k|Sm@WS$9u@L*5kAL7W@8gN#Mag8{>FLXOc8{@PK~3urhIl zu54x5RRga_pHgcH|7Pf{;zX&|l$SQzeHZN zzw?;Dcn;(whw*sg6@bEiFnfdDbZ?gJ-23ZaEi>yW<_)BzX zv0u@l3yJax9L4PZ6dn2lV5Pt1ok3Q;!D*ob!WG4MGl~b>-{zuZw+H5Z{h2bFH_XS8 zCM4vh>X5nf;eP#6VB4iZvmgI}W#k0#5b)zNNw?-b_^q`o=7qqg0qEu*WPOK>65I!9 zPxnh5{6{(Af1mv=oZq_ff7vu5655Q=1la7u(@Yw<;8aJ?f}vsymyJ8h;vK%AYiX&e zSm*zUjD-5$sBLeruihM4O-n<~I_o?%6l}6a+uB-Nx&Cz-9St?ZtbK2}`dwU@Rymy7{AawY(zr9=jhywJ|A1s8}Lo?yV@p|^G-oUm^bgwXU-ibR!} zoj@UM01FkXWA&{J9>VSaXwE+^Q%mv>vZND)$P+U2`;}P`o07t@AJ@m3m<*Gv}X#h$;n7%(%Uy8mMLlK5D1m#~lpx5UaQwuUoXz!;j z%`atujsdzbKRWmx(@h|=A;FmLhR1%lWtNFbUQJv~U8Z|#M@fBgY$ z3V~G{u<12 zFn6CR;o01pEE51`K^iQd%2~(bud_dSKm%ImMvQQXRi`t&(JwP2|JK(29rGjxz2NLc z#`x>oAxn5bv#2IzMF^Vhq&p3j;|?K;$`k_kRnYv@xc>Dca6FVdeL+wVAFxZQEFc%U z=CorbSA-u=%6}kXtU;0x#6cYeTpX}ej6nH6ufPjv!iLcn2IN!)f?X!FkFZ`vcqrY4 zW)R0;V@}LMeuNA@o1$7ZP>go-IPO&6XGg0P({#@x zC98GQVtfwuFE($D`ey+){@MIpX8-IA*1!IXYe4uH_xX3;G)g?+FRu0P&;70A{eAWi zzTmg${`64(tsB2}!Q z*#3R7%&$nUxAwkAwbndEmV0su-beSyysz$!yQxS#-Hb?hNRz(miH|S7-IO7Kap)T0 zcpE1^t&!5TF4I$8TB$ltD1>WXGM4`Kj67Hg7{gr8E9!D`a;hae5hLcu&ROWeXqz}7 zPQEg=d|QSC=4Gz}IYA1wnw zR*;;*`FO*FhhgO`uuH=0)-7la%<~e@6Vpr-KTyd?rrn_Ecmdz)Q=iGZDd{cSP7Ajs zjj$g*x(ZvLfdm&oZ}^%mA$Z^9xm_OY*Lq3QuV*4Z;qT~oKzP?OY53z<)up%Y>Vs4r z>XCW5o-CeU4DR~#qn1=(QM|s7Rj33wC0DXRVkoq(g$BEfZGqY*@P`OQkSVNRqxbqM z6tSW)!KhcV1IT-2a*W;UVcmRpnUOVs(lvu!uitsGg9>xj z#y;hwFmxU6)eS}eWH!ynaUV`HkPjCj#rW47KtM$L{6*5N7YW}Y)3y1S5f%msCkUh; zwlS#$C|jQ;uj?P>C@?K3dcYh3WnVeXQehh57W_m!l_bV8-4+PCr$RuXfynTwi*Fwk zP(3Ub_m{)ceBt_Fe@t-++yb|4$hIi;bJfnQi#G7AvVx&Og`^>V8N}sw>jU&`vm+$u zMIqWHOEUs&!@``adEb6HL}L-Em?O$yfd~$#4^Ux_DkcFKZ5s)S3H+&Ns+vxC5ydFu znlmckPJjaO@$Qmc8R-M^lqy<%Ahm{OLmMG!BS6MW^istsk%#9g0GafUN zY=A75->J~lFJrVeu7U+>%+bu6<}frcFT%}5f;e*M$9o;w(_Vf$@V=DsV{jXIOojQ6 z+YrewU2bAs22sP>wqB=TVp{pOoZMDMvxep|58j<3JoDn&lf-ABAo z-hz6fzYKY!JO^||z8iE$c=hQFebniG<<+Y(_*ARO&!JbX^Q}~!mHkCe^(;}JluV+Y z>;6E!Q#^wDy8-+NL_~T_$*$qBJ{7>GK9+%u`w)LKIKzo-**^QWBX7;c^ z7H%~B6w~RQRpawvl>j89*B71N3VlHdogAjcHJ!bq5=&nZ-n~V=kUOjv5aAERXyLxC zWsKLgMX26oBLtp>fAuPxQC?j>83vdC^rCO>X`e|4a%2Z`!F5N_&oED_HuGzi*Z zViBqjC*rWb{_#|cO-Kx1cu=npQ#J2SNhzvw+L@G8P~o&CAtArmZc9o^akj~pkdWkT zlQAYP!Pz2xP*{YkS$w1LS8o7;aF8hP(|brg)s1*W;HyK z^@m(-+v|PV4c7^;P$3KGktzYD0!s(|DzVby_y)F0Rd84)HaqEDk|o^5G&L<(`?#-? z@3+r>rDp@dl&-_zXmR&5zjC*naVMI#80+mLD`$Bq*tv5p>li(w9V}cj5Bdp&FvMOGRNDOerM0P%OJq2 z0$e_cJ4RDSV5@hQK$inp?eUQim9ej?6F<a~(x z0JVMd+r38dE#PH3;(g!N6L3>p4R^Yf{{Bmnf@kwpsBLm2$2vqg;1IT#ti%vD$ZMPx zpnw5|YjOgrRW3+uMRo*J1;#00KZo+^%-TJ$9903yjd)}5Te?haC!h!+3N{MRVYAe< z)bgpgKvjn|)kX1POn8`?*?0SD6@XCK#d`VA7%>o|A0W5`(TolW5w6m!RH3Wu%P*^g z7MLj)-z`(4`Q}k73q~$w!C!T{WpsxcYT17-`xx#PfMjV~#X%RPi+ zWYs@X9us1!y`S?O&4WkKD`l365?Ilqyq+QoFnGnMk}Zu4b@S&c<-q%wg86#{e2DlX$j zHCJt=_+jfP`oTdpT+G={3XVqi8ImP%&DIZGNF5p8n%5Kf$o-(9SrLnm-OLOyy$E=x*i6S?*Emehw20tt63D~9 zX9_au#yXOb;Q5)Gu39M#7l>x}piabh^{!T~lYac+tvZ`}FsT*%GFZEP@A`+bK%z-X zGaz*ZJEAVrdn`!BQ|1$l`lfU3LM1~~{!tm%!gjYlp6>QAwJ|jyX_n!yzwoStX6Ff> z&0Cy8*4j&*?vR18n1^=`Rb-^>>?6*Cl*!*1dPs2*W^n~X1ug_JR$jjQxuK^`fu}68 z2}QhBzI2iM46K&kM0)_%5(FK&F16X%l;6l*d{#k0fcdq&RYu~yUKt)VV zkm-nz+dz9A_1MpZmmLCe1{g~>`MxI2+a%`knMI7Q%g8Cwh3GBnb=iddFBD6%(rmnk zY(kr|S?ZN^l}U(35^ZSDIieHjF8TFTpq4D;Cs>Csg-)?zf=7%qNf1$rNgE5YzMBin zEJC~UfqVvy?9Og6-+n2*3X5lYjSTh11%~vZQpW(oY#Y zm$V+MTvNO|rhr}#RnY+41l%Iv2!tCJrqI~X#G{P;Mh*bLyvxI^;r>oBC(2%dwA*b? zX&fg-z8cla=}X(9O?i)+JtU+Qs~UiM5VytQ4xG3ImI%d*g)p+#GGUVy*A&~;>vu78 zBfP)pgmu}E2khv0_;JJjkf+C4uHLs~K0#IolOz*kkI#Ti1E~w6W@OCV;fHWTa4`s9 zrWSN=B1UFp6{OW^vWoxpEbIiu1k<;>{Z(rt8tqrUlJRK7sX_CEsPwd&Dthve3zQbU z=j^npDR~a?OF@s+gIK@kwoMUrx82tS3CvD>uxh9dE%HS7$~+<|*qkVL}@nefUtvKRWI&@rxXBB`fT&yrSEih3Vu zJPE}fG>WQ+uF+whCv;4e$$Fm1cFj^6PBaaBous}K%bRX0Aoyd#%~&X{_8eux>w3Tkidd#?2N)-1W07>QarONo~ZX4o*eqSqH7a`4RzSZ+jA-)Lh%EI zJ^svMDw!nCbFTPKT$a>0pp+}v(1;dlLha_bQ;dXG_l;Js)nx=#t%IJcF)o+hwy2w= zs3u)62|t;ez2wBY)>rJ#>yIlh`3RrI_y zT*I&4%TnA!ComVY9M#tI9qjAB@5CYI*|V8HPneci zWVu=o2l_0VwteNar$e#YA+dChutozrtZkOm?F5M; z25`{U2#}s%is4j_Uhfv? znO>b7d0SsmPMa=vxeBq=DQJObHO-P8RJ1V)JZa`G&vVQ$_H8O9!_5Zm=KkNVw)m)vbcn1%PxbvK6L(pxsHK1z zhWSNZGLQ)qUSN@B_0xM+-xteven*bmL2M_MIMp*Z>_2OA9fN__!Z<5)EUai%wH_`G z`43%uaCh$4xyKV_Pz!*1fJcdU-^?AMSL=D2*~LuRcN&(ifDYAF%)8ixZ(q}JE5N~_ z67=ImQ5&Q@KKosMXPFsl0pppHATTs=#h6{G)OhPlRDvgJzHVeT*9vJ08r+vQ1cyh3 zNuIeKZT^aGjx{cLA$=DVNFE-<>C&z$WH(^Pl?Gm}REv#E#KDgb+2;4SGddnt#Ti}5 zyC`ObP+<~jTNDwr38X!JB|UDzXNCp}oZs_AvAqP_ySZmF-k$Ca=f+imkQ?icb&!%U z2P(NZo%-Sj000z$QgkW=^y>qngF{Srm>7D&m09h}uFf;x^qrZ3ha;V>K!#@U@`S{o zKFGWVc?}GswEXm=1(L(Hj^uFezu)2i4@mw*O%Hxj)1TDzCpF!e-qPm51@9?s(g;hO z-^=5^@I(?czrQk5VZnY*&wpg9teT3rFq+4{U{0pb-0neCfYAd{ad{O~{uCS%@=J7I zF%&Oo@wHrrWuZbb5~jEIlbZgdra!6aPip#;n*QHGO=th6rlGxQS*M?IOzxG%hOW^o$kp?Nwe&o zRsfROb9A&}*GKK%jXX_)Pc&~;cgN>z_s`ev|7%~n)BimN{J%x*28C5t3uLyP9pr+= zm~2Yy@a63K!cMn#b&S^keZ+KhP1|-DV19k;f-xTZ5wzh`c4BH;?(Kb7M8X^1RG49U zzu_JM;=ob%8R#s@5d~D-VPM+7rh7MeYW@}oX72C!`w)$tTSCV{ensMT?;ADy=)7|6j1le~M8z|CvAk z8{tu4GB6%5XhtwngqB&TJeH7;9h%d9aVbL?kn@Ll6LHdwT4N3d0%jY=ApG%-v+{my z<6qORLC|HTe;dtG@#Y%`DC19Q^XHBe4GE&kT%9MzE>nyX+o_6{jcLZn zUDPG(U{rq-?0?IB_XE?xhQ?Q5s%*u6yeok#@#Z(CNbrdtVnb1gyB8PdrYoD&dfX26 zwC@oWZTIQ8fMA1|JybhL2i8-{FW%(r8s$AM_4}{#6Bpylatub^rixC%{e}sE`}K z3aMxSAKr`TtluF=t<+?H|6x-2zZAEC5b%Jl_jgSgFVMGHA(389W9!NngA(1=%kG6R zG_IFWtuVqW7kKdcjLPH}L?0+RS1n)T@bC?S0y66=$*!kP&eN&r$E9A+%wDrN7d|J{ z|FK#-(f@$WGYHvXiO$ia2Cr`rY8;NCf_i3)J;?G6sP{m<83Z%0&7t^9xHF)D9Qb=S zJu0=lW8kTx1WSX0nv<@(NSr%Z)~PxsXn$+ZOg~Lp+gmcKIwWZ)c4cQ?2Dml)+vfh= zBm=4k;)tyH5T1dxpa2kPhU?_RN?+X7h$tjs71HU-%~Jc(D!hWB8d8n}zgrvo)M);z zSdc&Ze;D6Chz2WvqvL8M-=B3_?ymg74eHZD_64`3cwJlS{|Sw~@(-wYx!(}{^1(Vo z)PW;==Axay`Qb{ucBx^Mfk4=%TGW0 z^rKHd`lrEo|Fj#i2LO2gIQ;!K?7!gy=)heq+}ZOR096z4U?smxZsC~uf;-{?DfwKs zXW0}}=Xk4{`cj2^K%4bblMaBygtPt%pVZ4lCCz+rbLU4}3`fQ#aC=Zor0Nvrl3uZc zOR~^nf;^A{ZTM=i`-hgtD~hKe>(IrGDM^LJI^%5U%e)OKw#Vi3UO@k$vO3`A#j}KZ z=}lesmaT@Gu>8}~^3Y~^h6vIDlqwMeLDOb0F7nEOT~e`E{}3P&7KoG`0)4m8e<2G6 z=$*Re;xP98r*cF<)a2q4|IFJ63kC-t%qpC2aX?tNx3LNC@q zy(RSM|MK;+PW-CSn?WyI*DcV`O)_g0DUf8`{$-`ZM;0^y+6?$C*{>&F?izr%`iodp z65j(TPAgV#Yi@XEcj){ApAAr?0F2=GwS&xK6y#wK-Se+XBWodmjnue`u*cE6Ok&gj}L@?oaAkpnQ$T@#|c?f0Ks%K(cKBa3F>d}+eoT_NB*Q%*( zX^k+K?#UqPRx1CcrHOP8d0D(;%(2HN!U3vqUkiEcjwf&kw8HqMD2~NAlJ?h1C4ls= zwHx3J&6Fe!QzCiIXJqLB)=T}5*VdMw{~>HZv>R|!b1U46pI-^?8{wyMvbj!jMP|*> zz{y87*K%%ON_`jjC}}gggoL@srDMFBfv;P6)#W)`P+WEr`V_%SCuC!f+DDg%$r(4Y zLN_y%70<0vvk)J8Lu?ZmJe7M_U1aThmWj+FBfveVN_)%TWZL;GQo(k4{7cFoFX}B& z!YrUOhKgYF$rWOG+PvMVu4HvTrr)%>D6KxWP(Yi%~c!Jycp_H@q zt9O=91f9>yARu+BykcFjiHo9%G+mVOkXk{noHFt}1@ovym{riI=d0*b_``)xEmW+v z&n(BC>zk|>r9K)A>30K<+C+f>$=kpJT?i-dbnq4dc~v&!s5;B`rDH9~2zB0);-z;R zWdHz>^S6ps-{SM7XejO#pgS|SVT4(s{PNfjy$dYR8MtANNL6`cSy9VLN@!=E7^GwoT;mbCFAh=FPrXB%=%)fwV2{e3Y@3=`W@o~&W%=bO2L*C3 z6aAIxQBFv{g>Gt{(=2#NN1?PiXK;TZJ1jMpp)MH!#U^j79@3ij=;0n+FUjDp z_P(z;nN{jpWMRdX_J~5G|5#@}g*!AKlZ%zK?)RLwJfOvxko)E*(|GVT^lr*C;Y$Gr zuG~zD;4ch@BX2Te0PUwLB{#mjje?vyDs`X$g{A7@sQ!|7UisLfgzMS+hoY%DMPJ>;t-DZ0+?(#WW@^2xR+34EGSj5T&#{Gfci)`&tR{Vi`YSw z;VBk7*wx0;l>uh@V5JA7-JF9;a;PAuCbh>4<{7#w>w@VZ0T?(N`TjcqG|RObX$vKK zOmWA8-Gj9+)HP1Y05R+#$bKD5G3NGKNmeW26d$J3u^jG z?0iR?!59nUY!^1e1r^9F!nz&+kg(e&$hkOg2khpVCf;?9$NO32`;}Vk_QXuI*15qd z1qvP5X8!h*bGQ0*O@0dm2+sBb8+Nv)D*6a|0MvL;xm5%94G;cKdCfO<4jcC2O&Cdn z;!<_Q{*tjoFBF5D@*sHP=W!Tpk1>J-alNAjJj+_9?{$;$GUy`;eD(~FUf0P&`^rJH zd*#Xk$(qzBWz;G(GKek6?%^DVgr&4}+q>51fZzsAvOFG4Bg0+SeiFSz!8QN~+Q=53 zUPF-Ec>fkyf)6&SN3Y`@Y{$9O`XZRd`q?0}a%VHsgB1bxL$sm|0my9RsIlede@<>}53QP*Ml)n#dU}S4aOVl&Kdv0=zXU@da zCyT(31sqKqlW5!XP|TGR>>dZ1?P_<+_3l!g(k0IjS7$?drR=pfbB4p$-4~U}r0tXD5Ps9ow<}N=p!8n9&}}l9lA`V5+Jh zc)aS_bi_7Z!#jXtrwGY^d6%#r=&}u2mp~H2I4>`heUR+3_XU$GxUM^Ns8Rukc=WU7IFSxJ`@`5UJ%m zwF@-w?!;}B!aN+n!-2nS}+b72q*a?H5pLRkJI1|uk?EB1yFY! zrlW3P$dTy+b9e)F8`3)xtT>~+LOKomC#gN?m(Di+nUi_?#g%l=cGqA28>~VB@~F_) zV*n-_*~2Fx;q?IpJ~rrMcmkgEn7(;dX;#b1R8}(d!btqylNic9T3;GrQ1-3St(U3MiFf+`jw)N@jDJo9aK%sMKi)C$0NeHjR|PqLcUV!@`0_DI_2a`)OO+d+O_ ziR0EmYRI8FBUpLipLcnD+3s+cOV(nU@2{V0+Z&L3`3bUiA3vuiJtLLN&*O&#rn<&f z$$&rAE7;@FO)gOchUo2|;Z4wS8tzgK8<=~<@rRW^Fe=RX$;UEPW0qZRmqB6qh!CbQ zdFZM;|D9x&iva-Cwbg1Pq};AUbp!Y1oW>m%h_LeBDx)^}I-t$iuq&&F?wZjeQM~1X zr~`s0>Pm#j>=l9n1RuBAjD$8wdpCuk(a{7ywd;tyeLcXOtA3PSO|VW;x#0#@dQqZm z!0=X)7+u6U`Ji8w*@3zcFGSKEjH!828l!rx8*ukXjAt>ao*md59ghVDD;&sI1{&+S z=5pdE5Jm=>9Gee7P|5--T#|5(;>6oUAf^=@D$oT+Nj#-aB99CyxBx_OFdp-{TIvFp zIhZFAiu+|MEOTHF9au5Ul>ufVJjcax-Y#1(z;gDihSBiqI9yq&>>_UNtPyr|9Or9I zMaoR+2=>g}6|v*(r)tr?cf8ru>y~kAbNK^j&BeC`hp$E(IFg{5{FC@FsgPzvC2s~- zx?3yuW;NO@C1r(c7fiK%^Rs;2rA@fFufE&CAjl!sbJm@q3#W4f9L^UL-IT%#ooU@3 zIb^MrEJeEmBa3?S+{&9cC4Of48-A<8&rXf#1&=8dor z6rA(5njo@DhuH`UPlQ^n5IE)I?1aS^!ySGSxD?=RMI=-r?$-#NiSYNJkZKT*>4mRE z1qM*aG)N}*AT^?cgD7R0rBDZu{-s(qIp~T24<#`!h0T^|bt#1nGFvcC_q;;eAX7Em z0SNzUX?;=k$Agi(SkY!>-D#B+;|g2)LEo}B0T6T&8khpG`bu2#4sYPyXkAWQb%$4a zoq4dHo50>^pCI2ZqVHyNX`y-4$3Fn)^B=j_BdFq|GXRL-=Jv}}g04ZxWhBTA@Y~~c zG6A%m2%hUmI#!|s=Yx}Om z^q`=&RaJmHrEZR)QjG~CI|Of$w-M!XmU0il0_QiWThtq)J9%xUgCNMm-)oCR%u@g* zPMl8^V8L1*NZa?-+iv=~R9bM;a=K0~$rNi%kxrVDMFV7VUvqVQ7dMqX>3vK zrc*)efygj5IWi=T)kC~MhN{c_y@n2Ag*!z zAiB`BAi4lQ3Yjn3%3@1m;e@c$Eok--=#2!yC(F`>V`+1J!Rr~d`-7t|+4JwN>c}I% zqa+qw7175=t@t5_}Q&*pzK1|$sE=Z(8X8)P;+mR>F{WF zcBcCYR;arLR$|q{1qi)AKf+`>eX0jr5r|C{yK;l4Kms?G?%#fzbBfdZQ!@1~VxHns zV0aj^ck~;*&79Gb=lUrL9b${X%kPzo8MHqik$7tbS+g^#-uZ`$8l_db)~ZwExen6D zq74ribTs8RqLfw&TTsetM9i!F7YY?(|M7s}e?;a_H23T$&HYJpf70BaG`AP2>q<=A z8;7e#O-%bv6;9zF0U&HYJpf70BaH241wn!Ef@b6_L=)g0J4|27A< z{V?CZ&w;(&^z@#FzLHtq-mxZ=aRH8GXT2VC$Nd7nofD3I`{eZ;RrhP`AX~m;<$bk% z)%gbD;v}1O<|vz>?sdZ1~5*)g22B#6eu}~+6}p(%(A9Tx^o--aBqEU{j7#f)>tI%+w|sY5R^ zF?vQq)I}3Yzf+iF*j_1$&G|;8i)6ei%`$E76vr2TpfE+VK9&`lcMVAY7?dme#0-(%E7Mbeg0@R@;)lBe}U^v&JQjF*SwDiH5`V-`de=ByMm*;@8Rh zZvMj!rRbp@sJt;9V54Z-@EUSk;EQ=U*{O1?7rik0GQc}ay>MfJS$SCBt8_}?~v zC@kiUSYfMq(U%tbdbdILYZ3dhzIdkK2=nhfuRJgLp<&2W4P%(6uJeFE*keP`rDgLI z#>*DeEMva72o$}euU5(^@afFgw1{hpwY5r00%l9Y9g_CklU`3Zhf_H`|C#LZ$=#Ie zk<2XNwgG?qN}V9^pdzhx42!!J6L>e0)`f`nE8TP6tSd)H*=5W2akour7@z~Ku{1{-Sg0Ba+FQ0c7JQ68>$4>kj;&x z{q>QT<+w_Pb_kK|>Ydq(i7$Vg-B&Z(tMKA5FKB9hcv6cP;oMVlo)BFA9x`2DL|qKz z@D%6#_Y>PcOKAPKcqBDO6i?!QYsZpUwu-Mcv9ThFr}ekc7VDIr_kJtvKX(5EN%SB2 zDc(S9*sFO2gzyKw#aU1VmER(?*}Q6~xBTdr3s}rAAif1-etOIQg<|@e-7d#Q3{D)$ zrLtd$%SK1}Tsz}z+ZyPfXtdMy9UE2UC6x_bMCMPM%}4%0BeA%Ly1h@A!VGbHG696* zhcB=Yzl}Y8-sRKr_k#VCe7zy~TTyz(dsDpWiZ-RlUFV>* zZ~t6quGZ=FxI0<;Og>HUse(^O`0NWlyTkwgjtCup;;Z+&{##xx0pk4lJy&YoMT=q{ z8MerKJyuteJsrfLcOf3FXY4CS0ix4%NHnp`m7xt&w}f|#T9lh!>#I?OYjk^s#}KcEc4Pa3yH9h>;sg190MY^i?V*V1|e zyVi;}#aY^hbvQ#K43TP7nPd{X^UfrBX3`VoO^p}LbOwzm3uOmjY-&}riv?uH2eIFe zWF+A6x(PJ|C1FSK?k7$k;|j?P`gm(h+u&H4OivB9d8+0=N)FdE?PPa_#~&yTnnK;T z-_gCPRT<73ad4InZM#R;=4z2LiahI{Po8C6)d5$-_FG0vJ2=&?t=fSq3XQkD1Pe|q zU$OPEqacPhAJNuQV1&oHemFHAvKab2n@wJaT-$9!Fg7{pC8hc;et_FG7i5@d-UorS z8Md_m zss;T-hs+s;2!&DRSFxkAZUjaKNxWH=GfM#CyVl8L)T!g0AI+BsmTdtq7{xmu96{^4 ztu|$YIz-z4K0j`8A%mwILjy~R=s~(!27WN|w!~q}Yu!nxAe*iJ662_-*g|*dC#S_| zA9zhXusJL+!4vdz`nYH}5Sm$;@NJhO1>-?~^|e2>5eqw4cCy^=BW9?S?lNHm*|EYQ zjWeF#|6G5ay<&FvS}fbqN4T*+YzbJ&HcAcvpkB7}_y$U6Qlw9_SDmIg?HRbAg7h1} zB#+wRNBMZ+%f)*+_P`0IuOaZIICz0BPi!wc?)0h4_luZy566W_akX__J&XdkoS(ND zwG!=8WGPLqw#tFMRAmD3uVdFWW76JgG@eA2;pLX?V@6Q=IdwcIwQ-z;(rM2gBXqu# zy{W3LcW2naBHTcHO;pZjbC%yz(|z&yuJHH794!UC7ryTI}w;zBG z7W=OIczq%dX)}uj4_XJm9zmXA-2H148tcR2ZqCHgnC6E9v0=c0(chV}hs*uv}gAos=w?7UZwLrSkGeywDbTa**!_-&RRRbEi?yMSZa)uZC4*U7(@7KW+{B2 z7)Al5W7vn#Qqa{f>EU(D`JT4SqynP*@sB0kZz{2;Qx}!eu!V5E*^{>GN+&P(x_2`L z`|iizu!+rokvIqzR&onXia8~fl%|cqervcxV#l9V$<8+=;*gk|m)i3ORL7w?-=BkY z2qsIffJFAwl0d9BKVz6c zoq`>~5_&V78@&6@Y7--7QdHHod`+9Q7VFXEU`VVI`H^61FjMl9Dx}d+k?lKcKp#j5bCAaf?Y}bKIH6^ z`qeg}7|RBY4|Bzjw|0IlL%TIrn7+TPKmkt_uz?L%%z`zyU`mksM!=BE*-VU3%%7ax zB4#;TXWWnTUKRnO>b%ItJpD}$AnY@)g`nhUY2jsuu~SDaVsN}Cr^vd2FDuv}zm6;j zluMdBj1Jw#PR{F%wozQeM+kRy*{$@I;wtLUJYqtGbX+fnaAHsvB ziUw8!&{7i5e*u=V#;R=#LjSeOX8EDs3ot}ojo*rzI#dlth&Dbuut+-%fxR?ncen2; zEqX*tYxNPXc{**j^*f!aT`AU!*=#*IrPNsMSF@@xD(88wY+sy*1B-g*!)~FmO6UD- z;0ioV;fmk$3e04FiFXPCq_(s{#Th>ZFA&kZXjtFoXvWRXObjAlbL6$}L34h999=xSAzISUC_DO+ zgNu{LQ`#gNAyhJ`nQ!7RhP}cAhhl*8%PFD!afJ-AG?lgb7RY#QOP`6R#Wds~=_`=WSPuOFO%4pdX@*iTqxrt3zMt@oe8vr9nCY2fpUF>za60XR72PS5%Fb0FvY1FrC)e&@TPECw=?f- zS-hPnvjzMNe!xhgSDd9OJs);7D#)IiKlpq3^7qWX0M6=8pyvng9&DGc@f&nO+nJhI ztn3f0=}2|KOkQ~u1x#`e=1vmWBI6?ij(FwKoS=6~rkxw+l{z%EaRSPu_z4MLXOF{Y zDqRw34$Y(zHS0j^70X|v2n?OIadq(vc;+V0vxS+ST_;HjLkXJ+Mvd5$Ey4*3gWTVd z%(mbfcvQvD)His5NPHEd1_)*i);9uxM!}UPOSYvTBJBK;b`Jd_UqN1!y$o%^E2#E+@ZH{|7B=(?wC2q0X(U_g4}j-6->G2Lwo`boysAQol=vq#HXf zXS_0pUS2obD!1SQwdY5iD96{VmT1%0IMu8owJn+ts98eVQaZU!0l-QY<^EQgl$bB% zLX%ISJ`_S7^v|%s2dNY;d9i%jfPrIYseTQVMY5D@&cJ1_R{>HxMjG}osmk!)9Wy

tz#Fm;HQm(B0^g8-|t&vM>;gE34p-!oXv0!D6#> zU$;vouNiWRHFLN{Ca7*+coj{h`ekPh-Z1Jw-o9xzkLQR*g@)Y%vH5Hh=jOwyWM5;2 zt%^zXcmapsY59MT5;f_MzdHB+>~W!BXB0l0DVC=n)L2M^e-u zD0MGk=?Vl{Rbqqb?m`IHljYg^8mZgdJW`!vjRE2W(}NYU2@R@PykeiR=6IM z$TAMLN72P)hKVCZQX2{Mder-n*Yt2#fpulC95u@7G&QzoY0WF(wWDV3+t)UG$&!ln zl5&zxj{^fKR*2PD>U{eB1`)2Nu%YA`wCfc@98Dp^F|#OlYlMHiOP?=2NikvCk&rP2 zJ_F*_mW{16gl~xYVp*Pu)eO0(TJz}ni}p~+I+)RN@{OcMIZqE0!lJ8?*mCMR@xhb& zdb)2^m(+(ts$p2ccq5fn^!Ys)Q;6bf`yKYM*3@MkB3Ejl{Z%o5S{noGik5xr=!!l$ zal-LhEN8AuxT&t!TT(Nsoy18b(|O*qi@q5z$w=9S13G+t-yE`nu4^y2cuV-n6!?np zM4|sNd(AIuL>WvmLrQxXQJxrdNUFUxq&s|BIet@FxSkm2FQhhNahG=x5N8T`rEfR9XSK6JloU6l1PagH86HneQhJUoNd7C6v3KJxtmgtzfW5pG-6v!UouyC&L9g(EP)RMLsQoV?y>V< zl!pEJ`|&&E%ox)iDTfe{W)&KYB&D<6hmilS7_2CK-jh$mN+#tFdz{3Iig^C2P&D2| zqyX6|M@6iQ8_22}=+$<6D^iH7K!qrt@121Koi^*nh!!^T*=Q%K2y(Z--52?zn zp{ZPQG=og47A+w3W0qN$cr;Prx>YWXqj(8nU1h;L`2zZWq~wAfUX6b85Tz-M|0wrh;LnM{||_6tH=m1)e)( z_#*s^zakPaQoL7O8PD@iEt6)Z45+L#JKi>EY)8TA;G|udU00u;XqkoWTF=aeDasgq z-`XQUh~oM}M(~59mCbY*sP^HgS!7jrP%mm5=#KWagn(|JW88}B(5m%&DWy}LM9`WL zKupvNoQkCRQRBw|kV0y9f8y$Aa$fjXRJy_>h>Y zd1zk}Rzv(m-yTVRNpOu+IaXvO$*q&wcfjqXu$zPI8M%H-dulG{)=hLhkXH2B*GzF} z6vOBZNpBX#g*3>-cRFoMx}2lgE!E+@K77SkwEC*8YjPULct-)|39RE^&9{_m`pY2c&)KQ@ z0eT7lYA67ZIP!x*=9HH#zh6Rx@Ww)fEm2vF0*U#ty&bo7qppOUU@xx^YT(qSB%3aAn_wC(~G|! zke16d69b$oT>aUkyl?rwn*&YFb|wd-&F-{&p8oqEjtqdT_>rwOzB!#!7INw)uQXX> z>T)+(ce1iDX6T~4u{K$!=&m-cqob3$oc^pej83{Zug0cFpa%r}_-}`-|6i%_`#TP4 zw%yK1?9Qv>elLtCUcX z?`yqoPVL+Et+i3;ly7ubweot_M@xH#5_rpQWOb_enuyEC>8@Wy+7<7y2?fBW*PL5I zEj8zsHdrae$8HrxckCv|#-7E*Pfb9ntbsC%?vMhG@RfLAaHOw|;eB(R$3$mwlmPk= z>Ha^eGjb-pj-cR8)hHf|3qHy2PqO=y><%5AEwQIk88s&MKKB};RvOfQQgn}&tZ-5X zV&*R$;L$`pnnY2{O+;cWb*Qn(%$$&hTJQ@kTalE6eG^ZiL#IqAg$tBWms#@!|+vdN++sAl-q;h3hVA@tlx2|fEM8oo2d6>F&-ACiP(1- zD8gjnDVXqo>C_7C1;L!;*2O6|2mq3KK2=EA1AO0-+{*XA-LwgHB0u&+is6U(R~As) zIZRl;(y%J`W-5f)n9jFa0?4FG<7nb<`Jle}P!0{DYfBEq|i3nD7U zM&hMmu4Wu=u-4K~ zA@eG3W*+gs%%p@JR*UEmW_O~y#dzEr?=Y0qBP35UMlGwT($mpqx)fz&`XB(}H)c+^ z?zX1H@(leX(LTv^Q+=dVzD{&G&6)QahgLrQ04JvqnYX3`vLD9(8*BXe`v1m2PK6R@ zlDa;}O$;bJp4)`o%sGA6^iDNx(}D6sM^G6G-GVz0?+qt$tnfupc+fs_uYlj$)|^4O z&t2~bcWlEaNyBm+0>*LK)(&pUCi^+GuGJO!#k4>jg(b{pb%5koR=ZEw}_%d_`O&{8e! z>@fat$>{nQYNSP&+~V?Wx}ntWWf=8&T0X}FB@@Pe?>}vu@#$rYw~@ZxFQO{vO#~7v zmPZZ1wZ|Zq@x@{h*ZBrb_e_pBi{5w4K&C4>#Iw-ta1LaDRAZLzAyGA#6M$9_FKwdv z^ca}zKtVI+%M0%?DJ;B9W%}dw?>s}Q4s8sYuJ&->Ccj6)C1_G;%Mq6T4X&~dwmU>JQKyDiv0^tPKP_IZU+MJsJAD?m<3>?)(uL@b3mvEXjy_s##N!+G4X9+zgrx?zURq$@&B3W&m@HFo2yS z2e%}#802;KQ06wQ8Db`zhdVN8y^wzKtY`2+e7&p^UJ!awg7op`n=5dtJtghrcOSg{ z%!tUk&8dE@${|_rr<1>xW%`I14r&8gmrtDqADC<+w!0_17-yE`NQFvU_f+Eu-y#+x zC_!&zxDd>o4)nB1OUQ6PR(-s@%O8wqd$JCqMl=Wul$*c4oLcRS;BqOO#ao zC>u;A6ofqd^I2vm;{4SK{%6JHS2BNb`oFF$wfT3KP+Ri$?+(TOcNg>9$38dxrMLOC z#HS_xegyi|#HS`cHSy=0vH!2TLd|bj`s6o-`d|M5!*K-5p)Mq0A2NMCA>^qYO%fQk zw2)?npjxz1mnA6UPk}!*9c39bn@#aXTBNE6X(%O|&aGh$uw^&Kn|1MVuI=#Osms$f zy+6{GxSD$x?fHfx|H2#SZ&?=d69HUlz*X1eVv<5PU*jgePd5I(!`;9?_q%@q{LNuu zk^z|Eln}Cs0_s#@@~`iq6M+)0U4;#phWEl)9Lmt-92J{Yj06BMTq^*#NIj^~Yxh(i zKi0b~L!k-2+MTLKFMt{$zE~=8kFqYZ_kG9)0}*IgPvLm?vj?f51iyYUclt`!Jg6>Q zKIS8(!nTe~_F&0w`4Yj$bZD@;s=Uujp^jM!MZou1Lp~kyqc_8=5sgO%Ef!UIM_P{f z;{y|r5$N;{!<;}xf&2nc8b!tiJq;4l zM);EB;=<_7<)bTuaW*|rUkrdH@(kScW#PIz;N&bFv^?aNBw&$}b=N~6ea()hg%cjLp!ZOH%%B)XJbn56kd^X;=uqsvRyyA+@&>1XCkw`umt zYY}ZXiZF!3XwSnEln=I%)5x@HrOihL-T{|G24gF!p3jt}P(-pa<4aIt&0g5m& zE53c|4t-#;y5UDpHVlIgTbBJe7KVzDA8P{pvtf9@Y}R72@D&lilJCB?q!%r^P6QPF z3h;ZZqhm+{jZ35xS_AfkM;Cz`y!s{5Z!-3nVzK>6WC5qkVH7g7Gl&6OW>j!kDmM^#!Hi-PJ+sJd_AO-&DQuEcY8K#-V0^*g zZ{3ab{YXHvQp;)iik$X8?qTa*8 zsz)!>^uj6bb@1@fiigldxl3~ET>t<&h(Y0b8EI9xB%z1NTKG|b&)K%9kH9NAlc^Ob z+Dk&tD=L=_(DKAH-`$Rj0!Zk6y*bbl>;lTlR&uQ;H^(Ehu6iI8Ly`{-q#Npc)qjQF zF*Xj0;ff;#E}D?gxR5r}4E2j;MrLqg1YqjOOK&H!v9vrlbssVl2V&e-^fZ15{*{U< zpF!hjTy>LNo9WwrO$^oeLTw)`RVT@=Wp;TLClwLmkaT){OPFc=m2swxee*) z$va6(ft{Kurp+E#=i-ohfxX>nvjs11uPXKxYvmt=0`PGkt=(F)X{4z_Jb7xWb0>?l z{5^?)(tr5l7qA1rGi{`11u{%IRk-j6l|aiwjAak1qQ5wpiHi}On{|g1Cubf4Q!X-D zRuVV{42lp%91^kw#TqwWN}w4t3BN?5S%vN&Dnlt~23R`XW?I)u$WYl&kJ*~YJ;FRy z=zUGy&SU%yruqNC;qQ~X)(w+^q&`PEbbYZFqw}Za2SiVEA;YPuve}Ajg{u;bxtQDZw zv<^zaL&|7s+1|R6#Jl+cSF*0>PVTa7#pBnA^o(LN9s!uJl|2z4Z#puoUOI;57&mCg zC{!f&p4&+%qavo*fsBe|N|z!4C!v>slzOo#C|A=o&XpG~=4GMsMr`3W-3xIc)woU! zOan`~zP&usTW4G8DTA=mgyaGNH~W*T3;@S;SQNr0x%S@EBOtC%aG~cMR0$BExV9nK z`})Tz#g;NR6%gZ*Z0VtYP${WhZCToB+Wd}Vj?6yPPA6l0h`^!I3tVt90gC&jt-hCw z(fJxG;#aYiHmL^ges; z1e}?pU>f%HVF=T|76kl%A=>4~#em-Qm%(lgl;$dF5FOFk*aR4DxFLRiEg7hP(&=b2=r)9^Lz7$S~BI zT%0Fcs#L17nv9lOwLduQDOvdWUIYjL6s=%In8WE&a^-YxtH?jmCDc0x(Zo7-JuV@E z+`58cIt)BYAEUdr(wRCmUWb!p1eGt$qT-n(h4wCt6MhBb{d;o_4Q^lS;c*viPvS28 z-MtFK5ECvwd=P-VhvA$=m}_YQ^>=#$(M~&nM-1N@fAVc{PFFXI5)Z+Nj2{i$B;B1t zl!zkFAy_%-2D-bwXR84~y0k_TO)-ZM)uf~seEaOfcd_Om{Imlt4Y^x!p|hk?*K9K$ znQL(T^)AVU>v<8YQ{oR-iVO)vkdpRatS z`hvLU;h)UFc3cpyS#03Ms%2>P!=pc=?2p$@$*mmMh09*q2l#Z5(Iu^|7L|*ep!{B$ zorhRxA~%0e#)x(%;UD;T&Phb({V0jhl{ovPy{kX+eb-;jxLwR!hIN56F_V0cX(vQ6 zg(E!w-cUySCyFy75Kp}?SXB|cSyMu(yoMO4iCa-5TDU!4;-Z3VIMrnlYG_YP3uF$Jas7jnH$8N2;88$|}FY zSS=C(?98@Dgui5t+1pD7|h9CJNy2*uG7rFJ4wi1877t zu2kW%QU4lla{tP&6mIqjy!=WN`~BX0(ho~|JI#-E3!A}Y9b-{NeZ<25%N4hOE5(4& ztx83_%QYxMa+yDk$tHbHmd?+-vDD5>iKW+uuwWRZ18M{$y~7kBsqQdv5D5oww;ott zGSmRtlC~R8r`{I9_=wZcBZ&&uB)ggX7QE`zA2;`h)CwyK?UV!7yo%62`9^L>-ur%G zkSAQzcY8LXK2!Vmkhw;5?(5O9;i_YI!{hka1Z}G%2G$(R3#Iq0A>-s=<;|7dyY|<( z(*mrPR>&^veK8jem2H|BvPXptxh;ISReAV%X&O>wCPVer<(Pq#0YNn#mtIe5fO|rf}*5W=OR*%^~VLLaQn>0jVHn`J>^7IR#-R1Mr+$tKP^mo+o zpAAqf4%*FRLG?|2UGnwPSz<95YyNp7dGa$70(`!1>QNU4?QkxPs3~sW`kT_1+OTPF ztnzJMDr^umLvTsVv1uhWQHC!>CP?9$yHQ7C3W<#f-{(!@qShCK{vB)KzSmw z%xK5M+B*G;wyC~D+^!2wzG~@!r%yo_|94Jytbw`!bWw@g9{ zFmPRJUpl_-CTF5IPaCRZPWDx_&f4=ckeC9r?OAv3=VmI^Po?=@@dzG%Mwz5+K|guP zuWG{;$?~sPU=zEqM>_4APbmPva6G~jA=LN}F!NrYG7s11vGTguo?q1ZORrwj>hZkC zLB2!^#0pcV?fhx*x99LLxRvUh+A1rA^j+cIS%v0gQIS^hw{~pSd8wOKPFLp`SEZ)AkjNZ zYCn8DR4|ROqj$GICK_O=I#{~&YPCgoIupE(V@K(TH)#-D`;pRgVvEP8jTh2J-~)Kf zch=m4GeispC;K&Fa-)1iV{^A1dr`kwmu6u!%1&l!Cr=uW3iP#jM{yFGyN09Ieu(8C z$bi|id*z{TPLQ~s^eDyE7D~|xG!{&P05-A!buH1N+;zJF6qoZz_2y&Vhn5&q?huj_ z;k;zXF(D{6ngW;z%^M#ha5!V4RMVy4cA=@=HoVfL-BoIe9iPdS zjhU?f+*e%?456BJvnuGtrZA^g=L=6qA z=>oCqD~GZ*b6pU!`57_7gIw}tB6?#TH5L{MF*O?&^1O~py1s;MB_PP=sANa|nPYj! zR_>@@?*APw=GU`5@4LL17m{5kbLFCC4w)*9>U`gBrX7r5c0#N|rR*bjDA`md@Tq_Huv zysgAdRAEi7tp6iTVlCG^(%?oj^SWzuE?Ou?;;V1tm^xL2%uTJDtb0}RH#6D5E)Pc# z*{MUZd^LYy%ehCcxQu6ut+>A}rz7ll@VlrhS0w_OJL6_-U3gULHGk*%P5u6+et%QH z{u?fl0b_fX5iRB%(Sr#{K15YsBkDPc6{=N3}-ko2zU9OU4tWJkBf@-4j>h zENy9QXjD^aRvxh!Z5v0eMw*veB*SUG{}D1}A^11-`(@D z__R6nHL}{ThL6NYtX_C9-qgjJ9NtJYp9RDd-DB=ILf{?rP)gphZ-}n``R}#0zYD)* z@LLc5=h?zr|2xbAk51n%!3?xW)~ikFM0+73S^|f~KV!OR1K~tizZgh$8i zuEPQBPITyQ!y&IOK@0Gw8p&i3;Yu|rC|^5R*JLAS`NsiDzNoE)ccG$h2m5vK|APW* zcs9!@8v~5q_9k$XUvoyNBmFS+Si#{4?j7Ko)CHM!JGpObpu0Je9q5f7M+s$h9H25n zSy^b1vXVeq86m2&JXBdpsMg;%Kt`={#S0@`@r+BJrGOOHBp3eLZc3}%7gbE*w(ce% zE=Bn8vROqd6!O^-s_&z{eNmIF;u!m%IIod0iRDiPvmp70Qi*y&h$KxEB7xg5Of?)R zT|1P{4G|bp5#x;g|MaBMz%8aS=XA1OD|+*r+EE%3Em_R+&Z{h{5807rG6LW5VXtyU zxh4mPk^YEj#%9vuKwC-%(uNEw}{t$q;|2S-eFOikAACs#t1)2WGmh0+oJYN>y9u z!D|sv?Yq8yC?qxUcCy3Y4;>dT?usMH^PG70*C+IgG`5!Z{Q3^nvYD*m1V6sH3h11 zJtKfEdv4jbV<$*;F2RkzJrCpAyQ7M25CUXGX{NwX1*Q^j2%-O>Gz7}Zh<_-}9~vY? zS(b;YtWX2^ca3WY%5lsb4_DCX3^PHc(IgktxbbBeYkYsb6r6^{=t=6IZ?BS$GHEb3 zA@8STX8#P>MHM(B;UXnO^tb}i9uZifi+*8HB|FGNcN77a%8#;PM~TDeOOFyJ4*q_) z7b{K>!QQej-}U##(SX8PrKW+Q<#lIvM%+{}9|2^eB$pLiFI6uO9{AK)w1c%dBM84* z$E2jvKjTf43&WPDh0)b?WKrBJbA!@j~pszCdr50Z60B(Iubir^#ot)V;coUY9vz zB5cs@Hm)UCys} zN2KJc`~rE06_~?D*VCFUgu6b%jT9n`o-C0f4xba-I%wBP6}`qhMAFh}NUgEB95*mE z#NEYt-|cx(9ks?^`Gs9FV@H{C*wNOzsl5ALD32*j-ii+Zag&vROpF$G(e15%xUAPo@5h{=IANKJN9BLPh5C@m4icty~b%4ZYC0NlLV#a9SUt z0iuPHAl$<<>IpffXNa>Zn^!kPxGg~_qc5t>djhws*+6v z5lnE%i62=JQjL|hK_%AWC|?Y4YMwb9d0ao_2Z-a#gsVyLZM(m z(gP2`<>XZML%Yx2^QZK3JAN%~#@k2mRwfg_)$=0`4lEy-M^)#{$8vd~iHpJX!qA8s z&`ZzYG$VR<0;(ps;yCC=rh@8oI61yPVm{0bg>_@hT|BrQE~0u`GXUYK#(C#h8~ zl81s30jI>`9`<)$+8Z#QGv1P#%RYl}->Gi^bG#g60=QkG$X@G8@lG~+U=2U%#%Ndw zNb(@d-G~=0sYX*6*B~FgcP`?{24zs0^zDdNiE&V9>#mjP0DFQ+T(wtbzhzrH~ zg0RHcsnz~*9AH`#yjP~+5joQ|Gckqgq#aq1{k4cz5*gi|ME>^8-iZgm;E97~r)g^{ z)>-S_xIGxC4x6MEhAzh+AHG$=`~&Ks1=~Q!_;}(&?qjQd7@#mQ=#W*zzbY4lo9H`e z-0PeNLvobuOrj?v#vU!g$+HKiiTXkan9C0|U`(xTSWPW>#?XBQ#+3p^$SlnuaE~D+aS$qQG!S zW}2$~^i9;1-!-nZDtC>~4$t|(MhhA(FsP8wzlW^Vk6Z%Q)bM1Lj__liT5v9=6v6-i zJZo06lw?u4>!1qCwPt%%;7~rj@(?)eoyUx7&jo^is#D~*$=Sa?@RTLJ`dkNOr9JpLNHUj*K6NUhCLPMNql94g%OoV$w3jVylvuEWKF8PV{;z^NJ!0U z;qBXs=QDExfC@dQwBtj5j`5Maf7MmOBoS2jcnELLX`@bQ$0h{Um?U!`NCW@?^j3>o zEDDcc0y8zU7L^(GkgV!Wk)B(uN(wyBn<2%HwO!E*gvHAoLQ*DSGsmrP9>^hF)Bdx$Ov423aGlz;EVNXbPMYew?yq*MTVK`G&%hv+w)WbW!t|+f+DWK4 zl5bJ6QTAqSE^yrHQ)0p`bZPV3dY&CICe{Y0#dd}Gy>&+824i8uMeUminHbqm^6<`yX6pFDx6bnO41*y|4JroV@w98q#!?7LaSCCXIWZlJSa?z}xuQb%^A{`I^9{lC z>)O-2bonwBnUh;@$u)ogb<{d?>S2O zcINe}+O1{X7IgRqTrhLABn(7}uVenW9TtYktm7%FiMKeo)_Rr;P0V^h zKHI{FLj4e#I^&+KKg{R}V)dhvb|+sj>v;ZDrPk_8){H6wig5JkIpCRlSZ%6}&>o)D z3G$lCT5RMtHJg@x95G1Q%{+WW*8}n;;jDimzM`Q*5_vl#nZEl-?MIVCWqr!FW@QyX z;P@5f^%FrLl;=-0yScgY5*^hrZ&FoP*|QcJK%<>G`1l(g$blki`dg_e5ZBj1xBS~h zTB55@=~=jD+G%0B;W*?`sYtJU8()GZD z$&srFl;$IxyaN z-gw3s1IiOulF)w#!Tb*A%sDGIB#vj^Sl}T%Vq7mBo#lq+2B7pI!j%5ayvo(!Qz#qC zHZ8}&@AZk2YoONKm)js4X=;JsnL%mO)i{mx4e5ljL3=#)vr46V`s|DW9Ub!a(M>a$ zlPsq{?_`1( zs1cylil%7t@WK|9hDgMAJ|UBMuh;#EZBx1JtFoM5`a2Lj8Me3rDkuS-;Ts#!Y!^b!$+0OT{$T( zFk+SVVXQYYAnYfcKrvQZ{wI8*o_>0K!q2Zo2x%XWFVjrTP@#_MA3!MA!%oI2o?XIO z*m(ZwHttfQ#GhjN=&8!(-#BLv;ytGPgywzb?#*B7RX}?cl>w)xdhTrpAR*euj+B-X zW?R2fyWx59N#O2X`J1Pstq*wMyMK(XVN$vegFo-$h2yjy`o?~V0Ep9;WTJ%B$Awm9 zlK*V*ETg8J9><91X=7{_e@Ud@suoX5#F>TyO((?%W&l_B09B}DZ=Dj_xB`}APUejg z#*J0P6}z2;cKLD@X-m4JZmZlbuT5W4t__!pyDY+_?ku zaR=lOhhxyhAJ14#g?L4%E}>y1Qo{|K*~^alQr9OYv+6d|;bi2mB3lWQD2$05-hCsC z43SIx!Miq;#t0L6D?(3|yGC-fE!;r>ZGB+{*JDkNys|>EHlzUi%`u_#CAEhE<;onc zLBd#Hgb4)DetM~u=6g=%!fh*{cZm?rRu|3J#Lg9w`4PXu`mYSJAz|Q!qV>5!8TTD=J{|wFlHeJQIE~EQORIT7;_-%a2S^Qno z{~C=4zD+2b7|#=<0@K`Q`Zc=}<4_x6q5BoAnS!xbD|TgP?;tjF-teio5WzxBgdlbf z@KYbu*FGnZQoqr%58d&Ip#xJ1cRT>9WL-hzeT1I{Tj-7<+jtV=m?r{$%Nnxo<`*{{ z32|W#%rUbGCfV@nbrIwr!QziTmj0l3+4gV?ID&h$(b+$Q_7>=i;M5_XcwKQbH9ccZg$Q{^uPY;?^YTjd5-b5xT?ft{ zUd8b6nQ)q%gd3tafA>t9NsAUrtV2!XCS5NO_MNM0pJumqEA(Cc@bxA=|f_WGtH3 z;KZ;Bb!x(ig&Pi$)L%h3|F0M9`E0_l?gW)D)>Z-|E-7JcB?^PTNv)WF^<(uj%vFNY z2Sq{hlCqfI2sF>-UQ9$64wpI|5SZzyk3tjTu=sd9?wzkNB{8J+0j2$DOr@0OGsLps zNfgEY3RvgT*MRTpuK14h6o0|omTGC^0nBy0C1mAg7&0>@W_f7B))6hnb+-WjU9N{6 zZxD|t_QCp2OLOEINO*5DJsK_>zDd^%YtdNbAfV@6K*?TbJ^kb|Fw|L0(2!Fm*2fC3 z$sVxAxXp~ASwT8lJ7;f9heYLdO7QU4vA|E_Nolt0F<-PFp5@^bIMTJg){9PF@6I$a zTvr+x!L(2a`~Czm@ecZNZU^V!M*8X18GrB>WOsC8k&c*WQKM| z_LB?~Ms(~0^*G!^UX9&1^ELB}nruF{U>Uh#b(ZGK@03$}vK~vt>-`&*SPrgHn-y)Z zvSZ&R?)VvXOt=|paUp&2OifAm`*3j4>&vjI*Cd2B#$$2#j{ zF`=JWKP)=BXxx@Dt2oXR4L*%Gt&cX!kAZ+QH>4r8p%r4MwHFmX`Q?w;KP;VP2)9NU zxGHuy&^+TJ>dp`R&u)1xkUv4a#5=aG!Vx=9$}}boB-9~Z)|{e6j#PS0OkP%D`TaRz zg*(QR{npsslZygWQUwNDznwGEkRw}(^vdmjlMwM`@QESE@Bw#ylV^+$yRt2Ro&zyb zgbg)Urkh5XEz`{*C{*g=6O<_R_C)|dD9bQB1aD2}6Im>xnz9)*4~y0+3A7(5H)5?B z-{YZ-Bd<8;7>&5GQE2q{al@{FxH&-3kM*0PEU0HoSuYF{+)2oyO}Tz+RGmMRBSS%5 z2^|t_LPw0sOhya~)cVo8OFw-P)bEuP2HbC$y9WKozrT!Mban0jcDL^c|MjPA|E)*( z6A}I6kFT%%>+(Ae@_!c^{hNsXO+>HE6Oi{Xca+6TGJl)T3$U}IeV|BqR=F@yh`&n& zph`AU$mS;sJRTJ~g=3TaXCdsJ5LgDZjyxXD1Ek2$YD4TuZ|)Yefr3>aAs; zC%@EJ__Bk0z(wCZmok>a5uo`TggFJERygFOI!f#?`Yj+P)I_D%(GOt)3V)0&#u3c#%q+cK#-!|9244GJhhXVSxUhILKL2g$0Cp z_zo%G|1A#kPT0$5O4y241@K+hBz0Do^3?~u4eF8c++eZcX?4C2jeXeE)pVit2Hx4* z%ZvK=%sLO-HydH|Umvss1XVBWuV2fW1y6apXWidm%3i$$vBFf-JkXp@=89!9IesU< zMeth-{)ZaEma-ilx$|rG^u^iO{KkB_6WEp$I(G;CC#$1dajofp*6S@i*tl&=fFe<^ zsbp|8AnG)p?={}WUBH|?bxaisMnFr~@gPl=9OE$}`Zl*#YAQBN@3yJyo95^0|A}iz z-`ic+*-MCu1Mc&EOXHV6V^B#ElEfuV*K`1Q2IfWD`(eVz3ls}cuN0GsLcA)FDNZJ! z78^~qv>b|)5nxaxU6*~sC23FHWYAurdGY{bL89{)jTSvW8;90#ygRd?=CE1VB-MXP zOy*(19@pS+c=Vu8VQ|@R0&rf|apOoqUPt_O)nb_(mwEb<#J`db>j<=*gYo62+eTvc zt*hBRkD)VM1Bn*scdfg5bp7pSntn2xMw%2q$--<%lEzgN|ByZ0;hn6(8R(KB2(jH}xlb7|3qK;OMs&el^= zz!RO8RQ9UC-Mz!KWXG9m4Gn?B2lTVg7?oDn6f_s>+0`^EFh^-8N)1|S^E{Ygq5rqS zI)eT=r;DmM^fzV{l53UK69S)lc>!He>%Sb^f17eV^=kISK1f4Bc~iSQRgOT*k@UHA zzOaXV0!i(JwesZYhY6@bjWt(^a{LpDe!B132_$TIFmjsUaU-<3XthPoVxC?;<$nN; zgn%A0bJg~^MXTBA7%pO?r#$?zgPvi*LKPDGj2R|hz*%AA$d$gqWEeu zlu`#(#WnT0b#gXd+~|UWDM{wf8O23qFP_{;q4Ejt@PC_iIK@cYgG7mGl)QRi?UyFK zym$Pd8;)YyiV_{G%A&M2s^&grlRxG$r*5M%E+1Y=9BeD^Edl&=t#FPW^nPE){JwDc3l08HW~|?P{sXi8EwjI!)PLU* z{nm;9-8zB&Co=oU_s{QU{!1w+yjv&#L-y?0I|jw^<<^=`v$?UqHKGfxa^_(y$VThq z@e#%%M@hRiHUdY~v^wB=L(J02&V&=ZKK-mJ%4)SqlX;_8n>I`aiW6a~OgFJf$R?T4 zHi-kl+6xLF2dqmPec2I@NRyL>hS->FjtzE$gCz9j;pwFvm)Z?m%hqU3R^M35udHS0 zMMmDSInDkkAunCPkgod~ti)b3qj6^;003ntP{v!H%ZNhig}Qm~llXme|0!XUUZCnE zSZEhi^6ug@_RcTaJs6(;n2a+{%gh}K#LdU!h>p^zhT)r5l+FMNfaGuX#Qc-h^+^b+ z;ZWVb(pDPw*Pc)(H};P;1!?4QMfc4`{n0b*z?XkI_}B0;W}zY@z1HG7sBJnYD$r@A zrcSK!n%=EjE#0y}XqmUY#fuj8;Wnl0f;-GV#Nx$`wY)LAMEMIs{cLWN8@OTbywV1F z9%MC6Umhn!v6r_F_HGKEpm9oih*FTmIS#8h{Gz|@*9)(=JGDpKOa0@^xWJo_=PnZ; zbuI(_u$oDgek=EhfOhdM-4HeyOn&v=Kv|?sj==sENDG-B>v_QqAfIC%m5V0 z4LE#wJ!+EfTrI9KS$ATJG83n^iattpp(TsKwI02m@%LIof%-_1*^%p>%HLEAHiN-ke5E5aXztxqLxIjHsKdZUb`z#@v#iqbgflSOw zlwFH@KVo9p?Lzm*!WQEXm(%GkIT7-IjsgI%arkhZe)J}qvj|Ur+%~lJIzX}JFhMm> z3FytkMEg9bBY&33j!X`w1LTnF0nPnjtnGm`zLEIm@9kgYQjV{b)U(`VAtxre#X+56 z2lmBYeU!ARyAX!vU&}#2K>ic5AoR+>Go{>FWLixEzV7Y^hNyVBqw=tE?@og9yroSm z<1whKpAMQrwnqrBq^`2`vWB8=L}oBliPTUqyV3&r@c@+$)8 zCQuX(_ce%*B-r*o-ZgOG8sI?|h{_L2aV(m+gPdJuj>WKaIqh-&MVeV!tWtAoo>U)? zbxc*VEgy#>q}Iwl`RJ@?DJHTLIbfa|gVEF#CwiTjKUhxD;$H_m3WC(d@6F6~&|Lw= zmb@|Tw-yaKW8@M@fw_OTLdcxDu zZcLugZkWcGQ`&J}i;|JC48|;Rcs7M7I}bxYbayd%mOMEUG_#I83+yP=-vk}ktxvBhm;eEB<>8r_5u+0HT+5EAD;DtFhd@wCN zBXZwaN%IxQ0p>lJIBJrV%6V;DYslb};UYY*mPu5Rz^mj<9TYV9Q=&J8!;c-SWjqIU z3zF`nAC_a_jSTly#an`OA#4Dtl)PnR{U|NEjMd_Tn!dLqgu_B`l8*P<_rpwvP!T~U zR}bT|OXz=prG<-O==Btd7Rmb>niIk7Y;+M&x!`qcZbtC((YB7T)N;JpsP^~y{K_nV zGPCCcfMD=dEnm8iaR>!D7XQgI&0?(4av9^gh25TMm?bY`8uO;Uj{U#@?pp!mo3AZ7 z!@*=86!Ohy$@hmM=C9g`?HQ5D%1@ zGUz5t9-`bfKh!QPuSPoyZWG&2tX$MR0EN^zX8RmJ3iwaBtxuDXXAm7ogI<;rl=9?4 zMCZ9guN3Xv1Fp(*4m|$qfc>!R1ZA#o42jBK0crj=v$lCP(1S>a2nF@K*#55){1#TY zOqqB_eEdE1@C%b18N`8PjlmjSKW^*=%|hjHqg7IuG;(8EcNG|k4WfAZlAz3jo7gyB zz2OHY0pe?{>@dCo$|Rm1fZ0qTs3qDBn(p*Tu{{EGn3B;t7PRbF0(uw7r*`%h;kK6Y z)%IJVAt#!jPFMAGkN=%5{fm7eSEtMV+nr16YVxp%(WY}JS-TdK*ru@lO$-%9 zV=OY?sLdKDKd*HDo?#mtKBr_8sk!=;4y&_IU9QTZ6sINX{(G%;dJ04OS1kz_0x`+e zrTTkok%AS0s_Q_un$;fzY3QHGAM^!hy&q&urCx1oy$ItdM<^raSMdd4yPG7;Uk=$hwxbIBm41O)K;2I&gOyaEE1 zMk&-nQ3FSh1o!dVvVLgZRXXY-(B=Y&9RxyNHmgG=tzIKaM;~cMmzT0vXQB4x&vN6S za8PdSBhSDQ;`uiXyE*T{andsrQq9Q zUSghr0eIL^Rp^-_jW(2br~z1?3U=>N#qATjj17?TVDL{m4{PEgP0sPbZ|}n89W$dz zmbh|DH~K&56%SmC)<|4(4?ecH`T1nk+drq{S#ULiAwA#; zQ0Mp$e>oGC;MnE!>7(tP$-CMpaT@8A@>ex4IJ?ipv)7|$Y#(M3_Lu_zfKad40Si#d zx*SIMONu00CeMynfJl4>CfOfGQ=pzX^*P9-cX8yZ@c-yFF&bFObM5X5Y5GBWMWjGr zuyl?Rrx&yb>nf?YhKo*3#~jno@gnu%Z>*kue4+1GH`&+GR|Y}kZTTX_l-wQ2Q7d?| zkws~2ox_JX$s3ZS!yq|G(X^!e`X1D{RibQY0>5dVCvl$DXE)A@G$uJ>9y^>-Ksf9D zO`umUypo48GMJ!xVMFehs{6RBod>J-HtiAA?$U5fLgJzocbd5pug!9hNV=)AqoH%D z7qz85vn!XO?#J$0;}8e4z!|`$0Y~JpA$mwb*^)*nRn-f^<(#pmoaxoj4h$O}|WA_jI`JIqV2zbubbv#Ir09Lw3O zpve-_+EP_s%uSsdIJsy)SOb|lUwV+#KY!5MseVQhH?QJ@eY0symJthc z+7C_KhM!Tv0EA`rtFZsY?e^_?Q_xBz16I{B!j>q#hETzZjx?;-_V8Xlfp%_ubcB8J zoFD3dNLi^Q!y1-h#H2=o?Ew02hk)zb1w!1U-+qG>ac^O3Gr@I@_7AP*fh_%`F0jq6VyVekBn&5(2jNM=-aWz zoY*G4+YZ!K&y^VnU#>1dl@qyyPlII7U_2bWQMv* zfvjpTA%eEFQQc#|4VaC1<4Z;U8cUIqY_Z)eO%&Es*Fcst{Rr~DISzKK3Pc5rq1Rh87` zqvAKwhO`nTgwda*CaAgCzZ49;w2q|lO5%I{h(7@wWUF=n%{vH|Fs%x?)f8Y#MxnSt zKt`tAtdhT*;uK%9NY2tPNYpz_L+vYe$>ztd#h?OUXcP}4V)RDJ)r1A zPDwE}Q?NVVyLt9b4HpS;^hy2YNyns9j==h9MkjxTKG+LACN^W9HEYCB2T}WZis9qK zS40uY&+2?Q8*lzO;2G``s1=fINC~G=`C5FvbUxPsMU%@7hoqd*CQ_Bz?2XX&pkUp_ zuX<|*e$xZYfIZ1HjVNkymdD!`ulEMj)+?j|<2jE3oFgC_x)a=_otFfyK0kvpUWrIO zQk+wVXDCSpt`Voj;+Eh?L{poe=jTgWk5h;1WrD>>`sW|4&8IL*yF3VC(w9?FExv*l zBw%Ake;_3`cj*m05%K}7e9D7ov~~{`2%2ksHd8%oMip|$pl5Ba5G+C-lW18_)8_bF z7%xOU!-4h-XLK@qzr?@5M!sE4noSj><=7Y;28MpP^S+z11waK+4L-r$Y7zn1`>&z~ z(wxIBrX@{Mx_D;)`UQc%I)(jOIX;pX&n9Zl7KBUw9Rmi8Kz!@TmlCfW;sCN(-H}K)@FgvzVqH=eJ=ZlF22QPWHpx8QW;jAeAGrnk&Z>rN zY_gDLntJG#Ki{KOLbu_4_N3ePv2@wx{8s%>6x#Xz+>QSU>7VFhk>B+3e;0K8@5lK~ zAOEJ0f78deMaa7*ka%u$6p&l_{oJ;MJ9mN6UvJT0&vfARfzg3i0@NFv*y2wzGS5-b z=?BuLgOTm#FO`=Bikm5{iPHU2Y?StGlvc&WGG}>*L!>g`?vW{sW=O>!5UUd;_BszG zuH6xm48^yC)OrgpGMQDOnBc`SrY(bG7iGq!b4_CH5z-{ixHu^xk{=|kiZ7&CwEF4$ zC#blzOPa-T#T31To*l$f6E5q$jk=FYI#*bgzv<)O^zm={`2X*)@PB*%lVw!o*zj@x zppOyY|CT;3$9GI&*F%Cdx7l&oiLvb38TdH(@Ok1kJ(NA#Fl$kcnW+9?zJxuppBv5& zzJ%}cq3x1c^M143+4`E`7V?Tbf`rS;_=wIC_2DKi>U!|b!Nv{Fm_A$9>CL40z~t5R zJ(s6asnlq+SSbIU{1(A)E%={q2o}h#LIyy*g&yr|u!Xr`LjIw$`iV~eob8$o^HY|= z0gjYC7AUxHtB?5Xr@OR>K^f)y2-=9IfG?|Ld3p+uO9jP>97;f6qv_V@{WAo{%%EVl zNDZ>-es?6COLzVP2#}y)O2i~SNP8}~2S%DG}uYz|;dTXulCO0E(T z9u(gR>AO{&mI?Ce7rBCw+HLDio*qFQ07$@_ z1NS#X;QCnCq}_C9F~61Jc1e3_bIbhY`oD&rycQ~i&-W+GBPW9(T&#BZDDUZ!(iX^Ln)8$5sYNY(t(VWE9Ls>)6nHv_1ZePA~n5 zrk;~%`LlHYENft2+_Tu_=T$Z_;sl@D)1U(0;<_|^ME}y~47H%^@Gpp^Shb?~e%`Rc z{_CV>0N3W8?!-OOAw-n6XgYFPdT?!s=J0-#eNC#Np8+=ipvifmoPQtDe;-)L4>C)^ zX(bfh2kQ8|;_n0=b3bS5$gTay_MwZ>8%Y5$3=ujt?L zh+FvX^GY3%x|QK$B|KuYU5sWCQKR2ykR+S1NAUj-dv6&Y$Cjk)mRPcwnHeo+CW{#@ zwwPJ6n3M5C`1_h_$_OjpyZf8I6nvwfiq}$=y!H@RL-xyb} zmH!ssfANO*rYbP*4<0q@_9KYc_QS#i12mF4KVTXO+AEhJ%(<|*11sf_dgUbPno_az zczj}gGM8!9NnLeD2pZpqKAF|c)_?M={=HkGA+LWMYd_cU(484zpY2GtuXge`fd(H8 zJOJ@o+NogVvzMG%ehNq(Ek3xzOre1C?~SW)N~VA6PGNj?v-bbZt9NJ+Q}uUggum~K z*%}arI)*%4Cx$-luzC&AwqJaeCv}rY-N^p&=U5J_|7mRhJf88j^NCJB!2Z@oX;?vX zIu1SB9AEe6eHBJNtEF&&P;LL=Y(Oyb^9HNEOo7UZqq(P94gpeo+L`jPUnT!n$^Y+O z@_$Xve_fyaxGqGY5I=79h_CtVciX z`RuCdC&Hwg47?sy?LBM>mTaI7D}$iX_$TTS?-NVA(%JeZ8p`sTSZJa%hl{kE%V}<( z|0D+kweKr+#Q>zD!lf*zO1$9iBYSKrPGZ^;4Z-&>Ov;q%N?2lELqYT2SPGe4hX`3# znTpmy0djutitNNA`(}Mo(Ln2D;&H?@eZYMEc-#YM}5VoT_1n)2fo9&wHHGNb`!@Wcmx&4G7PG_ zr4lttG(W8MFRPCbGh8BJk8g`*e!4Vq#~CWs+8Bp{F4xJ#IE`~wtyV(i4SI_k)i$|p z+Uql$-Z6C9Hj8{!J4EdviX(S#RvCf&f`^oM z*N|O98IC=SkXjWZFswwj7qFKt01Ej@Qz~TeWB#t-93)XtU1$QDjM_s}t{^G`p&)Fv zc4?6_v1ZeIi;EUy(vM#B^Pi-ZReMS>Z3fz6UhEXCPx*rL`4E_DQ9~BCOx)+?0^7Kr zD(}^`$AgIBkU2YJG=i`XFYT%+uT?mU=P%+@xBZL76n)%RcD>d&S@eP=!G!*#NtXGX-i9J&y3zOBBx#mRjrEIP`3; zf}EU6oKQEecV)JXS0(xP!HLT?5g>#{%yboMP+5-7EPG^Z&xkRXv3L6^Zc2)1tGw4+ z;}I(}dt*I*OkzZP3tTY5ci}k<@tRe!uj&)zt`5Mr$Yxq$iJ8}D+5vm4EyIU6862i; z-*hcjB=JvKO+-IY>}8V}dB{?nXba9c^q>X4)Awo&YQIomtD{aM>D*)2=9j5#*R8%b?*#Rtq8oM-*h}rFOJX#2hDRlXYA@$hA99ns(@6@ ztLIv|f~h0*kb(git+8O1>lWEeN^SmdO_=o1cZ z>|Qq{4ehQj8Dx9kqH*0$SOWL=6>x9GnJ@L8^ZhH7fwL(IilN@TqFxSsHEK=WoCMo- zZ$Cv9$Av(1RnS|K5k&bzRBT{ln<}Y8xBsyC0FOhB$hN}vaWsMci(A#Umdk3HLYWC( z5#)JwD1Lu~NIOR@{4G;FN$!@^cTETf?na))%lBTe?l*WJ($EG}Bc1jH~_8ZOH zABl$sK&TMwTOAOk=$z`Pd~z5PPEj3JXRtCV_(8bk9#O?&!Ml3FHg{shb`KtE-7PW$ z0nT1J@*-*6o<}1_+1cD$*t$qlOxoKSg}`N^grkHH#DtuNbVc~PcacQ1P8!6owUxrh zFYjl-L()$}>UAoZnKD#%+lTG3py2U(n2BS2)+&}tfk&T!J@|atP(M=A%m`YHSDn7C z0w|g7rqAyY^-840u8Gb0)HCUtS-Iq+`U|_WtG}*QXCZhh;_=<5L9Ez*;6v>Tq|1{x zlHHFCGNU?fDNAT`5u`U#XR0R|SrE-v&m=SM7`Uly;ZlxNy$gvn3&YW?4NgG@z4?mO!RJAi8HB5eX*G+M<^+_O#Smy} zhzr!tS==zqOLzo)=ZAH+tW&98YIU`x1i(6rH#Iv6T;0{g@KJ0oc5lt$+v~VMNT4{yBOW+$hg-9aD-|iC!NH`s0G%Saf=k>JlviL+v915S^YtHPe-{2 z4UDB$e(%fI{Fpj*j33$KDI%?fRV_?RiY7+h@-?TbrnhPLw$G3u85p4s!3V`K{g``t z|3$($m&Td}4gDOiP6}ZRIp&es@Lp}0qC%H)&1Rm1Ou$5kUHBz6kNQ16t>9<^Lf($> z1h%1sA_`YV?XVX=^aDA(m`sghDql?=6KU|n7Z*i@_bqjrektXyoUcyiWN!H1(;bFS z1wXw@(vom#vP_W6?K>4-1+i0}2`yaGov3VYu;DEL&3}QH7r9EskIJoDi;?q!9FB1! z909k(l;VXDrDImEBoPJuhiOFZ8wegZ{sqQp*1cA^&+E2*4U_`{E` zGtwMGTPo?N@7W9u5idgX<1tXyMWOz(Eu%sH_QhO!*MUAn;B9XzXS8AqE1xK5-wJ6Y?JwTF4VNhtW;D^#f1>Dim;xuxPGti9iEZCb zN#0>4AMT|2J2Jj{pnVTFrd~+)C^huOntGJna=R2SxFVkrmf9JySYiBhs-lFcG-Uzv zZ_LS@yuh~~v@u@{Q`GvWo5%5n2BcE}q~LA+WUOtC-ADPZdBTKhxp+_&rRH;)f3H zWqt{1OxN}I!<$cwSll^APX-xtEhIo$7w$7ZALzVwFX8)%ijSxh3!i^B2*2)Ahv~EnqpMkRj&8!Cb=Sh;dflXg#D4|D_MtC=2acMM6VwsCk#SHF1*#?veg}NWNXpGvyO7)&THi7)%9m#MsAg zPloNqLV@#@{Ls`|g5aT7r`MfvYH(%dOK1GsonvG`=}FdQ_mSG9W4H9YAZ?!(-kB#Pk@hcZhq*irDtpV{Gq|0SE zT`44XyTKJ&ptH_COs(b;Vl43~0!M$dXlqcuCDnfZ%3Dq;S69V`Cd7!k zN1%H&yX=};%p2R*5Iod-bn8|d04Q~mv0myliI#CBa0BNDcHqd4AI;$!xX>z8T+_{A zj!;`_(c$0=IcmD))D&Wwjb(f)YA=iw&;3YOsD7{mmV<)LKPoQ~^R1L(N6;!Xhv`KK%*NCqA1{P6O=ZmJ$JW==>`MN z*ICnC+vTy}=mA(jXMIdgW}3%2Sq+_A^2uh0Pj-i*aIVL7>jPa-k6L!cM#=d>{*jtL z2sCV4%);wMLG=N8uM{|%&01L_B?|CvTm`AVCeo|V{pl~=%=%2=8T$_F-zZ--U^z@ED_NR)e;dN&vXlYrd zNWg`y*8`<&x0XyPobf8sb_j>Id$NO)TgbiFUJWnPUJts)S2L%KA&&A)0GgLDjfBDy zCrempLvJnt)}^FDQZJwk!GzR@gqlG?9pcJ%L9AqDpCh~jiJB(W2|y$qgY=JhUzcdt z;;A_b-RneX3{z9^Mo{o6espix7i`x!TSdv?4qGlykEyk@`HodpdoqXKU}K5yRs#~J zF&%brd0P0^6$o0g+`p%pbLWO=)Cq+2G`55NveAX=H5Xiz9{iR@1xs&QW_mf)+H#Di zJiW){zb_B9MUcI5r}0Uif&Agzp)g@6P#o^GGM6C&0x~d6d<%$UhvmvLj!7_kFU|2L zQOd-}86_FU&)Br!Tq1b{j8hixfq%UMc6Xer9LQ;~Q(d)5MI8>)n_%v0Hx%WxYP5-I zTJAbJ@uYLOmVTE{8$~`$C{96Wxhq;oZo;Ie>XWXFH0Qgo-te3 zcFgI1kuZ)u5b*P6EPf&)hf~-oHGjPjus6U0$3uf?Be|oUWD``K1-5)I1+p!(6Z}e^ z0%gaOkmLeo*Y@YmWzgREsjY!ilO746soPUYz@&NhynqzV@&IGvP4s2{9>yYjTTc zZ0&lGgmI6@t#_>R8WB(#`{BmfN~D>%-z7mgo}xJc?!+jBL6mA=4y6RbxJE(9HCBz(6X*3Sg5Tvgpx{af zV@pgvI20EXq<>x+oelCL2gZ*nD+>CE^sV_6$_w;37i9pS%#@w$43qEyTiAuhqD@Q+ zpKr~s{DBa7L3r^JtUHU1OB4yJpCHj3DK*hRa2=+Va;FcNp%Xmv+3?DX^xSD z-4Pb)z3ICreCo)Ejo4fnw^~m5hr$yFdjyvrvr(M)nhY<>hQi(0#|Ij34Py8%sLyCf zCRrbec#>l9Mh2q{cR$r$5ihwQp0ozvX$!mkXB(OTMnV{sCL~{d30r$`28QQwKcc_& zYmG*gAfW0F?l^mSkI{pl&73=$#!=O_J-Dq4g!0xY(vbyc9rR%Bcb$ACgLDJgl_I{( z*sm^UFZ(8wX7%H`xDwIfz`uwVYf%zWC$#lZHt%vt$m5uDG&ifTip}>TNEW*nE^&;V ziiC6uyX*$VJP*0Q?ge1{xonO`OaqNhhX3F)-!?e0=s*;j;ZfnBW4(K-h&nMYHN=9=S%iUe^4fs`r0CBJ#dKSEcqv{&! z4rLMI;M#!yazdi^sXTwpZTLPm&hY}lhXP{lsXIxtXMl%l9cd z{u>4SuQGno#J_0bUo`PAnmCUPPXRvW>N55dd^LhHAAJnxy1BpR`2mT-ScRH5RZ)_Z zYZln;1%}R9zQUfq$yeb*Nje_NEqOYcrJGOEzQoJYMPyKsHsV7?vOPrluD2$F%9X6p z;Wko3#h)19!!Cwm75PR;sp!}uNtO3L(@-P`NvQCs^a&BQcI?ho@e^&o9hiJkUJvf= zIo_Eet^4^Tay+;#SklI`byXgWVK)h&F;=DW}0=wY9Xc^yl)xJs@@@0x#<-4p+->)aKfnV3QV(?(OpogzV zdWuu82iT=SzE)~{dVUk)X!A%NpatSKVCKAos2-4%6@*{fWW`jzc?x7fs$%}5BIh62 zqF(}ef56QC8MN}31lTVx{i|fb+e(vsqu>WY&|kP=BM)`|l-c zJ5P3bKEEH-;ebo(ec;Dk2CTe?n20xs$5oZp8CUwb=yCj z7*tY2tBQUr37i*}L2#f$pDUySqO07OENI1bAW$v^ztPL96K{$9#8ZD%QwRPD&O0>yjMNp+4@)@VLw-{M29w+;jC8}X63 z1yn%pgCIll50L|op$$O#{j|LNga;`q&Toy1*53zT;Uji>EEEe3R{r)H!LWmz;Ry`_ zy6U{hJN|bIu|6DL+TMD;I$wXJEv$ba_xoq5B!U3loBq zRJ0dwfD$yfLWR)~Vzm4eeNYZ5dliS6nbVOi)|aRHJ5k}Z3pA0-vf-^W_r8i4+FV12Df2;xSTa8Q;ozF3Qb16%HOxJEKpaw1lSI?)F+@UHi~|(g*gLSb|K4B^j;}R1+W2?Dr06Pg4^t% z1>vVcqU6vKr{DYowPK^+`sq)RnHN;Dli1vZrdf!*TWqk9#$lCUcH(a^5b}M*0;ZT} zb9&*u`Q!X#c!j+Z+Cp)6c=r5P`m}uOlR)DG4M^HrtM(7ZW)6u|9$+WenXwmkdA`?p z)!rxT)tjvYo2_#ZZOFg5@F&ip5KdXhN>?e2Qqo|*9&X z6Gv?liN9XYH2j_VwZI=MZC>C~_?Ybrk7aB?Z7zlGqI@7pReyF=KCw4lzV1LIsaiXO zyH*G=Ex)e2TCGnYz0j9H8zggpbKXBM&HPt|-@m9idm8^y>D+Tjm~yEo_q5R*b&&3p zqXsPh^m)Dye=UA@pMYeGxN9PfVPzCcqn;llvAXTUPf_A>wg*8O5f$tgLUN)q*f)f@ z80ElsNY0n>KWJeWNk5Yg@%FHAn@@*j8ic;LyR`{hhfF->jUZrUCExmk7bOz>)SCt> z*$al1rQtVo`2wQ&b5o|8_v}^g?=}h@{UPiA6iq*jVa(!de@$WjRTbvfY~}X_;vSXrAxD(TFIx)G{D>1UJd47 z@%aoX7lpXK^{1{Z*F?~@`BAdyzasCq+9g@dAo{!5GF-uyFh>Ek`RCTST=5+)EfvaoWU5(>PJ41!<-LW2nGZw$ZGme13EF~$bLA8J zbx?=82ZJKmeSy&!J4dlFikCLztnYGzB-Cg5T|_CnV+~TPm?KRxaVT&5Zx*6WW>lvI z4WRs!nY)qlopYBI3$%{S=xaql-Is=H16DMb*E9N>VphN6QJ$6pV9=>RXZ+RcGxM&!tASdk6*MI)zjiZz z@ioNmO)*vKrc{WB4)>#asB+Yt*KCFO?r5H4r7jMKpfi?e>w%f&5pMN?+pO?Xx%a)FAk|HHyv|@%#)V?7_J30@9*e7 zvCkxmr}#c6`QF?O*<~l|L7k7NB63Ma&226*b1q{$N~s9mr{Fz$mZ6sIK1ea^u`qR= zW&TOW2W>3Mq^%?>xTjibdduwE37qzb^NhyAPJ?g zK|ii8HdVId%8o3^SyKJOlboqpawvqlv^(7*?1|1K?2}Y8q#(llk9tr1#u?awx~N17GR;m@hZ@h#-0m4|qR^I;L_ zdl>}-0DG;U{Z^CPCf9toX$GQ;x);SGHM3m=>rO42*z* z_)#obgR^7+T0UwhO0;A2>W+;Ui7UFMy%*BO%)o0Ris*#H2#=$!A5K}}>eKb@(xw{ggge+cT{q>g&<`ExKX34E`h&HKwBIg4 z`%?t4xlV!`^|tZ+ST(C~pHY+cUc1*;Kny>vq%G2-=u$lZ8ZioR<+upCu+t2+4kLzW zeM_W@Q|>U|_n)i|u`dNa4{qnry?&-7T#wFapmiEaq0j`>2+NsygQ!aZBq zk>6bNC6)>^*sGc*=qY!C3^|p2MjysQqIYS)7)R^;@v{N$9^>B45O8i%r)D0*N4fnA zW|9b0l|r=AdlT*>f;y=imICl%X2zBfHK~ zj-}bkz^3m4g_*?{RaIIrY8-9OnHY8ALZHqk9lE9jDU&UlNasb&8e3sp+m3;;@WcoobG~5J34F=l)hEmL9UeQ@2;u)+@gpT0@B6L}_0TMk)jH zeSt{&EgfVj^c(kA=d$0XM^+~|HyAqC{w)mGgnOT2I!(o z7TWrg~qL%S{c$pcP&H%+`hkP3*ZhaW0uf%U3ar z;<*F*s7w14$3!xGxgmL$$M%4nQ_b7!kkDvWy1zA}{xRIBV)$(%YQNn{!J5+- ztJi-KplKw-J)CliQwVDbW_DD-we6q=&ahLB&Q6^62i%CUj4pUIp)2Gy4wfKDD7)Le zvE{HER>QBJnz!^kCpf7OHkFMOf;Qbai~GZ*XtSk|>o@Y8}hbEyA$~lt6 zAh!f59*D;TNdq;to^q$Wl?m(@k)1Gp2Wi~@@B_;&!146)po%xbWC7t=0%yeYyO8IY ze;Y%u>46=ULdYvKR5FY*eXU`4*K6RN(1fohbXkqq_jOdK#oUjZXe9HTzU@rI_Meu{ zv35oiEpqE_>Ew&EKrByS0*#tz&1Ya}+k5|YjN}jUBVi8cvNA=kkohS2Q_662Ay%)5l z1jcq|9VN*xFAg7_2RbNA#^DUsd>7XmJ=&MW9|6Rx)@pq@ynH;c+H%n+{=*gvGR$Kp zP6|%Ai^`NO7MDpuA~tB`R<tU(S8p>ZhPz%)*2&_oexNYxtXT*ec zwAz>sXyhhYClWh%n2sn&I$YpojyjzMcq2UQYio8BU_s9NYEVPid1prVHsPAFNqSsM zrP~s;RudCEw0BwAW*q(P!6~|o+OtcN3zh*fj#F? zYx#Aji#zEBO=)x)jq1L^|| z=HvM3^GbvumG8NM++9fGk{em2lpUKp$w-|oK+-5sM-`V2haz7(Y{U0G_3^CrKEk9) zM0qe8Qix_C+Bc;*8Sy9`SYR6_Y%R4%ss|0`(%m@A*ZL0_zL$f-ftgI9V2%{)IRkAt zw5&f=mEOLQqdX^dfqrCDFA&qz5a{7|IM`L?t#iaNMp?Rao+s*U{%K5`3 zpA*-Qbh2O>KF#)~S;d-I`L!l=rHfCK5}LipZNNh%Odk^ZDX_0nt=AZLZupb>{JpDy zp4OxzS$jFvUp^6b%Z~>sf6a+rB~Qv!SP$a1+kI5Y-INe7Lf}tYZor_G z{)D0=QXekWX%QUJK7--;NG98A1gStWO<+tIMmx9eaOQ;U8u7q@3^s%k;fTw~h7?lh zkbi**oYMbeq>9(9ioqs;XnrP(rKbNY;)q3!P^ZZY#JLkzYV{85R>JvGM8!xYTwx}R zbyuJ!b$t?a^{mJ+=IB8AwtLsjC0tc|0{M*sW3Bn{*~FWfwBW!|=0I~%txGU(EU6K3 zA%mR3Cr#pa$}HbE^5+U!^aDcSWNK8{XOzck@d9lrOs1pA!kI3a+>p%3ErQpG-R(Ws@wsZwZ!d{p=& z4rFJ&o2mgQ_d)!I6RW_JE~LCMq0BQUvYepKF)hk2%v95Q>632xeH`Qf|J#pArW{OP zNWO=aD$}c)$qL&v0L5oX<03>b`0b6%*BK>Dqc*xmMi-D zB$3U6LE_#sZq0<*JSaAY*bLScxv{(2!(N)UwYS&ylIdJW>@i6bJ9wkAN zX4&b>_)KjI6{T7%|E#v7c-1UX&>1UoB7X(4SteRgtX-0&**!LUOE9EX&EU4OoyXa% z)lendRl~Z*^6JLAZWYx8U)+C&{jM<8ErNq-Oaj#7U|m$RAJC||bhjHZFT>njNhHbP zB?xkdhcMghU&xA}`D621QFObPQcb@>eK{JjZB6&3w8|Cqp@Sj~lP0A0i!FP)3^{U0 z@AiLSeDN0Zk86!45mQ-}yydRSdE_)Bw;BADHcNw>kjRTCKaF&L4~=6mnb)X`ZE#!c zP<)Bn{GZads_kIzEu@9K9%=x-nH17%(V?Ch%dY#5%P* z4Os+32&V&=BW5;PQ~e+;wo_8ct&Q84(JKxCg?52ZSB#av2_V>_Je;4vlR?RwI)8}; z*7B^*v75+}pm2StzruN%e03=H^C3ay+$i=cCvvEH&unGP9%=Tqa<5r_vRf_)27O=6 zFe-z<0xS~r(;J(o8CKDpc@ImID#xm`?cQw3CmyDy{PP`Wy&F4HRJx z&FX1OXB)~CmMi{&DgXQ5g8nzI|A`^S`Na_btI*-U_4A7%{>2dgVu*h+#J?Egh6HT% zgmkw#nrRtPp2qw&546^R*`w6hulJqH{Q&_Ll1FVld1Bw$Z$u@ocwU)$HH2S=*b+fs z>Ip2j5*`m@eb3z><}?I0E8<9@@V$@+FYLGGi6w|UZ@kTfWU|?`6)yMi0riB1-KgXV z4ln-F1Ua&c6@)LUI4$7G z_6>0LNGr5ArJCx7_MiGBZD*_;EtnNT0n1Pp8pY@}^XOSV9u<=LU@*jjA+dxKBTmRs9?Z*OLLL7ZbfES1#ArVg4AVvX9fcHSp z7cNrt+f-;bK3+4F&ObnZnFx$!hqo|hq}rw@>VHW*hpW#dc8I3MKIvNN2q5Qd`sOyi zP9MW2GrsP5)yF$_19R)cs5`-y>P<8vqB8DWLUEv~Szr7k$T7O>cU>RO6h304OV>f$ z=TucGMrR*yrOJ4UGxYc{V?>Bff7Sa?*ja0v7dPlGO8@HqBW;W)>^~t$+7mU$jUEMb z3CIGirFG-mcKK`4U+E{i89z%+^-d397JrCsNy2gE>q@3Of9Pba^^3~$sebLIfZAN4 zd`IqWL>e)o9v&B{Q6yWEeaIwjZ2aRu+7BW1Bh&2~T$_7qGn9RsymY~>f*xPsgq1XZ zU1yGFE9FZKGXcji$Qq<9Z7h{IH^Cpxd2oE8wkl!Y1UNBD-#|$xnjS&>8Fc)`@$+=gZ zk34UanENFKFS=zXu8_A2yeUHR{=~wyQ5`Tns{tspY|d~b^rg%{1x)`Qsg{ELEvNqT zunx|ysXUqLZ%L{jVSBnSZi&r`rRSNg>^M}hmZf`Q+ahgVK?H}PLXV;@%BLr!o!0$1 zj0?Y83*ywxXf@`*sUKFZOa{^1ELa{0W~U36{UKv;*L_xXXlRyblug9(>23y1&YkyMJk6dg zbAV5N|ItTrVJW5NtEuW*CRYqv=47;IH%FPFh5=8If-^_qmZr-1VfIm43AW zKkUKWeDt}np7p%pZ9A7``>vBK#JDa>!?4ha_1M3!BkF8rD|EC@x^PMi5oib@NZw?- z`8&gfmc8r796ET%uPnhyG%{MVRm}<$5i039py6Q6xqRll=MZDYVZjrH(nn(y>~kK; zfU7NqYGrv35;jQd9N1VsVYyh8c7EVWU&r=(sAt^Bn*-=0`z|D1wx;bV_C?gTM%!A; z77W-^MX=M#D7VREdf#Ek)jAi#>mwid_Q>Sx^TtUmzyZfZods-FzEpID;jw;dFpK!6 z9Cz}ji>sI}sG`(T$as>BNw{wOY-~9h*=jOkJkBxotzyXfGEubCg)l2oo+Mp3>$C^& zBTD)T{$m!~_GT35af`0H4%?$Y!jYy!S!AjzX;|jd6{5PHXw!~srU6Wvr;^vx_Sp(Z ziw>ME9-RyPU&pi*m(XRDf&me%B}(yeQxC-&i` zgE;>X@~66eh+dAjtf!7M&Fd*GvA41vsQi9@EVnyg! zVw{rD`S)tXejX~W*e2XY^A}%fvhUxh;R{re;%_N2tl`G90k>b3x|y8?K979K)QZC* zFCbuhy^U{7IZoq3Z!}`WgsdM~=Fu(iW~>73H+Ow21Q9eM+IdJIArevjLVZSr_U3%b zJGtgCN&l_t6bM^mr1g;KcteXWc{&A#WyC}aGap%DT=LOuwXa@kCVQm2s7TwXzkwuL zqr3xMt^E5>p(>3%K6SX8kW!crf)$<1E+p_kK~tK$R=ivW%(^#W>gYOnjt7F#_-lnBOQcP~R(~o7uoQGTxTh z7Cb3EuVE8)fcsSSUx??j?XtZ*xZTW~^6HkCZN0EJPJM*CBYO_*aGUGdrDrt+M#fgn z*BkHhH5AVrndHb!2lz><`1ZK`xUeGcHN6#-l>!SsXv(C;7aPiy9`4z4HQ>N?EYI@6 z$$0SgSq~E_dIMnCytb!leNzaYUKxTOr^F#?xNfa6%>s?a8x^sFsTe>h?+~`pctiHw z1s3%#7U8n9$5U&_{P;%IL&}47!Fz5LfpLH!%;wBJEP#N-HlgqsyaGruaV9~+W?=U? zKojVXPT}z3IJ=#Y+;e^+DYpvIE=bJpKPgCE{!Ea)kEliy3Oz&4Mhv}*2xk)J2Q8x` z9bVI!MPDqH)}xgar!YQ#S$SGaa#%_KJ#kwS{{W z!)EXli4nY_8uPeaBS018x5ovK^u++V3mP6@_}ZAe?RXE(FjBO!uAjlwy{TU90nc06{QNCKBH!#=Y<<@U43t>SWa{MQsz>@z78D zv8+&(Uzkd4EKlKpn*-EjB*!6cWdALj;l@?AOBu?DDe7sm02;xYPVMH~hsU)|-)ddn z3B}6#t_@&1L?^iZvdX!boXsjHlQ9$eoeSp@9nhY-*<{}|^mORbs@6Dx)Gmpch2!1= z=w>Uk9;-*}as=TNSH#p7hrwa=1izZT=R>m&|b- zy|hf=33xC}QTceoKP*rnz$YAv<15Uf`E5Eyr_s`frmSCA2EOQB&Q{_rxr<+O-*A9_VB-=n+LwQiS&Y zskTJw4l2bo`_^N9tuGqL^S$xGsJ2>>pQ$Zxrg3+WB9tgN_YVt#Y@RQQb<_Plpu4AA zYmvnn%+fxyeT~S~V%3t5VL$~>%Vk#2Zp+ z}ra9B3LHwArY5~olI2&LrZz4Qn zFEg>{Tuy+p@A&rZ<+M;60vxJtJfV|#~X`;{xwKDl~N&h zk3IhZSK`^%7e|0K>J$h!g$xc*MD{V{jpL%f6c%M6d2XzDu20UqXaHy`0z%9gC&eX& z6_I@rH4%M5(`$U2L|iyvcJAzvc_+o$M_39YrhqhGsN}?_3gOXmGR=6qJ9aN!K&9Us z&!8V*eH;)y4b@9;riWrjN3U;JFmsyElhptqH(MxD;&qu96K12N_F-yKFBRK!}>L zNc*jOSa8qmm?vu^5z#Z79n8cB_F#(v0JKp2>!#1glWBzWvp*Eq}$SH~|GQ`T!FOm|PC<1XrLdL0H(-z3As2oPq~ zwxcr#{nK%#GVRYk->OwYT$0cu8!Jb*0DFguFMbyl^T z0BPnO>_Xx7)I>p8Rsu0R$hu+<2LT3Kn>2Tgb%jq*C$q1weNP=`pluRQ3jPMAJIc1D z##eY#&|)5pD9;xX>Li~WDbi7*YsOo)(S;($cQ%}uO?JJ4z&F{B8|rzKi|G(O%R&h; zC1pc=C{vCzLNFN*J-Awl2p&_)zK!uyU^iT169BTdnw1BUqdV(L>!}asz@#?r2D5si z+C-wF=clm!+3>Ve?y4s}j;gJj(OWXlSnQcZjuaQx zwK1|b7^tl|xZRwA7Y#1{LZTIOXlon=YiLV@SqbfcP`4VtI)=!sO@Gc_upOevfj9P{VymeyKC!fRjFrM$daGVRfMg1Hx zJ!LkRL8Ce`->`I1rKbzb7H;%-owr`GRjnkCn!Y&Xte=Q`5fmxwz7*ktgIQY(ECW7l zcF7!`(8T${gQF=O(V4a+fAg%*-A1et-pGix=uh1qCe)w+5Vnj{)!3&bEk$^waJ)@H z+x8dhq1*TgciPPZZq=U;wC@qrus6S_%|+4Pldc=J^6~M}+fTw2SgNeyT1Y<<2 zm69)ownjyRKbxr}o>CbHI(!)U^wmayv>JCg(X+)5AOMf{WijaJ-fIozd$JJ>Bo(AF zv~SVUd}ipxN{!rd!07x{by*!h zRf&o3%_{CX@On|&{+a2|pZ96XX5tAC-?^C7>Sub7A@Cmxv<`kY)-W57@7c0Puot{w zAl>&g?+kLa%vq?<(I2nt>U*W=+#O5X7s{+O6C^z1Pd8tK_vZih>2)gBiDRHK#}m`$ zM=j#VxNW&}X=*rEmzay@Ys^RLhLrkxT7FrLu4YPh=oH7ct7MaOy+fCh)~AQz@Pu30 zm{rh#NQXv_ZPZqTfw&JmPt=-~^Kc$1yg?`M)1>-OaYV>KB$Kg75*c=8O)n5hQx2fA z8$GbL^ri;i6|p0#O4wp-?W-gf9aMxCnEKvr7?!#dyid|zMl(-iATu^OD`U&s6Lq+# zf=Sj}Mt?M%*32|j^8#QL=%(;v2PdL~S_e{EyWt!a8LA+bPO`hpF+;lyJEwDMpVF6Q zrcj7(t#&Q%=W5Iq=TTjsAL=m&tN?r*-Cv;ebjMQ_n~ptQSf7IS;?BRe>dQ3vcIj5D z(V#(t{l`YGHw z@d<F~)K}d%Qd@A#&KD!m%vR151Gbbwl=hSVJ3Rs^TFtDQyJYKen<+~I#I0|MFBlGW*xQGBx<=@SKEV2TC7 zjXY8Mi+8_YczgeEWP*SH(SKrum#P2SdI<(N1O?#zTosU#6N7{T0Dy4nEq~tA_#U)E zsTfU2MCMIgq8oG9CoPw5A_g>(zp~h%yl4KllLbx7ekup6#o@4XmdaE|^aa>L0LAii ztiu^~K9^47a)Twd4o`z+8yhQAnhxqKTZ3hi&Pu}?CMLPl$zqLuM8f%56)poJ0}#;r z-!R+%(Z(-E_!lGmixK|C2>)V)e=)+ZG8>Pm?{chZGYd{>WBXTM2dcDdUxkM@uWNbn zKhkw%%07$G!oCd~pQ^aoaJcW;9M-qo(r$KLNkks&LUSK}HFR^3e-g^US zf_r)y_0X25g)!6^X%nj9s{ds2?s4$ZcVpvm>?QD<2Y^rdNyY?Qo;SfWRHur74&kVT zRZ6Hde~AIEG|r%xvs60nSJN*~{ECVHHmMLh*#W}b-(s~V{oFVG&4OtuX{|wGf>(Cm zP*{kGhzK(Q5oRc?jq{$W5do~~Bsz@6M`fQRnneDXyyY$`vV(%@ZBUzF>UokT?OC=~_iJ6(1nVFfX z#LUcCVrD2YGcz+YzUt|oo!j^BdA&RD?Vi~^r4J!b&&)`3b2E#`^k4jA_Ke0vi!T)s zVXP#?*icY_A&)RM77=D7OTMRMzmbR1# zL@EFhHcgsoSP2_40r zo6Cs%v3KV%d|!V_c8u1b#hi(v+vg{dz;*^f{N~&n9qCyMVvmV=>&8KP*MIDEF`pK3 z(p~hhN?4mXs{_+D%Kgr0hwXG)mN;bvqoj)W=sYna)}stZgGIV;)%V_~!bWNV-j?QV zH2ZQaeQ27BXb!fiOaNgzcsw7?Q20gDk>fjNj!0n=tO_E+asq|#)NBTT*+AMAgG^51-^=)ga|h?eZG6CM2}P*YqbV+xbjxR(N|d0Hem z2$}7Z|DuOTU69tKr8Sl!gHToy6e<_nBK;5+5Pbxz|;uHjapiPD_lKgZ=FpJ+OJwQhUBO+$-wM3Rp{4{6pd9?atatG&Y=9_IGv z`DyKtiN6(*YHZ}yy>F%>1s@>HrkH&zRI=G*4~*PUQ62T$8{a4!D!Nfub_kmZQQle8 z?bX@LFaITn{L!U`;yHDpZyLrmusyh7t~iNt>|bIWd7GVjA7`?ox+6&`ckb9uC|NNtMMe6%Z6z~!3ESo&0+xjsWM9W_<_<>J!w5G z8+KR9#Fye^7E_ZJ7SWc4Xw0@rd+S(-ihuay;5qcKDL)+IeV$lnI{2mb8sF>SIt-Zf zF@1m>CV9gKBZoo9mE!Wx@$%1c`EMcgyX#km{`SPb*Q(#1`0WYAKOMh*$HecL_&+Bm zetld2`1oG(?`s?XGyl=FaXmirHC^0{4w%pm7nrgg=;a_Yq9yOVw6VW=TVVK)devET zZad?UuD4Ys#s2^z>377kqy?;ZjL^XO=CPsV;-arN)&%r3eO*7O6{18|G?E}?2$Xot zzy`5X`iDJ%mr7dV53V6eB$=a^j$$*Tm+aQLu0F~||lQ!=a=Ltg= zm;gVQW_&m=(4)#Cgh)d;Q(3VMClr3lzhH+3badT**Gd(z$o7v0SvNM8x}2|R;&qdC zTw9dJ08i_?34tV@ZK>5RYlgayly8kKg%oGJ$!vnT7Bu9ds2tNY{6=`rT~O8RPNPld zD}{PZ)32Hd0cNcmyX}%jo)O%+Qou$$y1!lW0(N*n)lqj`PSdiFx>rtB*q>(=7|q}) zB^BG2$wTeThD=P=8$PBUa+WV`nD4}I4NZCrASH4rKl!s~VjjI%*ta2%5_k7(gl2hy ze!zpFs@xpTP)+q{Uo=}b2kb32q!pQ zC+$FvME%Dc1i&{5_?7|a2&Q7!X2LHi9VaxgI_r@;(A}Yo&<4RH#8l>XO7&IakQOyf zY5d}WMBXI+OmC7pOJ)7E9xLtCZyfK&B%cs<(C|patT-hOp)qW84A8M}fGEEv1yJIK zdNPX$s{(eg%az|Zwy2=u*oz+&43FVS())op`@K9xYK8Wt@BpKvcR9e_yclnTp<3WV zN@(z#1_{EvNL@%WlO-yggO~_qt;)Pi$9C|9c~&rCy_A#T?5z1R;#&2Hb_=AP*ypYp zR|1}Rl@*(#c-lmGo+~-&FK*fu!@xs)U&6&sNN)AzlXrjB|0}WmRM#b(@-yn`XP8p` zss5x7^n#zu+7FhZFcmk=%{Ikh`SX{SP&;1O#HkNlrVHoy?F8& zL-y|_Pco2fV)AfBK-4d{=|RcZhwEMdXFh5$6xga@$!j7JbUlX@n*_cEOGmw!K_Gk~ z;RXqlS~6!hg6i;_XWIZzc&EMN$rbdxL`?r?(#d)Xc<(ayS#K05adKc4Qnem<7l(<} zj?hwyrXc^ds6b6hv^Fi)h>2*#Tqt2KoZ@fYAmm)ga)tx_DHMDad{sEkk+kM1X^{cw z)?#)&o4VB%;11hnFUIllHt`aB5$JTrw2^^q-{}t6a?Y{Rknrs3QWiZV5!QaSos=;P;6ogbJqk{@Ro};dGe{c zgS}?;HFr+Rc?ozA#c22L=8>$2fFhQ3QQ6pcA{ zzzyt*BlsumDG?K^DA$L$O^8@w`A!dx@VgWDlbb+r@d@eISY|;lA*kpx?#S)kCg~$; z-{(n3Qb)C*iza~Xb_R&ldwQmt!k=H?i2^`wd}g3%2{NI(4UZS^;Q5Nixh?{;Q}5_I zv~0#4u70cF_vFgDPavJRfS14LWg_fuvUH6kUZj1}S+E;zJ_rpWB1aCKE zidv}ZSh*g1QiP8!`PRKZzQbsa+3t?Gf~f6X9;|yx(PiXtRSQIoVqd_ND4BUDP>T!n z%H`08O9AiE5Zj{G-V|ya+fFPbk=r=kCHMG~?3SiBPm(1h>CRubC6*za>tT4^otJZ% z=?lV6JRL5Fo!Zb1^DKZ5kq5SFqBonKsIGwYT2!bO8O!Z;E+-PYIf=F2_dnZ zz>>k$d{bX*e;E@|f1!mgXQD-6CyPgixQUY&W9Jy>5U(Tz2B~xcG-?fmhpXQg4%tv z6`>+ZJ90IOoF3!%JtLddnXc6_-Bn>cux#D0^3NF?a;2vbjR^tP9Lq5vZ32Kpa|&|f zSMo~7eV!P^pCGchfLLHvX|g=Q-?FZ>Ghx+t`Kb?o4)GY--M^mU@V|;THN9R*cw1O4 zBiwP(89WZcM9r@}=8MXOevodDs$PPh;u39tf#1Rg?ORgYo3_>xP~4tM ztPtKV96K6qGym*kKF4^g=gmK=aI{fg>JbBM0)!;oD^uN3A{y7|{Nhr8-O#%VEcKHW zf^XG*GZ>M05#9%#xaQnHHDly0?XXk>X>0JBWuE{~92mY)Vge9SS2jae;Bx+^c09YZ z)zTM0DuZ(EJ@9A&z6%EvO$$GVM6+w%W7YAg$R-3w(yCJE`XjCEyu3-_RnFNXiHny7 z==l>65<>q*0lpo#|NHa;tJ7#SfgUigkKEmgOYa=joGrRSjC9pGalBDBn5Dse1~bk( zdC_6L(nFEjPv3>**0a>cdLiXIO58}Tj)o?m66@I(CVvrHjo|$zz4GXx1ox+f`%ef> zO9+IuAk;6jT8_SRbKkG{4sHr*MGf(d6ALLe-|D)1$rTb_ycFL_tU zVe69x*w;2cQUgdFM+Js9z)?W>>7hVlmd8BNzuPQ9^)y|olCknU6fp?#0llRl1L;aa z`q7brbpKluC}=(WfiCGnXbU7@S9H0;ZyNE&zcs=@E0NHFqxkUjELeJv&8%m9x(mz( zM^I_H_JIxO9KD*R4T`mA1jqvDqAK3KDKg!e?=~*_Qjnxv5nP56W?gV$ z!*heM#4hJ3R8!VteMCoFxIuzWR2vNkIEN!#!1sp|6#a?i-J+8y+xK4=<$MySHU4x0 zBi@dD^my_{?me;k* z*D(LU>TszW*%5_^=0?MWerYg*PE^L^Vl@L4J=vDdAaY2)bT{uqBt5K~zIsmsnz7|L zIYa=ssH{cTtGI0PyIL~ydi)R|TcUqph}$UuY}3P^c7%U%?z7bHyF&n%=cu1h*FbnAh@zq&hEws^)P ztVZ*{rSxni{;)XEONaRu!ixMgkIF3(2ijwiC$uP(%@X<>$j1p)UJ&&Baff(h3AGXM zSur;y(E)d9Pn5VsI1G@+lzg73MbbuUhK(SKuhyI$oLSI&xNU`0u#o8a!sfT{kzm-{ z9!xAMx9b3nlmmyf@7&&L%k^#_q1ae@S8lSP>dGcIUqainvEB1wK3u^18v_R#sCZygR)W{91?Y`PxDX0N zZH*8C@T0SNHap>{Cj zOeXdZ^nBEV=?y@Pwi7}*k=$iP&LpKq8YdwdqAp9R$7B0jVDP?t{vo}L2;)`85A0<|cpJThTzsg^L0c&bLro{-5 za=TZ%KuHX?q|<{KYrm4G0xOc2ssx*T%uPa)fEyk7SPHyw$C=3)PX&ZY%-&^u*PA3@ zSv~P1-rzynj@1>-s`tzfr~@1=L>2sD4LP*7o~4gd(H1JNwA;W+xs!Ezt*6Y28& z{v@PFTGZ|Q1C}#8BmpGk@C>YQy5aJ6q?PLXgPrcUd+t~0Ag?B@I-6Ygjjf=5?x)bgIG6BM*Iw2=nXfBOoXs)fR9A6?GD@b&yQ+9pmMiRuT52v|AEoz$lv`)iI*calndTyR1b zUOu^<*y7YLmi1Z2eRazmiVs2z5y`8W6Q9SNrWU0@(!4fBK&>k9ycMy3w)u54Q3zX~ zyI-orwt3a-^GwRVgrRT5Q0bQV}ThHg))qBTiLWy7o1N4 z)@YyQz$j+T!=$(*3V&1xy(;u6eV*BUk2?|Yj?bbG#+sl{W%iw1Qr@>6cw)^ycNv8A z!#Zf+fmxfIcI@#cXfG3G`c-P7U@;>PEZV@q%G~#?vcr3e9UfUE1UMh zI6l!L^YV{L<5;vHz8gC;EKp{&hX6buD*)^<5=K z>tF(~t+{XsPr<#2;ay1`%}KyNne6(q0?5m1Km7z(5174EeT<07p!iI%`+H8g&ady! zBAKy;sK(awCanB*ew|dN6A<9VMdT24uu9?nFFat3)3oQ?xYJkE-CEow6tnJfbUty#hElBtv;)B7g9$oIp*-M8StCt zdxhu2EUPrGZnf8tAtsWsAjXJvSI_H#HQKw7Z(ooes1W*Hjs)( z^~AygD$(#G4Z1!}amU%|ns%Us@#4cf;E@4{pgMRptT65iK^sbqs5yf|CwDyO?rdc_IS!aXbmvEji>xef?P}tfoNZ zy^JaNm-+=D7flO{^8lZURjW-MkW2OJjL=tt&K3H=nw=5dvdnO6l+~5{wM{!X%gSEN z`G?%3;K4$8+bUz_jDTBOz%o0*uOANnxBdMle0lztFvY(K-`|AqZ^HLC;rpBL{Z07( zCVc;MTX0V244oHN`5Y zlX;hvcIngb1Ef|{8w#Fn^3i!bZb!HQ-pD?L6`Q;~m|XDhE@$*J@zWgl-7q5Uv;Pp@ z{4E#vTQ2Tj=$-!Gu9Ed%e|-6wngD{+BkF0^)QE

  • >OeB;}?FReoj!Mxh?Fjio%X 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%0Ag6hW7|#?#mk>3xHU!SelH^pT`n7Gp0z3*-Qie)r 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 v?@93z}nD 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 Date: Tue, 15 Jul 2025 16:44:50 +1000 Subject: [PATCH 002/244] update set profile image modal and ProCTA modals --- .../Settings/ThreadSettingsViewModel.swift | 6 +- Session/Settings/SettingsViewModel.swift | 26 ++- .../Modals & Toast/ConfirmationModal.swift | 66 ++++++-- .../Components/SwiftUI/ProCTAModal.swift | 158 ++++++++++++------ .../Utilities/UIImage+Utilities.swift | 25 +++ 5 files changed, 212 insertions(+), 69 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 77d465a6a1..4cd9cfd3e5 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1642,11 +1642,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob }, icon: .rightPlus, style: .circular, + showPro: false, accessibility: Accessibility( identifier: "Image picker", label: "Image picker" ), dataManager: dependencies[singleton: .imageDataManager], + onProBageTapped: nil, onClick: { [weak self] onDisplayPictureSelected in self?.onDisplayPictureSelected = onDisplayPictureSelected self?.showPhotoLibraryForAvatar() @@ -1655,7 +1657,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob confirmTitle: "save".localized(), confirmEnabled: .afterChange { info in switch info.body { - case .image(let source, _, _, _, _, _, _): return (source?.imageData != nil) + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) default: return false } }, @@ -1665,7 +1667,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob dismissOnConfirm: false, onConfirm: { [weak self] modal in switch modal.info.body { - case .image(.some(let source), _, _, _, _, _, _): + case .image(.some(let source), _, _, _, _, _, _, _, _): guard let imageData: Data = source.imageData else { return } self?.updateGroupDisplayPicture( diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 42e24bb68c..4a8b545cbe 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -554,16 +554,34 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl source: currentFileName .map { try? dependencies[singleton: .displayPictureManager].filepath(for: $0) } .map { ImageDataManager.DataSource.url(URL(fileURLWithPath: $0)) }, - placeholder: UIImage(named: iconName).map { - ImageDataManager.DataSource.image(iconName, $0) + placeholder: Lucide.image(icon: .image, size: 40).map { image in + ImageDataManager.DataSource.image( + iconName, + image + .withTintColor(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1), renderingMode: .alwaysTemplate) + .withCircularBackground(backgroundColor: #colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) + ) }, icon: .rightPlus, style: .circular, + showPro: true, accessibility: Accessibility( identifier: "Upload", label: "Upload" ), dataManager: dependencies[singleton: .imageDataManager], + onProBageTapped: { [weak self, dependencies] in + let sessionProModal: ModalHostingViewController = ModalHostingViewController( + modal: ProCTAModal( + delegate: SessionProState(using: dependencies), + touchPoint: .animatedProfileImage( + isSessionProActivated: dependencies[cache: .libSession].isSessionPro + ), + dataManager: dependencies[singleton: .imageDataManager] + ) + ) + self?.transitionToScreen(sessionProModal, transitionType: .present) + }, onClick: { [weak self] onDisplayPictureSelected in self?.onDisplayPictureSelected = onDisplayPictureSelected self?.showPhotoLibraryForAvatar() @@ -572,7 +590,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl confirmTitle: "save".localized(), confirmEnabled: .afterChange { info in switch info.body { - case .image(let source, _, _, _, _, _, _): return (source?.imageData != nil) + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) default: return false } }, @@ -582,7 +600,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl dismissOnConfirm: false, onConfirm: { [weak self] modal in switch modal.info.body { - case .image(.some(let source), _, _, _, _, _, _): + case .image(.some(let source), _, _, _, _, _, _, _, _): guard let imageData: Data = source.imageData else { return } self?.updateProfile( diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 891e9f4684..cf58292a7d 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -38,6 +38,17 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() + private lazy var proImageTapGestureRecognizer: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(proImageTapped) + ) + proImageStackViewContainer.addGestureRecognizer(result) + result.isEnabled = false + + return result + }() + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.mediumFontSize) @@ -174,6 +185,27 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() + private lazy var proImageStackView: UIStackView = { + let proBadge: SessionProBadge = SessionProBadge(size: .small) + let label: UILabel = UILabel() + label.font = .systemFont(ofSize: Values.smallFontSize) + label.themeTextColor = .textSecondary + label.text = "proAnimatedDisplayPictureModalDescription".localized() + + let result: UIStackView = UIStackView(arrangedSubviews: [ proBadge, label ]) + result.axis = .horizontal + result.spacing = Values.verySmallSpacing + + return result + }() + + private lazy var proImageStackViewContainer: UIView = { + let result: UIView = UIView() + result.isHidden = true + + return result + }() + private lazy var imageViewContainer: UIView = { let result: UIView = UIView() result.isHidden = true @@ -233,6 +265,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { textToConfirmContainer, textViewContainer, textViewErrorLabel, + proImageStackViewContainer, imageViewContainer ] ) @@ -300,12 +333,6 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { } public override func populateContentView() { - let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( - target: self, - action: #selector(contentViewTapped) - ) - contentView.addGestureRecognizer(gestureRecogniser) - contentView.addSubview(mainStackView) contentView.addSubview(closeButton) @@ -344,6 +371,11 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.pin(.top, to: .top, of: imageViewContainer) profileView.pin(.bottom, to: .bottom, of: imageViewContainer) + proImageStackViewContainer.addSubview(proImageStackView) + proImageStackView.center(.horizontal, in: proImageStackViewContainer) + proImageStackView.pin(.top, to: .top, of: proImageStackViewContainer) + proImageStackView.pin(.bottom, to: .bottom, of: proImageStackViewContainer) + mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) closeButton.pin(.right, to: .right, of: contentView, withInset: -8) @@ -501,11 +533,13 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { contentStackView.addArrangedSubview(radioButton) } - case .image(let source, let placeholder, let icon, let style, let accessibility, let dataManager, let onClick): + case .image(let source, let placeholder, let icon, let style, let showPro, let accessibility, let dataManager, _, let onClick): imageViewContainer.isAccessibilityElement = (accessibility != nil) imageViewContainer.accessibilityIdentifier = accessibility?.identifier imageViewContainer.accessibilityLabel = accessibility?.label mainStackView.spacing = 0 + contentStackView.spacing = Values.verySmallSpacing + proImageStackViewContainer.isHidden = !showPro imageViewContainer.isHidden = false profileView.clipsToBounds = (style == .circular) profileView.setDataManager(dataManager) @@ -518,6 +552,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { internalOnBodyTap = onClick contentTapGestureRecognizer.isEnabled = false imageViewTapGestureRecognizer.isEnabled = true + proImageTapGestureRecognizer.isEnabled = true case .inputConfirmation(let explanation, let textToConfirm): explanationLabel.themeAttributedText = explanation @@ -632,7 +667,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { @objc private func imageViewTapped() { internalOnBodyTap?({ [weak self, info = self.info] valueUpdate in switch (valueUpdate, info.body) { - case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, let icon, let style, let accessibility, let dataManager, let onClick)): + case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, let icon, let style, let showPro, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): self?.updateContent( with: info.with( body: .image( @@ -642,8 +677,10 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { placeholder: placeholder, icon: icon, style: style, + showPro: showPro, accessibility: accessibility, dataManager: dataManager, + onProBageTapped: onProBadgeTapped, onClick: onClick ) ) @@ -654,6 +691,11 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { }) } + @objc private func proImageTapped() { + guard case .image(_, _, _, _, let showPro, _, _, let onProBadgeTapped, _) = info.body, showPro else { return } + onProBadgeTapped?() + } + @objc internal func confirmationPressed() { internalOnConfirm?(self) } @@ -1009,8 +1051,10 @@ public extension ConfirmationModal.Info { placeholder: ImageDataManager.DataSource?, icon: ProfilePictureView.ProfileIcon = .none, style: ImageStyle, + showPro: Bool, accessibility: Accessibility?, dataManager: ImageDataManagerType, + onProBageTapped: (() -> Void)?, onClick: ((@escaping (ConfirmationModal.ValueUpdate) -> Void) -> Void) ) @@ -1045,12 +1089,13 @@ public extension ConfirmationModal.Info { lhsOptions == rhsOptions ) - case (.image(let lhsSource, let lhsPlaceholder, let lhsIcon, let lhsStyle, let lhsAccessibility, _, _), .image(let rhsSource, let rhsPlaceholder, let rhsIcon, let rhsStyle, let rhsAccessibility, _, _)): + case (.image(let lhsSource, let lhsPlaceholder, let lhsIcon, let lhsStyle, let lhsShowPro, let lhsAccessibility, _, _, _), .image(let rhsSource, let rhsPlaceholder, let rhsIcon, let rhsStyle, let rhsShowPro, let rhsAccessibility, _, _, _)): return ( lhsSource == rhsSource && lhsPlaceholder == rhsPlaceholder && lhsIcon == rhsIcon && lhsStyle == rhsStyle && + lhsShowPro == rhsShowPro && lhsAccessibility == rhsAccessibility ) @@ -1078,11 +1123,12 @@ public extension ConfirmationModal.Info { warning.hash(into: &hasher) options.hash(into: &hasher) - case .image(let source, let placeholder, let icon, let style, let accessibility, _, _): + case .image(let source, let placeholder, let icon, let style, let showPro, let accessibility, _, _, _): source.hash(into: &hasher) placeholder.hash(into: &hasher) icon.hash(into: &hasher) style.hash(into: &hasher) + showPro.hash(into: &hasher) accessibility.hash(into: &hasher) case .inputConfirmation(let explanation, let textToConfirm): diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 937aab690e..953e3b5c1e 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -34,6 +34,7 @@ public struct ProCTAModal: View { afterClosed: afterClosed ) { close in VStack(spacing: 0) { + // Background images ZStack { if let animatedAvatarImageURL = touchPoint.animatedAvatarImageURL { SessionAsyncImage( @@ -46,7 +47,14 @@ public struct ProCTAModal: View { .frame(maxWidth: .infinity) }, placeholder: { - EmptyView() + if let data = try? Data(contentsOf: animatedAvatarImageURL) { + Image(uiImage: UIImage(data: data) ?? UIImage()) + .resizable() + .aspectRatio((1522.0/1258.0), contentMode: .fit) + .frame(maxWidth: .infinity) + } else { + EmptyView() + } } ) } @@ -75,71 +83,110 @@ public struct ProCTAModal: View { maxWidth: .infinity, alignment: .bottom ) - + // Content VStack(spacing: Values.largeSpacing) { // Title - HStack(spacing: Values.smallSpacing) { - Text("upgradeTo".localized()) - .font(.system(size: Values.largeFontSize)) - .bold() - .foregroundColor(themeColor: .textPrimary) - - SessionProBadge_SwiftUI(size: .large) + if case .animatedProfileImage(let isSessionProActivated) = touchPoint, isSessionProActivated { + HStack(spacing: Values.smallSpacing) { + SessionProBadge_SwiftUI(size: .large) + + Text("proActivated".localized()) + .font(.system(size: Values.largeFontSize)) + .bold() + .foregroundColor(themeColor: .textPrimary) + } + } else { + HStack(spacing: Values.smallSpacing) { + Text("upgradeTo".localized()) + .font(.system(size: Values.largeFontSize)) + .bold() + .foregroundColor(themeColor: .textPrimary) + + SessionProBadge_SwiftUI(size: .large) + } } + // Description, Subtitle - VStack(spacing: Values.smallSpacing) { + VStack(spacing: 0) { + if case .animatedProfileImage(let isSessionProActivated) = touchPoint, isSessionProActivated { + HStack(spacing: Values.verySmallSpacing) { + Text("proAlreadyPurchased".localized()) + .font(.system(size: Values.smallFontSize)) + .foregroundColor(themeColor: .textSecondary) + + SessionProBadge_SwiftUI(size: .small) + } + } + Text(touchPoint.subtitle) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .textSecondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } + // Benefits - VStack(alignment: .leading, spacing: Values.mediumSmallSpacing) { - ForEach( - 0.. UIImage? { + let originalSize = self.size + let diameter = max(originalSize.width, originalSize.height) * 2 + let newSize = CGSize(width: diameter, height: diameter) + + let renderer = UIGraphicsImageRenderer(size: newSize) + let renderedImage = renderer.image { context in + let ctx = context.cgContext + + // Draw the circular background + let circleRect = CGRect(origin: .zero, size: newSize) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: circleRect) + + // Draw the original image centered + let imageOrigin = CGPoint( + x: (newSize.width - originalSize.width) / 2, + y: (newSize.height - originalSize.height) / 2 + ) + self.draw(at: imageOrigin) + } + + return renderedImage + } } From 374a0cbd78013dccf92b84632436163325a5c539 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 17 Jul 2025 16:15:45 +1000 Subject: [PATCH 003/244] add pro proof logic for animated profile picture --- Session.xcodeproj/project.pbxproj | 4 ++ Session/Settings/SettingsViewModel.swift | 52 ++++++++++++------- SessionMessagingKit/Configuration.swift | 3 +- .../Migrations/_027_AddProfileProProof.swift | 20 +++++++ .../Database/Models/Profile.swift | 15 +++++- .../Jobs/UpdateProfilePictureJob.swift | 10 +++- .../LibSession+GroupMembers.swift | 3 +- .../Config Handling/LibSession+Pro.swift | 7 ++- .../LibSession+UserProfile.swift | 3 +- .../VisibleMessage+Profile.swift | 12 +++-- .../MessageReceiver+Groups.swift | 9 ++-- .../MessageReceiver+MessageRequests.swift | 3 +- .../MessageReceiver+VisibleMessages.swift | 5 +- .../Utilities/DisplayPictureManager.swift | 6 +-- .../Utilities/Profile+CurrentUser.swift | 12 +++-- .../ProfilePictureView+Convenience.swift | 6 +++ .../Modals & Toast/ConfirmationModal.swift | 1 + .../Components/ProfilePictureView.swift | 10 +++- .../Components/SessionImageView.swift | 21 ++++++-- SessionUIKit/Types/ImageDataManager.swift | 12 +++++ 20 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 SessionMessagingKit/Database/Migrations/_027_AddProfileProProof.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1723e4180f..c66441c146 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94AAB15F2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; + 94AAB1622E28742300A6FA18 /* _027_AddProfileProProof.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1612E28742200A6FA18 /* _027_AddProfileProProof.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; @@ -1532,6 +1533,7 @@ 94AAB1592E24BD6800A6FA18 /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimation.webp; sourceTree = ""; }; + 94AAB1612E28742200A6FA18 /* _027_AddProfileProProof.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_AddProfileProProof.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; @@ -3953,6 +3955,7 @@ FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */, FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */, 94CD95C02E0CBF1C0097754D /* _026_AddProMessageFlag.swift */, + 94AAB1612E28742200A6FA18 /* _027_AddProfileProProof.swift */, ); path = Migrations; sourceTree = ""; @@ -6456,6 +6459,7 @@ FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, + 94AAB1622E28742300A6FA18 /* _027_AddProfileProProof.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 4a8b545cbe..a4868e6527 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -570,17 +570,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl label: "Upload" ), dataManager: dependencies[singleton: .imageDataManager], - onProBageTapped: { [weak self, dependencies] in - let sessionProModal: ModalHostingViewController = ModalHostingViewController( - modal: ProCTAModal( - delegate: SessionProState(using: dependencies), - touchPoint: .animatedProfileImage( - isSessionProActivated: dependencies[cache: .libSession].isSessionPro - ), - dataManager: dependencies[singleton: .imageDataManager] - ) - ) - self?.transitionToScreen(sessionProModal, transitionType: .present) + onProBageTapped: { [weak self] in + self?.showSessionProCTAIfNeeded() }, onClick: { [weak self] onDisplayPictureSelected in self?.onDisplayPictureSelected = onDisplayPictureSelected @@ -598,16 +589,26 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl cancelEnabled: .bool(currentFileName != nil), hasCloseButton: true, dismissOnConfirm: false, - onConfirm: { [weak self] modal in + onConfirm: { [weak self, dependencies] modal in switch modal.info.body { case .image(.some(let source), _, _, _, _, _, _, _, _): guard let imageData: Data = source.imageData else { return } - - self?.updateProfile( - displayPictureUpdate: .currentUserUploadImageData(imageData), - onComplete: { [weak modal] in modal?.close() } - ) - + + let isAnimatedImage: Bool = ImageDataManager.isAnimatedImage(imageData) + guard isAnimatedImage && !dependencies[cache: .libSession].isSessionPro else { + self?.updateProfile( + displayPictureUpdate: .currentUserUploadImageData( + data: imageData, + sessionProProof: !isAnimatedImage ? nil : + dependencies.mutate(cache: .libSession, { $0.getProProof() }) + ), + onComplete: { [weak modal] in modal?.close() } + ) + return + } + + self?.showSessionProCTAIfNeeded() + default: modal.close() } }, @@ -623,6 +624,21 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) } + @discardableResult func showSessionProCTAIfNeeded() -> Bool { + guard dependencies[feature: .sessionProEnabled] else { return false } + let sessionProModal: ModalHostingViewController = ModalHostingViewController( + modal: ProCTAModal( + delegate: SessionProState(using: dependencies), + touchPoint: .animatedProfileImage( + isSessionProActivated: dependencies[cache: .libSession].isSessionPro + ), + dataManager: dependencies[singleton: .imageDataManager] + ) + ) + self.transitionToScreen(sessionProModal, transitionType: .present) + return true + } + private func showPhotoLibraryForAvatar() { Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: dependencies) { [weak self] in DispatchQueue.main.async { diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 799018a187..3e46ba1af7 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -45,7 +45,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _025_DropLegacyClosedGroupKeyPairTable.self ], [ - _026_AddProMessageFlag.self + _026_AddProMessageFlag.self, + _027_AddProfileProProof.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_027_AddProfileProProof.swift b/SessionMessagingKit/Database/Migrations/_027_AddProfileProProof.swift new file mode 100644 index 0000000000..62141548fa --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_027_AddProfileProProof.swift @@ -0,0 +1,20 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _027_AddProfileProProof: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "AddProfileProProof" + static let minExpectedRunDuration: TimeInterval = 0.1 + static var createdTables: [(FetchableRecord & TableRecord).Type] = [] + + static func migrate(_ db: Database, using dependencies: Dependencies) throws { + try db.alter(table: "profile") { t in + t.add(column: "sessionProProof", .text) + } + + Storage.update(progress: 1, for: self, in: target, using: dependencies) + } +} diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 2452ae6caf..b11ee2c5c9 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -31,6 +31,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco case blocksCommunityMessageRequests case lastBlocksCommunityMessageRequests + + case sessionProProof } /// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant) @@ -63,6 +65,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco /// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated public let lastBlocksCommunityMessageRequests: TimeInterval? + /// The Pro Proof for when this profile is updated + public let sessionProProof: String? + // MARK: - Initialization public init( @@ -75,7 +80,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco profileEncryptionKey: Data? = nil, lastProfilePictureUpdate: TimeInterval? = nil, blocksCommunityMessageRequests: Bool? = nil, - lastBlocksCommunityMessageRequests: TimeInterval? = nil + lastBlocksCommunityMessageRequests: TimeInterval? = nil, + sessionProProof: String? = nil ) { self.id = id self.name = name @@ -87,6 +93,7 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco self.lastProfilePictureUpdate = lastProfilePictureUpdate self.blocksCommunityMessageRequests = blocksCommunityMessageRequests self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests + self.sessionProProof = sessionProProof } } @@ -116,6 +123,7 @@ extension Profile: CustomStringConvertible, CustomDebugStringConvertible { lastProfilePictureUpdate: \(lastProfilePictureUpdate.map { "\($0)" } ?? "null"), blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null"), lastBlocksCommunityMessageRequests: \(lastBlocksCommunityMessageRequests.map { "\($0)" } ?? "null") + sessionProProof: \(sessionProProof.map { "\($0)" } ?? "null") ) """ } @@ -149,7 +157,8 @@ public extension Profile { profileEncryptionKey: profileKey, lastProfilePictureUpdate: try? container.decode(TimeInterval?.self, forKey: .lastProfilePictureUpdate), blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests), - lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval?.self, forKey: .lastBlocksCommunityMessageRequests) + lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval?.self, forKey: .lastBlocksCommunityMessageRequests), + sessionProProof: try? container.decode(String?.self, forKey: .sessionProProof) ) } @@ -166,6 +175,7 @@ public extension Profile { try container.encodeIfPresent(lastProfilePictureUpdate, forKey: .lastProfilePictureUpdate) try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) try container.encodeIfPresent(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests) + try container.encodeIfPresent(sessionProProof, forKey: .sessionProProof) } } @@ -180,6 +190,7 @@ public extension Profile { if let profileKey: Data = profileEncryptionKey, let profilePictureUrl: String = profilePictureUrl { dataMessageProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) + // TODO: Add ProProof if needed } do { diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index 6ccf4baa78..b5c35572a1 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -4,6 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit +import SessionUIKit // MARK: - Log.Category @@ -54,7 +55,14 @@ public enum UpdateProfilePictureJob: JobExecutor { let profile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies) let displayPictureUpdate: DisplayPictureManager.Update = profile.profilePictureFileName .map { dependencies[singleton: .displayPictureManager].loadDisplayPictureFromDisk(for: $0) } - .map { .currentUserUploadImageData($0) } + .map { data in + let isAnimated: Bool = ImageDataManager.isAnimatedImage(data) + return .currentUserUploadImageData( + data: data, + sessionProProof: !isAnimated ? nil : + dependencies.mutate(cache: .libSession, { $0.getProProof() }) + ) + } .defaulting(to: .none) Profile diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 60bb37583b..556fc4f84f 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -141,7 +141,8 @@ internal extension LibSessionCacheType { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), sentTimestamp: TimeInterval(Double(serverTimestampMs) * 1000), diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift index b67745e7f8..afb8a8a0a9 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift @@ -28,13 +28,18 @@ public extension LibSessionCacheType { return false } - func validateProProof(_ proProof: String?) -> Bool { + func validateProProof(for message: Message?) -> Bool { if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { return dependencies[feature: .treatAllIncomingMessagesAsProMessages] } return false } + func validateProProof(for profile: Profile?) -> Bool { + guard let profile = profile else { return false } + return profile.sessionProProof != nil + } + func getProProof() -> String? { guard isSessionPro else { return nil diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 723139a75a..1654b2b1fc 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -62,7 +62,8 @@ internal extension LibSessionCacheType { return .currentUserUpdateTo( url: profilePictureUrl, key: profilePic.get(\.key), - fileName: nil + fileName: nil, + sessionProProof: getProProof() // TODO: double check if this is needed ) }(), sentTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 6cc6ca9e8e..9c83debc6b 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -11,6 +11,7 @@ public extension VisibleMessage { public let profileKey: Data? public let profilePictureUrl: String? public let blocksCommunityMessageRequests: Bool? + public let sessionProProof: String? // MARK: - Initialization @@ -18,7 +19,8 @@ public extension VisibleMessage { displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil, - blocksCommunityMessageRequests: Bool? = nil + blocksCommunityMessageRequests: Bool? = nil, + sessionProProof: String? = nil ) { let hasUrlAndKey: Bool = (profileKey != nil && profilePictureUrl != nil) @@ -26,6 +28,7 @@ public extension VisibleMessage { self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) self.blocksCommunityMessageRequests = blocksCommunityMessageRequests + self.sessionProProof = sessionProProof } // MARK: - Proto Conversion @@ -40,7 +43,8 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, - blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil) + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), + sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -87,7 +91,8 @@ public extension VisibleMessage { return VMProfile( displayName: displayName, profileKey: proto.profileKey, - profilePictureUrl: profileProto.profilePicture + profilePictureUrl: profileProto.profilePicture, + sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -140,6 +145,7 @@ extension VisibleMessage.VMProfile { self.profileKey = profile.profileEncryptionKey self.profilePictureUrl = profile.profilePictureUrl self.blocksCommunityMessageRequests = blocksCommunityMessageRequests + self.sessionProProof = profile.sessionProProof } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index ce46c6d7dc..bc52da0575 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -133,7 +133,8 @@ extension MessageReceiver { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, @@ -250,7 +251,8 @@ extension MessageReceiver { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, @@ -601,7 +603,8 @@ extension MessageReceiver { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index 065b52a25e..838fb5c965 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -41,7 +41,8 @@ extension MessageReceiver { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), sentTimestamp: messageSentTimestamp, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 04ff5eb220..9c1bb1bdef 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -40,7 +40,8 @@ extension MessageReceiver { return .contactUpdateTo( url: profilePictureUrl, key: profileKey, - fileName: nil + fileName: nil, + contactProProof: profile.sessionProProof ) }(), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, @@ -162,7 +163,7 @@ extension MessageReceiver { using: dependencies ) do { - let isProMessage: Bool = dependencies.mutate(cache: .libSession, { $0.validateProProof(message.proProof) }) + let isProMessage: Bool = dependencies.mutate(cache: .libSession, { $0.validateProProof(for: message) }) let processedMessageBody: String? = Self.truncateMessageTextIfNeeded( message.text, isProMessage: isProMessage, diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 6abaa666a5..9640ed9a5e 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -31,11 +31,11 @@ public class DisplayPictureManager { case none case contactRemove - case contactUpdateTo(url: String, key: Data, fileName: String?) + case contactUpdateTo(url: String, key: Data, fileName: String?, contactProProof: String?) case currentUserRemove - case currentUserUploadImageData(Data) - case currentUserUpdateTo(url: String, key: Data, fileName: String?) + case currentUserUploadImageData(data: Data, sessionProProof: String?) + case currentUserUpdateTo(url: String, key: Data, fileName: String?, sessionProProof: String?) case groupRemove case groupUploadImageData(Data) diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 23d8c15486..45e43f3614 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -91,7 +91,7 @@ public extension Profile { .mapError { _ in DisplayPictureError.databaseChangesFailed } .eraseToAnyPublisher() - case .currentUserUploadImageData(let data): + case .currentUserUploadImageData(let data, let sessionProProof): return dependencies[singleton: .displayPictureManager] .prepareAndUploadDisplayPicture(imageData: data) .mapError { $0 as Error } @@ -103,7 +103,8 @@ public extension Profile { displayPictureUpdate: .currentUserUpdateTo( url: result.downloadUrl, key: result.encryptionKey, - fileName: result.fileName + fileName: result.fileName, + sessionProProof: sessionProProof ), sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies @@ -181,8 +182,8 @@ public extension Profile { profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil)) profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) - case (.contactUpdateTo(let url, let key, let fileName), false), - (.currentUserUpdateTo(let url, let key, let fileName), true): + case (.contactUpdateTo(let url, let key, let fileName, let proProof), false), + (.currentUserUpdateTo(let url, let key, let fileName, let proProof), true): if url != profile.profilePictureUrl { profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url)) } @@ -237,6 +238,9 @@ public extension Profile { canStartJob: dependencies[singleton: .appContext].isMainApp ) } + + // Update Pro Proof + profileChanges.append(Profile.Columns.sessionProProof.set(to: proProof)) // Update the 'lastProfilePictureUpdate' timestamp for either external or local changes profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 2d6c3f258d..58a7673456 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -52,6 +52,7 @@ public extension ProfilePictureView { { return (Info( source: .url(URL(fileURLWithPath: path)), + shouldAnimated: (threadVariant == .community), icon: profileIcon ), nil) } @@ -68,6 +69,7 @@ public extension ProfilePictureView { case .hero: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), + shouldAnimated: true, inset: UIEdgeInsets( top: 12, left: 12, @@ -99,6 +101,7 @@ public extension ProfilePictureView { ) ) ), + shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) }), icon: profileIcon ), additionalProfile @@ -117,12 +120,14 @@ public extension ProfilePictureView { size: size.multiImageSize ) ), + shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: other) }), icon: additionalProfileIcon ) } .defaulting( to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), + shouldAnimated: false, renderingMode: .alwaysTemplate, themeTintColor: .white, inset: UIEdgeInsets( @@ -152,6 +157,7 @@ public extension ProfilePictureView { size: size.viewSize ) ), + shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) }), icon: profileIcon ), nil diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index cf58292a7d..0e7fb9be03 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -546,6 +546,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.update( ProfilePictureView.Info( source: (source ?? placeholder), + shouldAnimated: true, icon: icon ) ) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index 65c4425021..def747845c 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -6,6 +6,7 @@ import Combine public final class ProfilePictureView: UIView { public struct Info { let source: ImageDataManager.DataSource? + let shouldAnimated: Bool let renderingMode: UIImage.RenderingMode? let themeTintColor: ThemeValue? let inset: UIEdgeInsets @@ -15,6 +16,7 @@ public final class ProfilePictureView: UIView { public init( source: ImageDataManager.DataSource?, + shouldAnimated: Bool, renderingMode: UIImage.RenderingMode? = nil, themeTintColor: ThemeValue? = nil, inset: UIEdgeInsets = .zero, @@ -23,6 +25,7 @@ public final class ProfilePictureView: UIView { forcedBackgroundColor: ForcedThemeValue? = nil ) { self.source = source + self.shouldAnimated = shouldAnimated self.renderingMode = renderingMode self.themeTintColor = themeTintColor self.inset = inset @@ -431,11 +434,13 @@ public final class ProfilePictureView: UIView { private func prepareForReuse() { imageView.image = nil + imageView.shouldAnimateImage = true imageView.contentMode = .scaleAspectFill imageContainerView.clipsToBounds = clipsToBounds imageContainerView.themeBackgroundColor = .backgroundSecondary additionalImageContainerView.isHidden = true additionalImageView.image = nil + additionalImageView.shouldAnimateImage = true additionalImageContainerView.clipsToBounds = clipsToBounds imageViewTopConstraint.isActive = false @@ -479,7 +484,9 @@ public final class ProfilePictureView: UIView { case (.some(let source), .some(let renderingMode)) where source.directImage != nil: imageView.image = source.directImage?.withRenderingMode(renderingMode) - case (.some(let source), _): imageView.loadImage(source) + case (.some(let source), _): + imageView.shouldAnimateImage = info.shouldAnimated + imageView.loadImage(source) default: imageView.image = nil } @@ -525,6 +532,7 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = false case (.some(let source), _): + additionalImageView.shouldAnimateImage = additionalInfo.shouldAnimated additionalImageView.loadImage(source) additionalImageContainerView.isHidden = false diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index 7b50a4d73b..ebdf802c91 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -32,29 +32,43 @@ public class SessionImageView: UIImageView { } } + public var shouldAnimateImage: Bool { + didSet { + if shouldAnimateImage { + startAnimationLoop() + } else { + stopAnimationLoop() + } + } + } + // MARK: - Initialization /// Use the `init(dataManager:)` initializer where possible to avoid explicitly needing to add the `dataManager` instance public init() { self.dataManager = nil + self.shouldAnimateImage = false super.init(frame: .zero) } - public init(dataManager: ImageDataManagerType) { + public init(dataManager: ImageDataManagerType, shouldAnimateImage: Bool = true) { self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage super.init(frame: .zero) } - public init(frame: CGRect, dataManager: ImageDataManagerType) { + public init(frame: CGRect, dataManager: ImageDataManagerType, shouldAnimateImage: Bool = true) { self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage super.init(frame: frame) } - public init(image: UIImage?, dataManager: ImageDataManagerType) { + public init(image: UIImage?, dataManager: ImageDataManagerType, shouldAnimateImage: Bool = true) { self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage /// If we are given a `UIImage` directly then it's a static image so just use it directly and don't worry about using `dataManager` super.init(image: image) @@ -175,6 +189,7 @@ public class SessionImageView: UIImageView { @MainActor public func startAnimationLoop() { guard + shouldAnimateImage, let frames: [UIImage] = animationFrames, let durations: [TimeInterval] = animationFrameDurations, frames.count > 1, diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 97cda9566b..8fda02a7cd 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -508,6 +508,18 @@ public extension ImageDataManager { } } +// MARK: - ImageDataManager.isAnimatedImage + +public extension ImageDataManager { + static func isAnimatedImage(_ imageData: Data?) -> Bool { + guard let data: Data = imageData, let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { + return false + } + let frameCount = CGImageSourceGetCount(imageSource) + return frameCount > 1 + } +} + // MARK: - ImageDataManager.ProcessedImageData public extension ImageDataManager { From 76256cf66fc0705b324daf4d5adec52100cb8010 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 21 Jul 2025 15:11:26 +1000 Subject: [PATCH 004/244] minor fix --- SessionUIKit/Components/SwiftUI/ProCTAModal.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 5b5b14c0e9..70d9a20708 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -19,7 +19,7 @@ public struct ProCTAModal: View { case .longerMessages: return "HigherCharLimitCTA.webp" case .animatedProfileImage: - return "session_pro_modal_background_animated_profile_image" + return "AnimatedProfileCTA.webp" case .morePinnedConvos: return "PinnedConversationsCTA.webp" case .groupLimit(let isAdmin): From 0d65ae2b8c0352cb33941f25fd43566520a6d7ce Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 22 Jul 2025 16:07:55 +1000 Subject: [PATCH 005/244] add logic for current user's pro state --- .../Conversations/ConversationVC+Interaction.swift | 10 ++-------- Session/Shared/Types/SessionProState.swift | 4 ++-- SessionMessagingKit/Database/Models/Profile.swift | 9 +++++++++ .../Utilities/ProfilePictureView+Convenience.swift | 6 +++--- SessionUIKit/Components/SwiftUI/ProCTAModal.swift | 12 +++++++++--- .../AttachmentApprovalViewController.swift | 4 ++-- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index d1c74eea08..1f07af008c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -23,8 +23,7 @@ extension ConversationVC: SendMediaNavDelegate, UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, - GifPickerViewControllerDelegate, - SessionProCTADelegate + GifPickerViewControllerDelegate { // MARK: - Open Settings @@ -243,7 +242,7 @@ extension ConversationVC: self.hideInputAccessoryView() let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: self, + delegate: SessionProState(using: dependencies), variant: .longerMessages, dataManager: viewModel.dependencies[singleton: .imageDataManager], afterClosed: { [weak self] in @@ -256,11 +255,6 @@ extension ConversationVC: return true } - - func upgradeToPro(completion: (() -> Void)?) { - viewModel.dependencies.set(feature: .mockCurrentUserSessionPro, to: true) - completion?() - } // MARK: - SendMediaNavDelegate diff --git a/Session/Shared/Types/SessionProState.swift b/Session/Shared/Types/SessionProState.swift index 9ad9bac44a..329e738526 100644 --- a/Session/Shared/Types/SessionProState.swift +++ b/Session/Shared/Types/SessionProState.swift @@ -10,8 +10,8 @@ public class SessionProState: SessionProCTADelegate { self.dependencies = dependencies } - public func upgradeToPro(completion: (() -> Void)?) { + public func upgradeToPro(completion: ((_ result: Bool) -> Void)?) { dependencies.set(feature: .mockCurrentUserSessionPro, to: true) - completion?() + completion?(true) } } diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index b11ee2c5c9..991cc14438 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -351,6 +351,15 @@ public extension Profile { // MARK: - Convenience public extension Profile { + func shoudAnimateProfilePicture(using dependencies: Dependencies) -> Bool { + guard self.id == dependencies[cache: .general].sessionId.hexString else { + return dependencies.mutate(cache: .libSession, { $0.validateProProof(for: self) }) + } + + return dependencies[cache: .libSession].isSessionPro + } + + // MARK: - Truncation enum Truncation { diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 58a7673456..5161d41806 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -101,7 +101,7 @@ public extension ProfilePictureView { ) ) ), - shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) }), + shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), icon: profileIcon ), additionalProfile @@ -120,7 +120,7 @@ public extension ProfilePictureView { size: size.multiImageSize ) ), - shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: other) }), + shouldAnimated: other.shoudAnimateProfilePicture(using: dependencies), icon: additionalProfileIcon ) } @@ -157,7 +157,7 @@ public extension ProfilePictureView { size: size.viewSize ) ), - shouldAnimated: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) }), + shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), icon: profileIcon ), nil diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 70d9a20708..8b8ab14c21 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -101,19 +101,22 @@ public struct ProCTAModal: View { let dismissType: Modal.DismissType let afterClosed: (() -> Void)? + let afterUpgrade: (() -> Void)? public init( delegate: SessionProCTADelegate?, variant: ProCTAModal.Variant, dataManager: ImageDataManagerType, dismissType: Modal.DismissType = .recursive, - afterClosed: (() -> Void)? = nil + afterClosed: (() -> Void)? = nil, + afterUpgrade: (() -> Void)? = nil ) { self.delegate = delegate self.variant = variant self.dataManager = dataManager self.dismissType = dismissType self.afterClosed = afterClosed + self.afterUpgrade = afterUpgrade } public var body: some View { @@ -278,7 +281,10 @@ public struct ProCTAModal: View { HStack(spacing: Values.smallSpacing) { // Upgrade Button ShineButton { - delegate?.upgradeToPro { + delegate?.upgradeToPro { result in + if result { + afterUpgrade?() + } close() } } label: { @@ -324,7 +330,7 @@ public struct ProCTAModal: View { // MARK: - SessionProCTADelegate public protocol SessionProCTADelegate: AnyObject { - func upgradeToPro(completion: (() -> Void)?) + func upgradeToPro(completion: ((_ result: Bool) -> Void)?) } struct ProCTAModal_Previews: PreviewProvider { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 4645b61ff1..c795a9fd0e 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -714,9 +714,9 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate // MARK: - extension AttachmentApprovalViewController: SessionProCTADelegate { - public func upgradeToPro(completion: (() -> Void)?) { + public func upgradeToPro(completion: ((_ result: Bool) -> Void)?) { dependencies.set(feature: .mockCurrentUserSessionPro, to: true) - completion?() + completion?(true) } } From bf78f1f1436dc369fbda280bd3460278f0309469 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 23 Jul 2025 16:27:39 +1000 Subject: [PATCH 006/244] got the session pro state across all profile picture view --- Session.xcodeproj/project.pbxproj | 8 +- .../Views & Modals/IncomingCallBanner.swift | 3 +- .../ConversationVC+Interaction.swift | 4 +- Session/Conversations/ConversationVC.swift | 3 +- .../Input View/MentionSelectionView.swift | 4 +- .../Message Cells/VisibleMessageCell.swift | 4 +- .../Settings/ThreadSettingsViewModel.swift | 2 +- Session/Home/HomeVC.swift | 3 +- .../MessageInfoScreen.swift | 3 +- Session/Settings/SettingsViewModel.swift | 2 +- Session/Shared/FullConversationCell.swift | 6 +- Session/Shared/Types/SessionProState.swift | 17 ---- .../Views/SessionCell+AccessoryView.swift | 3 +- .../UIContextualAction+Utilities.swift | 2 +- .../ProfilePictureView+Convenience.swift | 9 ++- .../Utilities/SessionProState.swift | 38 +++++++++ .../SimplifiedConversationCell.swift | 3 +- .../Modals & Toast/ConfirmationModal.swift | 7 +- .../Components/ProfilePictureView.swift | 79 ++++++++++++++++++- .../Components/SwiftUI/ProCTAModal.swift | 10 ++- .../AttachmentApprovalViewController.swift | 11 +-- 21 files changed, 167 insertions(+), 54 deletions(-) delete mode 100644 Session/Shared/Types/SessionProState.swift create mode 100644 SessionMessagingKit/Utilities/SessionProState.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 9a8077d82f..3926def8cb 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -199,13 +199,13 @@ 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */; }; 94AAB1572E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; - 94AAB15A2E24BD6800A6FA18 /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1592E24BD6800A6FA18 /* SessionProState.swift */; }; 94AAB15D2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94AAB15F2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94AAB1622E28742300A6FA18 /* _027_AddProfileProProof.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1612E28742200A6FA18 /* _027_AddProfileProProof.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; + 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B6BAF52E30A88800E718BB /* SessionProState.swift */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BC2E0908340097754D /* LibSession+Pro.swift */; }; @@ -1532,11 +1532,11 @@ 94AAB1502E1F752600A6FA18 /* CyclicGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CyclicGradientView.swift; sourceTree = ""; }; 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShineButton.swift; sourceTree = ""; }; 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = PinnedConversationsCTA.webp; sourceTree = ""; }; - 94AAB1592E24BD6800A6FA18 /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimation.webp; sourceTree = ""; }; 94AAB1612E28742200A6FA18 /* _027_AddProfileProProof.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_AddProfileProProof.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; + 94B6BAF52E30A88800E718BB /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; 94CD95BC2E0908340097754D /* LibSession+Pro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Pro.swift"; sourceTree = ""; }; @@ -3560,6 +3560,7 @@ C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( + 94B6BAF52E30A88800E718BB /* SessionProState.swift */, FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, @@ -4325,7 +4326,6 @@ FD71164128E2C83500B47552 /* Types */ = { isa = PBXGroup; children = ( - 94AAB1592E24BD6800A6FA18 /* SessionProState.swift */, FD71164928E3EA5B00B47552 /* DismissType.swift */, FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */, FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */, @@ -6411,6 +6411,7 @@ FDB5DAE02A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift in Sources */, FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */, 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */, + 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */, FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */, FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, @@ -6627,7 +6628,6 @@ FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */, FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */, FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */, - 94AAB15A2E24BD6800A6FA18 /* SessionProState.swift in Sources */, FDB3DA8D2E24881B00148F8D /* ImageLoading+Convenience.swift in Sources */, 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */, 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */, diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 41277a293f..a69ce10f16 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -25,7 +25,8 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, - dataManager: dependencies[singleton: .imageDataManager] + dataManager: dependencies[singleton: .imageDataManager], + sessionProState: dependencies[singleton: .sessionProState] ) private lazy var displayNameLabel: UILabel = { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 1f07af008c..327632b5db 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -242,9 +242,9 @@ extension ConversationVC: self.hideInputAccessoryView() let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: SessionProState(using: dependencies), + delegate: dependencies[singleton: .sessionProState], variant: .longerMessages, - dataManager: viewModel.dependencies[singleton: .imageDataManager], + dataManager: dependencies[singleton: .imageDataManager], afterClosed: { [weak self] in self?.showInputAccessoryView() self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 783071e8bb..32fd1c926b 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1388,7 +1388,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa let profilePictureView = ProfilePictureView( size: .navigation, - dataManager: viewModel.dependencies[singleton: .imageDataManager] + dataManager: viewModel.dependencies[singleton: .imageDataManager], + sessionProState: viewModel.dependencies[singleton: .sessionProState] ) profilePictureView.update( publicKey: threadData.threadId, // Contact thread uses the contactId diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 0c207a6ce4..4bfa67be59 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -127,7 +127,8 @@ private extension MentionSelectionView { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, - dataManager: nil + dataManager: nil, + sessionProState: nil ) private lazy var displayNameLabel: UILabel = { @@ -212,6 +213,7 @@ private extension MentionSelectionView { currentUserSessionIds: currentUserSessionIds ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: profile.id, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 2cb4202909..69fda28b88 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -67,7 +67,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, - dataManager: nil + dataManager: nil, + sessionProState: nil, ) lazy var bubbleBackgroundView: UIView = { @@ -323,6 +324,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView.isHidden = !cellViewModel.canHaveProfile profilePictureView.alpha = (profileShouldBeVisible ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.authorId, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 7847374e86..2022620ef3 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1837,7 +1837,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob { let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: SessionProState(using: dependencies), + delegate: dependencies[singleton: .sessionProState], variant: .morePinnedConvos( isGrandfathered: (pinnedConversationsNumber > LibSession.PinnedConversationLimit) ), diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 07eab88f38..c9a4046434 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -575,7 +575,8 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Profile picture view let profilePictureView = ProfilePictureView( size: .navigation, - dataManager: viewModel.dependencies[singleton: .imageDataManager] + dataManager: viewModel.dependencies[singleton: .imageDataManager], + sessionProState: viewModel.dependencies[singleton: .sessionProState] ) profilePictureView.accessibilityIdentifier = "User settings" profilePictureView.accessibilityLabel = "User settings" diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 52aa16945d..ae87fff1db 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -297,7 +297,8 @@ struct MessageInfoScreen: View { size: size, info: info, additionalInfo: additionalInfo, - dataManager: dependencies[singleton: .imageDataManager] + dataManager: dependencies[singleton: .imageDataManager], + sessionProState: dependencies[singleton: .sessionProState] ) .frame( width: size.viewSize, diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index ce5a55c885..60ee75e6ea 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -628,7 +628,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl guard dependencies[feature: .sessionProEnabled] else { return false } let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: SessionProState(using: dependencies), + delegate: dependencies[singleton: .sessionProState], variant: .animatedProfileImage( isSessionProActivated: dependencies[cache: .libSession].isSessionPro ), diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index 8b073df5e9..6921cd568b 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -23,7 +23,8 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, - dataManager: nil + dataManager: nil, + sessionProState: nil ) private lazy var displayNameLabel: UILabel = { @@ -278,6 +279,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // MARK: --Search Results public func updateForDefaultContacts(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -356,6 +358,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC using dependencies: Dependencies ) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -435,6 +438,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC ) ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/Session/Shared/Types/SessionProState.swift b/Session/Shared/Types/SessionProState.swift deleted file mode 100644 index 329e738526..0000000000 --- a/Session/Shared/Types/SessionProState.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import SessionUIKit -import SessionUtilitiesKit - -public class SessionProState: SessionProCTADelegate { - public let dependencies: Dependencies - - public init(using dependencies: Dependencies) { - self.dependencies = dependencies - } - - public func upgradeToPro(completion: ((_ result: Bool) -> Void)?) { - dependencies.set(feature: .mockCurrentUserSessionPro, to: true) - completion?(true) - } -} diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index cc1c02acac..9346017315 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -172,7 +172,7 @@ extension SessionCell { }() private lazy var profilePictureView: ProfilePictureView = { - let result: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil) + let result: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil, sessionProState: nil) result.translatesAutoresizingMaskIntoConstraints = false result.isHidden = true @@ -557,6 +557,7 @@ extension SessionCell { profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) profilePictureView.size = accessory.size profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: accessory.id, threadVariant: accessory.threadVariant, diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index dc8831500d..9dad15ede3 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -243,7 +243,7 @@ public extension UIContextualAction { { let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: SessionProState(using: dependencies), + delegate: dependencies[singleton: .sessionProState], variant: .morePinnedConvos( isGrandfathered: (pinnedConversationsNumber > LibSession.PinnedConversationLimit) ), diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 5161d41806..49d2196575 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -53,6 +53,7 @@ public extension ProfilePictureView { return (Info( source: .url(URL(fileURLWithPath: path)), shouldAnimated: (threadVariant == .community), + isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), icon: profileIcon ), nil) } @@ -70,6 +71,7 @@ public extension ProfilePictureView { } }(), shouldAnimated: true, + isCurrentUser: false, inset: UIEdgeInsets( top: 12, left: 12, @@ -102,6 +104,7 @@ public extension ProfilePictureView { ) ), shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), icon: profileIcon ), additionalProfile @@ -121,6 +124,7 @@ public extension ProfilePictureView { ) ), shouldAnimated: other.shoudAnimateProfilePicture(using: dependencies), + isCurrentUser: (other.id == dependencies[cache: .general].sessionId.hexString), icon: additionalProfileIcon ) } @@ -128,6 +132,7 @@ public extension ProfilePictureView { to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), shouldAnimated: false, + isCurrentUser: false, renderingMode: .alwaysTemplate, themeTintColor: .white, inset: UIEdgeInsets( @@ -158,6 +163,7 @@ public extension ProfilePictureView { ) ), shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), icon: profileIcon ), nil @@ -197,7 +203,8 @@ public extension ProfilePictureSwiftUI { size: size, info: info, additionalInfo: additionalInfo, - dataManager: dependencies[singleton: .imageDataManager] + dataManager: dependencies[singleton: .imageDataManager], + sessionProState: dependencies[singleton: .sessionProState] ) } } diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionProState.swift new file mode 100644 index 0000000000..25dd608004 --- /dev/null +++ b/SessionMessagingKit/Utilities/SessionProState.swift @@ -0,0 +1,38 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SessionUIKit +import SessionUtilitiesKit +import Combine + +// MARK: - Singleton + +public extension Singleton { + static let sessionProState: SingletonConfig = Dependencies.create( + identifier: "sessionProState", + createInstance: { dependencies in SessionProState(using: dependencies) } + ) +} + +// MARK: - SessionProState + +public class SessionProState: SessionProManagerType { + public let dependencies: Dependencies + public var isSessionProSubject: CurrentValueSubject + public var isSessionProPublisher: AnyPublisher { + isSessionProSubject + .filter { $0 } + .map { _ in () } + .eraseToAnyPublisher() + } + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.isSessionProSubject = CurrentValueSubject(dependencies[cache: .libSession].isSessionPro) + } + + public func upgradeToPro(completion: ((_ result: Bool) -> Void)?) { + dependencies.set(feature: .mockCurrentUserSessionPro, to: true) + self.isSessionProSubject.send(true) + completion?(true) + } +} diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index f61c1212f7..f7b6deaf7b 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -40,7 +40,7 @@ final class SimplifiedConversationCell: UITableViewCell { }() private lazy var profilePictureView: ProfilePictureView = { - let view: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil) + let view: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil, sessionProState: nil) view.translatesAutoresizingMaskIntoConstraints = false return view @@ -88,6 +88,7 @@ final class SimplifiedConversationCell: UITableViewCell { public func update(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { accentLineView.alpha = (cellViewModel.threadIsBlocked == true ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) + profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 0e7fb9be03..dd19b76df2 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -213,7 +213,11 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() - private lazy var profileView: ProfilePictureView = ProfilePictureView(size: .hero, dataManager: nil) + private lazy var profileView: ProfilePictureView = ProfilePictureView( + size: .hero, + dataManager: nil, + sessionProState: nil + ) private lazy var textToConfirmContainer: UIView = { let result: UIView = UIView() @@ -547,6 +551,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { ProfilePictureView.Info( source: (source ?? placeholder), shouldAnimated: true, + isCurrentUser: true, icon: icon ) ) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index def747845c..6ee9b8568b 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -7,6 +7,7 @@ public final class ProfilePictureView: UIView { public struct Info { let source: ImageDataManager.DataSource? let shouldAnimated: Bool + let isCurrentUser: Bool let renderingMode: UIImage.RenderingMode? let themeTintColor: ThemeValue? let inset: UIEdgeInsets @@ -17,6 +18,7 @@ public final class ProfilePictureView: UIView { public init( source: ImageDataManager.DataSource?, shouldAnimated: Bool, + isCurrentUser: Bool, renderingMode: UIImage.RenderingMode? = nil, themeTintColor: ThemeValue? = nil, inset: UIEdgeInsets = .zero, @@ -26,6 +28,7 @@ public final class ProfilePictureView: UIView { ) { self.source = source self.shouldAnimated = shouldAnimated + self.isCurrentUser = isCurrentUser self.renderingMode = renderingMode self.themeTintColor = themeTintColor self.inset = inset @@ -100,6 +103,7 @@ public final class ProfilePictureView: UIView { } private var dataManager: ImageDataManagerType? + private var sessionProState: SessionProManagerType? public var disposables: Set = Set() public var size: Size { didSet { @@ -140,6 +144,14 @@ public final class ProfilePictureView: UIView { } } + public enum CurrentUserProfileImage: Equatable { + case none + case main + case additional + } + + public var shouldAnimateForCurrentUserProUpgrade: CurrentUserProfileImage = .none + // MARK: - Constraints private var widthConstraint: NSLayoutConstraint! @@ -277,7 +289,8 @@ public final class ProfilePictureView: UIView { // MARK: - Lifecycle - public init(size: Size, dataManager: ImageDataManagerType?) { + public init(size: Size, dataManager: ImageDataManagerType?, sessionProState: SessionProManagerType?) { + self.sessionProState = sessionProState self.dataManager = dataManager self.size = size @@ -285,6 +298,16 @@ public final class ProfilePictureView: UIView { clipsToBounds = true setUpViewHierarchy() + + sessionProState?.isSessionProPublisher + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink( + receiveValue: { [weak self] in + self?.startAnimatingIfNeeded() + } + ) + .store(in: &disposables) } public required init?(coder: NSCoder) { @@ -380,6 +403,19 @@ public final class ProfilePictureView: UIView { self.additionalImageView.setDataManager(dataManager) } + public func setSessionProState(_ sessionProState: SessionProManagerType) { + self.sessionProState = sessionProState + sessionProState.isSessionProPublisher + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink( + receiveValue: { [weak self] in + self?.startAnimatingIfNeeded() + } + ) + .store(in: &disposables) + } + // MARK: - Content private func updateIconView( @@ -491,6 +527,10 @@ public final class ProfilePictureView: UIView { default: imageView.image = nil } + if info.isCurrentUser { + self.shouldAnimateForCurrentUserProUpgrade = .main + } + imageView.themeTintColor = info.themeTintColor imageContainerView.themeBackgroundColor = info.backgroundColor imageContainerView.themeBackgroundColorForced = info.forcedBackgroundColor @@ -541,6 +581,10 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = true } + if additionalInfo.isCurrentUser { + self.shouldAnimateForCurrentUserProUpgrade = .additional + } + additionalImageView.themeTintColor = additionalInfo.themeTintColor switch (info.backgroundColor, info.forcedBackgroundColor) { @@ -575,6 +619,28 @@ public final class ProfilePictureView: UIView { ) additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2) } + + public func startAnimatingIfNeeded() { + switch shouldAnimateForCurrentUserProUpgrade { + case .none: + break + case .main: + imageView.shouldAnimateImage = true + case .additional: + additionalImageView.shouldAnimateImage = true + } + } + + public func stopAnimatingIfNeeded() { + switch shouldAnimateForCurrentUserProUpgrade { + case .none: + break + case .main: + imageView.shouldAnimateImage = false + case .additional: + additionalImageView.shouldAnimateImage = false + } + } } import SwiftUI @@ -586,21 +652,28 @@ public struct ProfilePictureSwiftUI: UIViewRepresentable { var info: ProfilePictureView.Info var additionalInfo: ProfilePictureView.Info? let dataManager: ImageDataManagerType + let sessionProState: SessionProManagerType public init( size: ProfilePictureView.Size, info: ProfilePictureView.Info, additionalInfo: ProfilePictureView.Info? = nil, - dataManager: ImageDataManagerType + dataManager: ImageDataManagerType, + sessionProState: SessionProManagerType ) { self.size = size self.info = info self.additionalInfo = additionalInfo self.dataManager = dataManager + self.sessionProState = sessionProState } public func makeUIView(context: Context) -> ProfilePictureView { - ProfilePictureView(size: size, dataManager: dataManager) + ProfilePictureView( + size: size, + dataManager: dataManager, + sessionProState: sessionProState + ) } public func updateUIView(_ profilePictureView: ProfilePictureView, context: Context) { diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 8b8ab14c21..9f49476e45 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -2,6 +2,7 @@ import SwiftUI import Lucide +import Combine public struct ProCTAModal: View { public enum Variant { @@ -95,7 +96,7 @@ public struct ProCTAModal: View { @EnvironmentObject var host: HostWrapper - private var delegate: SessionProCTADelegate? + private var delegate: SessionProManagerType? private let variant: ProCTAModal.Variant private var dataManager: ImageDataManagerType @@ -104,7 +105,7 @@ public struct ProCTAModal: View { let afterUpgrade: (() -> Void)? public init( - delegate: SessionProCTADelegate?, + delegate: SessionProManagerType?, variant: ProCTAModal.Variant, dataManager: ImageDataManagerType, dismissType: Modal.DismissType = .recursive, @@ -327,9 +328,10 @@ public struct ProCTAModal: View { } } -// MARK: - SessionProCTADelegate +// MARK: - SessionProManagerType -public protocol SessionProCTADelegate: AnyObject { +public protocol SessionProManagerType: AnyObject { + var isSessionProPublisher: AnyPublisher { get } func upgradeToPro(completion: ((_ result: Bool) -> Void)?) } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index c795a9fd0e..4e1fa7f5f9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -637,7 +637,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC self.hideInputAccessoryView() let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( - delegate: self, + delegate: dependencies[singleton: .sessionProState], variant: .longerMessages, dataManager: dependencies[singleton: .imageDataManager], afterClosed: { [weak self] in @@ -711,15 +711,6 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate } } -// MARK: - - -extension AttachmentApprovalViewController: SessionProCTADelegate { - public func upgradeToPro(completion: ((_ result: Bool) -> Void)?) { - dependencies.set(feature: .mockCurrentUserSessionPro, to: true) - completion?(true) - } -} - // MARK: GalleryRail extension SignalAttachmentItem: GalleryRailItem { From 8309985c22f64bb9d9a994396878ba653264d5e9 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 24 Jul 2025 14:01:04 +1000 Subject: [PATCH 007/244] [WIP] upload profile picture before expiry --- .../Message Cells/VisibleMessageCell.swift | 2 +- .../Utilities/DisplayPictureManager.swift | 1 + .../Utilities/SessionProState.swift | 3 +-- SessionSnodeKit/Models/FileUploadResponse.swift | 12 +++++++++--- SessionSnodeKit/Types/Network.swift | 9 +++++++++ .../Components/ProfilePictureView.swift | 17 +++++++++++++---- .../Components/SwiftUI/ProCTAModal.swift | 2 +- 7 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 69fda28b88..140556473c 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -68,7 +68,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, dataManager: nil, - sessionProState: nil, + sessionProState: nil ) lazy var bubbleBackgroundView: UIView = { diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 9640ed9a5e..d00a89bb72 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -303,6 +303,7 @@ public class DisplayPictureManager { } .map { [dependencies] fileUploadResponse, finalFilePath, fileName, newEncryptionKey, finalImageData -> UploadResult in let downloadUrl: String = Network.FileServer.downloadUrlString(for: fileUploadResponse.id) + let expries: Double? = fileUploadResponse.expires /// Load the data into the `imageDataManager` (assuming we will use it elsewhere in the UI) Task(priority: .userInitiated) { diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionProState.swift index 25dd608004..feb5e96ffe 100644 --- a/SessionMessagingKit/Utilities/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionProState.swift @@ -18,10 +18,9 @@ public extension Singleton { public class SessionProState: SessionProManagerType { public let dependencies: Dependencies public var isSessionProSubject: CurrentValueSubject - public var isSessionProPublisher: AnyPublisher { + public var isSessionProPublisher: AnyPublisher { isSessionProSubject .filter { $0 } - .map { _ in () } .eraseToAnyPublisher() } diff --git a/SessionSnodeKit/Models/FileUploadResponse.swift b/SessionSnodeKit/Models/FileUploadResponse.swift index 41ba747b0f..e271dc8bf0 100644 --- a/SessionSnodeKit/Models/FileUploadResponse.swift +++ b/SessionSnodeKit/Models/FileUploadResponse.swift @@ -2,9 +2,11 @@ public struct FileUploadResponse: Codable { public let id: String + public let expires: Double? - public init(id: String) { + public init(id: String, expires: Double?) { self.id = id + self.expires = expires } } @@ -18,12 +20,16 @@ extension FileUploadResponse { // that and convert the value to a string so we can be consistent (SOGS is able to handle // an array of Strings for the `files` param when posting a message just fine) if let intValue: Int64 = try? container.decode(Int64.self, forKey: .id) { - self = FileUploadResponse(id: "\(intValue)") + self = FileUploadResponse( + id: "\(intValue)", + expires: try? container.decode(Double?.self, forKey: .expires) + ) return } self = FileUploadResponse( - id: try container.decode(String.self, forKey: .id) + id: try container.decode(String.self, forKey: .id), + expires: try? container.decode(Double?.self, forKey: .expires) ) } } diff --git a/SessionSnodeKit/Types/Network.swift b/SessionSnodeKit/Types/Network.swift index 5913775129..c339a5a991 100644 --- a/SessionSnodeKit/Types/Network.swift +++ b/SessionSnodeKit/Types/Network.swift @@ -131,11 +131,16 @@ public extension Network { requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> PreparedRequest { + var headers: [HTTPHeader: String] = [:] + if dependencies.hasSet(feature: ) { + headers = [.fileCustomTTL : "\()"] + } return try PreparedRequest( request: Request( endpoint: FileServer.Endpoint.file, destination: .serverUpload( server: FileServer.fileServer, + headers: headers, x25519PublicKey: FileServer.fileServerPublicKey, fileName: nil ), @@ -167,3 +172,7 @@ public extension Network { ) } } + +fileprivate extension HTTPHeader { + static let fileCustomTTL: HTTPHeader = "X-FS-TTL" +} diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index 6ee9b8568b..de4b91df7f 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -303,8 +303,12 @@ public final class ProfilePictureView: UIView { .subscribe(on: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink( - receiveValue: { [weak self] in - self?.startAnimatingIfNeeded() + receiveValue: { [weak self] isPro in + if isPro { + self?.startAnimatingIfNeeded() + } else { + self?.stopAnimatingIfNeeded() + } } ) .store(in: &disposables) @@ -409,8 +413,13 @@ public final class ProfilePictureView: UIView { .subscribe(on: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink( - receiveValue: { [weak self] in - self?.startAnimatingIfNeeded() + receiveValue: { [weak self] isPro in + if isPro { + self?.startAnimatingIfNeeded() + } else { + self?.stopAnimatingIfNeeded() + } + } ) .store(in: &disposables) diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 2f74e8057a..5650afbab4 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -345,7 +345,7 @@ public struct ProCTAModal: View { // MARK: - SessionProManagerType public protocol SessionProManagerType: AnyObject { - var isSessionProPublisher: AnyPublisher { get } + var isSessionProPublisher: AnyPublisher { get } func upgradeToPro(completion: ((_ result: Bool) -> Void)?) } From 6ea0f7b8396c66ce75528161104b17905e0ee4f1 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 24 Jul 2025 14:31:28 +1000 Subject: [PATCH 008/244] add dev settings for custom file ttl --- .../Settings/DeveloperSettingsViewModel.swift | 46 +++++++++++++++++-- .../Database/Models/Attachment.swift | 4 +- SessionSnodeKit/Types/Network.swift | 4 +- SessionUtilitiesKit/General/Feature.swift | 4 ++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 4d8981b210..e776a79f8e 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -35,6 +35,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum Section: SessionTableSection { case developerMode + case customTTL case sessionPro case sessionNetwork case general @@ -47,6 +48,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, var title: String? { switch self { case .developerMode: return nil + case .customTTL: return "Custom TTL" case .sessionPro: return "Session Pro" case .sessionNetwork: return "Session Network" case .general: return "General" @@ -69,10 +71,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum TableItem: Hashable, Differentiable, CaseIterable { case developerMode - case enableSessionPro - case proStatus - case proIncomingMessages - case versionBlindedID case scheduleLocalNotification @@ -100,6 +98,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case updatedGroupsDeleteBeforeNow case updatedGroupsDeleteAttachmentsBeforeNow + case shortenFileTTL + + case enableSessionPro + case proStatus + case proIncomingMessages + case createMockContacts case copyDatabasePath case forceSlowDatabaseQueries @@ -140,6 +144,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .versionBlindedID: return "versionBlindedID" case .scheduleLocalNotification: return "scheduleLocalNotification" + case .shortenFileTTL: return "shortenFileTTL" + case .enableSessionPro: return "enableSessionPro" case .proStatus: return "proStatus" case .proIncomingMessages: return "proIncomingMessages" @@ -188,6 +194,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .versionBlindedID: result.append(.versionBlindedID); fallthrough case .scheduleLocalNotification: result.append(.scheduleLocalNotification); fallthrough + case .shortenFileTTL: result.append(.shortenFileTTL); fallthrough + case .enableSessionPro: result.append(.enableSessionPro); fallthrough case .proStatus: result.append(.proStatus); fallthrough case .proIncomingMessages: result.append(.proIncomingMessages); fallthrough @@ -232,6 +240,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let updatedGroupsDeleteBeforeNow: Bool let updatedGroupsDeleteAttachmentsBeforeNow: Bool + let shortenFileTTL: Bool + let sessionProEnabled: Bool let mockCurrentUserSessionPro: Bool let treatAllIncomingMessagesAsProMessages: Bool @@ -282,6 +292,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow], + shortenFileTTL: dependencies[feature: .shortenFileTTL], + sessionProEnabled: dependencies[feature: .sessionProEnabled], mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages], @@ -785,6 +797,26 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) + let customTTL: SectionModel = SectionModel( + model: .customTTL, + elements: [ + SessionCell.Info( + id: .shortenFileTTL, + title: "Shorten File TTL", + subtitle: "Set the TTL for files in the cache to 1 minute", + trailingAccessory: .toggle( + current.shortenFileTTL, + oldValue: previous?.shortenFileTTL + ), + onTap: { [weak self] in + self?.updateFlag( + for: .shortenFileTTL, + to: !current.shortenFileTTL + ) + } + ) + ] + ) let sessionPro: SectionModel = SectionModel( model: .sessionPro, elements: [ @@ -885,6 +917,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, network, disappearingMessages, groups, + customTTL, sessionPro, sessionNetwork, database @@ -991,6 +1024,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil) + case .shortenFileTTL: + guard dependencies.hasSet(feature: .shortenFileTTL) else { return } + + updateFlag(for: .shortenFileTTL, to: nil) + case .enableSessionPro: guard dependencies.hasSet(feature: .sessionProEnabled) else { return } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index a62c945c9a..83f95a3ae2 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -1172,7 +1172,7 @@ extension Attachment { return ( self, try Network.PreparedRequest.cached( - FileUploadResponse(id: fileId), + FileUploadResponse(id: fileId, expires: nil), endpoint: endpoint, using: dependencies ), @@ -1201,7 +1201,7 @@ extension Attachment { return ( self, try Network.PreparedRequest.cached( - FileUploadResponse(id: fileId), + FileUploadResponse(id: fileId, expires: nil), endpoint: endpoint, using: dependencies ), diff --git a/SessionSnodeKit/Types/Network.swift b/SessionSnodeKit/Types/Network.swift index c339a5a991..3c2803f534 100644 --- a/SessionSnodeKit/Types/Network.swift +++ b/SessionSnodeKit/Types/Network.swift @@ -132,8 +132,8 @@ public extension Network { using dependencies: Dependencies ) throws -> PreparedRequest { var headers: [HTTPHeader: String] = [:] - if dependencies.hasSet(feature: ) { - headers = [.fileCustomTTL : "\()"] + if dependencies.hasSet(feature: .shortenFileTTL) { + headers = [.fileCustomTTL : "60"] } return try PreparedRequest( request: Request( diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index 3ac1097085..f9335884d2 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -77,6 +77,10 @@ public extension FeatureStorage { static let treatAllIncomingMessagesAsProMessages: FeatureConfig = Dependencies.create( identifier: "treatAllIncomingMessagesAsProMessages" ) + + static let shortenFileTTL: FeatureConfig = Dependencies.create( + identifier: "shortenFileTTL" + ) } // MARK: - FeatureOption From d137e2bfe598865bf93608d0d79465c31aaea951 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 24 Jul 2025 16:31:34 +1000 Subject: [PATCH 009/244] re-upload profile picture before it expires --- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../Jobs/DisplayPictureDownloadJob.swift | 8 +- .../Jobs/UpdateProfilePictureJob.swift | 78 +++++++++++-------- .../Utilities/DisplayPictureManager.swift | 6 +- .../Utilities/Profile+CurrentUser.swift | 1 + .../Models/FileUploadResponse.swift | 8 +- SessionUIKit/Utilities/Date+Utilities.swift | 8 ++ .../Types/UserDefaultsType.swift | 3 + 8 files changed, 71 insertions(+), 43 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index fb82ae73b4..6fadba5a46 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1730,7 +1730,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob return dependencies[singleton: .displayPictureManager] .prepareAndUploadDisplayPicture(imageData: data) .showingBlockingLoading(in: self?.navigatableState) - .map { url, fileName, key -> DisplayPictureManager.Update in + .map { url, fileName, key, _ -> DisplayPictureManager.Update in .groupUpdateTo(url: url, key: key, fileName: fileName) } .mapError { $0 as Error } diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index 0353f92758..3f981111bb 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -81,7 +81,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { case .failure(let error): failure(job, error, true) } }, - receiveValue: { _, data in + receiveValue: { info, data in // Check to make sure this download is still a valid update guard dependencies[singleton: .storage].read({ db in details.isValidUpdate(db) }) == true else { return @@ -144,6 +144,12 @@ public enum DisplayPictureDownloadJob: JobExecutor { Profile.Columns.lastProfilePictureUpdate.set(to: details.timestamp), using: dependencies ) + if + dependencies[cache: .general].sessionId.hexString == id, + let expires: Date = Date.fromHTTPExpiresHeaders(info.headers["Expires"]) + { + dependencies[defaults: .standard, key: .profilePictureExpiresDate] = expires + } case .group(let id, let url, let encryptionKey): _ = try? ClosedGroup diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index b5c35572a1..9c1ca8423a 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -32,10 +32,52 @@ public enum UpdateProfilePictureJob: JobExecutor { return deferred(job) // Don't need to do anything if it's not the main app } - // Only re-upload the profile picture if enough time has passed since the last upload - guard + let runJob: () -> () = { + // Note: The user defaults flag is updated in DisplayPictureManager + let profile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies) + let displayPictureUpdate: DisplayPictureManager.Update = profile.profilePictureFileName + .map { dependencies[singleton: .displayPictureManager].loadDisplayPictureFromDisk(for: $0) } + .map { data in + let isAnimated: Bool = ImageDataManager.isAnimatedImage(data) + return .currentUserUploadImageData( + data: data, + sessionProProof: !isAnimated ? nil : + dependencies.mutate(cache: .libSession, { $0.getProProof() }) + ) + } + .defaulting(to: .none) + + Profile + .updateLocal( + displayPictureUpdate: displayPictureUpdate, + using: dependencies + ) + .subscribe(on: scheduler, using: dependencies) + .receive(on: scheduler, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .failure(let error): failure(job, error, false) + case .finished: + Log.info(.cat, "Profile successfully updated") + success(job, false) + } + } + ) + } + + if + let profilePicutreExpiresData: Date = dependencies[defaults: .standard, key: .profilePictureExpiresDate], + dependencies.dateNow.timeIntervalSince(profilePicutreExpiresData) > 0 + { + runJob() + } + else if let lastProfilePictureUpload: Date = dependencies[defaults: .standard, key: .lastProfilePictureUpload], dependencies.dateNow.timeIntervalSince(lastProfilePictureUpload) > (14 * 24 * 60 * 60) + { + runJob() + } else { // Reset the `nextRunTimestamp` value just in case the last run failed so we don't get stuck // in a loop endlessly deferring the job @@ -50,37 +92,5 @@ public enum UpdateProfilePictureJob: JobExecutor { Log.info(.cat, "Deferred as not enough time has passed since the last update") return deferred(job) } - - // Note: The user defaults flag is updated in DisplayPictureManager - let profile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies) - let displayPictureUpdate: DisplayPictureManager.Update = profile.profilePictureFileName - .map { dependencies[singleton: .displayPictureManager].loadDisplayPictureFromDisk(for: $0) } - .map { data in - let isAnimated: Bool = ImageDataManager.isAnimatedImage(data) - return .currentUserUploadImageData( - data: data, - sessionProProof: !isAnimated ? nil : - dependencies.mutate(cache: .libSession, { $0.getProProof() }) - ) - } - .defaulting(to: .none) - - Profile - .updateLocal( - displayPictureUpdate: displayPictureUpdate, - using: dependencies - ) - .subscribe(on: scheduler, using: dependencies) - .receive(on: scheduler, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .failure(let error): failure(job, error, false) - case .finished: - Log.info(.cat, "Profile successfully updated") - success(job, false) - } - } - ) } } diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index d00a89bb72..6de3d0b224 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -25,7 +25,7 @@ public extension Log.Category { // MARK: - DisplayPictureManager public class DisplayPictureManager { - public typealias UploadResult = (downloadUrl: String, fileName: String, encryptionKey: Data) + public typealias UploadResult = (downloadUrl: String, fileName: String, encryptionKey: Data, expries: Date?) public enum Update { case none @@ -303,7 +303,7 @@ public class DisplayPictureManager { } .map { [dependencies] fileUploadResponse, finalFilePath, fileName, newEncryptionKey, finalImageData -> UploadResult in let downloadUrl: String = Network.FileServer.downloadUrlString(for: fileUploadResponse.id) - let expries: Double? = fileUploadResponse.expires + let expries: Date? = fileUploadResponse.expires.map { Date(timeIntervalSince1970: $0)} /// Load the data into the `imageDataManager` (assuming we will use it elsewhere in the UI) Task(priority: .userInitiated) { @@ -313,7 +313,7 @@ public class DisplayPictureManager { } Log.verbose(.displayPictureManager, "Successfully uploaded avatar image.") - return (downloadUrl, fileName, newEncryptionKey) + return (downloadUrl, fileName, newEncryptionKey, expries) } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 45e43f3614..74eaf1576d 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -110,6 +110,7 @@ public extension Profile { using: dependencies ) + dependencies[defaults: .standard, key: .profilePictureExpiresDate] = result.expries dependencies[defaults: .standard, key: .lastProfilePictureUpload] = dependencies.dateNow Log.info(.profile, "Successfully updated user profile.") }) diff --git a/SessionSnodeKit/Models/FileUploadResponse.swift b/SessionSnodeKit/Models/FileUploadResponse.swift index e271dc8bf0..d373c686e6 100644 --- a/SessionSnodeKit/Models/FileUploadResponse.swift +++ b/SessionSnodeKit/Models/FileUploadResponse.swift @@ -2,9 +2,9 @@ public struct FileUploadResponse: Codable { public let id: String - public let expires: Double? + public let expires: TimeInterval? - public init(id: String, expires: Double?) { + public init(id: String, expires: TimeInterval?) { self.id = id self.expires = expires } @@ -22,14 +22,14 @@ extension FileUploadResponse { if let intValue: Int64 = try? container.decode(Int64.self, forKey: .id) { self = FileUploadResponse( id: "\(intValue)", - expires: try? container.decode(Double?.self, forKey: .expires) + expires: try? container.decode(TimeInterval?.self, forKey: .expires) ) return } self = FileUploadResponse( id: try container.decode(String.self, forKey: .id), - expires: try? container.decode(Double?.self, forKey: .expires) + expires: try? container.decode(TimeInterval?.self, forKey: .expires) ) } } diff --git a/SessionUIKit/Utilities/Date+Utilities.swift b/SessionUIKit/Utilities/Date+Utilities.swift index 689977cf39..f7e1a02057 100644 --- a/SessionUIKit/Utilities/Date+Utilities.swift +++ b/SessionUIKit/Utilities/Date+Utilities.swift @@ -48,6 +48,14 @@ public extension Date { var formattedForBanner: String { return Date.localTimeAndDateFormatter.string(from: self) } + + static func fromHTTPExpiresHeaders(_ expiresValue: String?) -> Date? { + guard let expiresValue else { return nil } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "EEE',' dd MMM yyyy HH:mm:ss zzz" + return formatter.date(from: expiresValue) + } } // MARK: - Formatters diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 291c78b7bc..be791639f6 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -121,6 +121,9 @@ public extension UserDefaults.DateKey { /// The date/time when the users profile picture was last uploaded to the server (used to rate-limit re-uploading) static let lastProfilePictureUpload: UserDefaults.DateKey = "lastProfilePictureUpload" + /// The date/time when the users profile picture expires on the server + static let profilePictureExpiresDate: UserDefaults.DateKey = "profilePictureExpiresDate" + /// The date/time when any open group last had a successful poll (used as a fallback date/time if the open group hasn't been polled /// this session) static let lastOpen: UserDefaults.DateKey = "lastOpen" From 259b8505c1a980374759df9bf559391b1de18258 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 25 Jul 2025 16:29:50 +1000 Subject: [PATCH 010/244] Fixed an issue with video attachment quality --- .../PhotoLibrary.swift | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index 9faf344067..10f8a01a7d 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -174,10 +174,34 @@ class PhotoCollectionContents { Future { [weak self] resolver in let options: PHVideoRequestOptions = PHVideoRequestOptions() options.isNetworkAccessAllowed = true + options.deliveryMode = .highQualityFormat - _ = self?.imageManager.requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, foo in + self?.imageManager.requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in - guard let exportSession = exportSession else { + if let error: Error = info?[PHImageErrorKey] as? Error { + return resolver(.failure(error)) + } + + guard let avAsset: AVAsset = avAsset else { + return resolver(Result.failure(PhotoLibraryError.assertionError(description: "avAsset was unexpectedly nil"))) + } + + let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: avAsset) + var bestExportPreset: String + + if compatiblePresets.contains(AVAssetExportPresetPassthrough) { + bestExportPreset = AVAssetExportPresetPassthrough + Log.debug("[PhotoLibrary] Using Passthrough export preset.") + } else { + bestExportPreset = AVAssetExportPresetHighestQuality + Log.debug("[PhotoLibrary] Passthrough not available. Falling back to HighestQuality export preset.") + } + + if (info?[PHImageCancelledKey] as? Bool) == true { + return resolver(.failure(PhotoLibraryError.assertionError(description: "Video request cancelled"))) + } + + guard let exportSession: AVAssetExportSession = AVAssetExportSession(asset: avAsset, presetName: bestExportPreset) else { resolver(Result.failure(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil"))) return } From e95bb71dfe02b467e69a3309d0447934db22fded Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 25 Jul 2025 16:36:38 +1000 Subject: [PATCH 011/244] update profile picture settings modal clear logic --- Session/Settings/SettingsViewModel.swift | 87 ++++++++++++------- .../Modals & Toast/ConfirmationModal.swift | 6 +- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 60ee75e6ea..e19311fa1f 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -545,39 +545,45 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl private func updateProfilePicture(currentFileName: String?) { let iconName: String = "profile_placeholder" // stringlint:ignore + var hasSetNewProfilePicture: Bool = false + let body: ConfirmationModal.Info.Body = .image( + source: nil, + placeholder: currentFileName + .map { try? dependencies[singleton: .displayPictureManager].filepath(for: $0) } + .map { ImageDataManager.DataSource.url(URL(fileURLWithPath: $0)) } + .defaulting(to: Lucide.image(icon: .image, size: 40).map { image in + ImageDataManager.DataSource.image( + iconName, + image + .withTintColor(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1), renderingMode: .alwaysTemplate) + .withCircularBackground(backgroundColor: #colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) + ) + }), + icon: .rightPlus, + style: .circular, + showPro: true, + accessibility: Accessibility( + identifier: "Upload", + label: "Upload" + ), + dataManager: dependencies[singleton: .imageDataManager], + onProBageTapped: { [weak self] in + self?.showSessionProCTAIfNeeded() + }, + onClick: { [weak self] onDisplayPictureSelected in + self?.onDisplayPictureSelected = { valueUpdate in + onDisplayPictureSelected(valueUpdate) + hasSetNewProfilePicture = true + } + self?.showPhotoLibraryForAvatar() + } + ) self.transitionToScreen( ConfirmationModal( info: ConfirmationModal.Info( title: "profileDisplayPictureSet".localized(), - body: .image( - source: currentFileName - .map { try? dependencies[singleton: .displayPictureManager].filepath(for: $0) } - .map { ImageDataManager.DataSource.url(URL(fileURLWithPath: $0)) }, - placeholder: Lucide.image(icon: .image, size: 40).map { image in - ImageDataManager.DataSource.image( - iconName, - image - .withTintColor(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1), renderingMode: .alwaysTemplate) - .withCircularBackground(backgroundColor: #colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) - ) - }, - icon: .rightPlus, - style: .circular, - showPro: true, - accessibility: Accessibility( - identifier: "Upload", - label: "Upload" - ), - dataManager: dependencies[singleton: .imageDataManager], - onProBageTapped: { [weak self] in - self?.showSessionProCTAIfNeeded() - }, - onClick: { [weak self] onDisplayPictureSelected in - self?.onDisplayPictureSelected = onDisplayPictureSelected - self?.showPhotoLibraryForAvatar() - } - ), + body: body, confirmTitle: "save".localized(), confirmEnabled: .afterChange { info in switch info.body { @@ -586,7 +592,12 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } }, cancelTitle: "remove".localized(), - cancelEnabled: .bool(currentFileName != nil), + cancelEnabled: (currentFileName != nil) ? .bool(true) : .afterChange { info in + switch info.body { + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) + default: return false + } + }, hasCloseButton: true, dismissOnConfirm: false, onConfirm: { [weak self, dependencies] modal in @@ -613,10 +624,20 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } }, onCancel: { [weak self] modal in - self?.updateProfile( - displayPictureUpdate: .currentUserRemove, - onComplete: { [weak modal] in modal?.close() } - ) + if hasSetNewProfilePicture { + modal.updateContent( + with: modal.info.with( + body: body, + cancelTitle: "remove".localized() + ) + ) + hasSetNewProfilePicture = false + } else { + self?.updateProfile( + displayPictureUpdate: .currentUserRemove, + onComplete: { [weak modal] in modal?.close() } + ) + } } ) ), diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index dd19b76df2..9e94ed96d6 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -688,7 +688,8 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { dataManager: dataManager, onProBageTapped: onProBadgeTapped, onClick: onClick - ) + ), + cancelTitle: "clear".localized() ) ) @@ -845,6 +846,7 @@ public extension ConfirmationModal { public func with( body: Body? = nil, + cancelTitle: String? = nil, onConfirm: ((ConfirmationModal) -> ())? = nil, onCancel: ((ConfirmationModal) -> ())? = nil, afterClosed: (() -> ())? = nil @@ -856,7 +858,7 @@ public extension ConfirmationModal { confirmTitle: self.confirmTitle, confirmStyle: self.confirmStyle, confirmEnabled: self.confirmEnabled, - cancelTitle: self.cancelTitle, + cancelTitle: (cancelTitle ?? self.cancelTitle), cancelStyle: self.cancelStyle, cancelEnabled: self.cancelEnabled, hasCloseButton: self.hasCloseButton, From afb7b65794c5f472c97e7e302c58789b35589a12 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 30 Jul 2025 15:13:48 +1000 Subject: [PATCH 012/244] add profile update from libsession --- Session.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- SessionMessagingKit/Configuration.swift | 2 +- .../LibSession/Config Handling/LibSession+Contacts.swift | 4 ++-- .../LibSession/Config Handling/LibSession+GroupMembers.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 46ba49fdd1..742f90920e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -10305,7 +10305,7 @@ repositoryURL = "https://github.com/session-foundation/libsession-util-spm"; requirement = { kind = exactVersion; - version = 1.5.1; + version = 1.5.2; }; }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 40c1f21016..586ff2e952 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/libsession-util-spm", "state" : { - "revision" : "ba7d5f08e4eb71a2efe744df2ad677d8c180c6bb", - "version" : "1.5.1" + "revision" : "c224b53ae973d5cc707def1c11a20e7104ffa028", + "version" : "1.5.2" } }, { diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 3e46ba1af7..d75ec471d5 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -43,7 +43,7 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _023_GroupsExpiredFlag.self, _024_FixBustedInteractionVariant.self, _025_DropLegacyClosedGroupKeyPairTable.self - ], + ], // Add Session Pro [ _026_AddProMessageFlag.self, _027_AddProfileProProof.self diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index f00fe98af4..3d6c9288cd 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -810,11 +810,11 @@ private extension LibSession { let profileResult: Profile = Profile( id: contactId, name: contact.get(\.name), - lastNameUpdate: (TimeInterval(serverTimestampMs) / 1000), + lastNameUpdate: TimeInterval(contact.profile_updated), nickname: contact.get(\.nickname, nullIfEmpty: true), profilePictureUrl: profilePictureUrl, profileEncryptionKey: (profilePictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - lastProfilePictureUpdate: (TimeInterval(serverTimestampMs) / 1000) + lastProfilePictureUpdate: TimeInterval(contact.profile_updated) ) let configResult: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration( threadId: contactId, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 556fc4f84f..cd64a7f73d 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -513,13 +513,13 @@ internal extension LibSession { Profile( id: member.get(\.session_id), name: member.get(\.name), - lastNameUpdate: TimeInterval(Double(serverTimestampMs) / 1000), + lastNameUpdate: TimeInterval(member.profile_updated), nickname: nil, profilePictureUrl: member.get(\.profile_pic.url, nullIfEmpty: true), profileEncryptionKey: (member.get(\.profile_pic.url, nullIfEmpty: true) == nil ? nil : member.get(\.profile_pic.key) ), - lastProfilePictureUpdate: TimeInterval(Double(serverTimestampMs) / 1000), + lastProfilePictureUpdate: TimeInterval(member.profile_updated), lastBlocksCommunityMessageRequests: nil ) ) From 5b2fe548b440c01645f52f322f1872203cf215a9 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 30 Jul 2025 16:10:32 +1000 Subject: [PATCH 013/244] update setting profile picture modal --- Session/Settings/SettingsViewModel.swift | 2 +- .../Modals & Toast/ConfirmationModal.swift | 4 ++-- .../Components/ProfilePictureView.swift | 19 ++++++++++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index e19311fa1f..f3feb2a026 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -559,7 +559,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .withCircularBackground(backgroundColor: #colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) ) }), - icon: .rightPlus, + icon: (currentFileName != nil ? .pencil : .rightPlus), style: .circular, showPro: true, accessibility: Accessibility( diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 9e94ed96d6..7ecebb2b1b 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -673,7 +673,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { @objc private func imageViewTapped() { internalOnBodyTap?({ [weak self, info = self.info] valueUpdate in switch (valueUpdate, info.body) { - case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, let icon, let style, let showPro, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): + case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, _, let style, let showPro, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): self?.updateContent( with: info.with( body: .image( @@ -681,7 +681,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { ImageDataManager.DataSource.data(updatedIdentifier, $0) }, placeholder: placeholder, - icon: icon, + icon: (updatedData == nil ? .rightPlus : .pencil), style: style, showPro: showPro, accessibility: accessibility, diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index de4b91df7f..f12c6b0312 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -2,6 +2,7 @@ import UIKit import Combine +import Lucide public final class ProfilePictureView: UIView { public struct Info { @@ -56,7 +57,7 @@ public final class ProfilePictureView: UIView { switch self { case .navigation, .message: return 26 case .list: return 46 - case .hero: return 80 + case .hero: return 90 } } @@ -64,7 +65,7 @@ public final class ProfilePictureView: UIView { switch self { case .navigation, .message: return 18 // Shouldn't be used case .list: return 32 - case .hero: return 80 + case .hero: return 90 } } @@ -82,6 +83,7 @@ public final class ProfilePictureView: UIView { case crown case rightPlus case letter(Character, Bool) + case pencil func iconVerticalInset(for size: Size) -> CGFloat { switch (self, size) { @@ -97,7 +99,7 @@ public final class ProfilePictureView: UIView { var isLeadingAligned: Bool { switch self { case .none, .crown, .letter: return true - case .rightPlus: return false + case .rightPlus, .pencil: return false } } } @@ -451,6 +453,7 @@ public final class ProfilePictureView: UIView { case .crown: imageView.image = UIImage(systemName: "crown.fill") + imageView.contentMode = .scaleAspectFit imageView.themeTintColor = .dynamicForPrimary( .green, use: .profileIcon_greenPrimaryColor, @@ -462,6 +465,7 @@ public final class ProfilePictureView: UIView { case .rightPlus: imageView.image = UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)) + imageView.contentMode = .scaleAspectFit imageView.themeTintColor = .black backgroundView.themeBackgroundColor = .primary imageView.isHidden = false @@ -472,6 +476,15 @@ public final class ProfilePictureView: UIView { backgroundView.themeBackgroundColor = (dangerMode ? .danger : .textPrimary) label.isHidden = false label.text = "\(character)" + + case .pencil: + imageView.image = Lucide.image(icon: .pencil, size: 14)?.withRenderingMode(.alwaysTemplate) + imageView.contentMode = .center + imageView.themeTintColor = .black + backgroundView.themeBackgroundColor = .primary + imageView.isHidden = false + label.isHidden = true + } } From db0754760087d4348bc367a10310af4138ccec53 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 1 Aug 2025 13:17:21 +1000 Subject: [PATCH 014/244] fix unit test --- .../Sending & Receiving/MessageReceiverGroupsSpec.swift | 2 +- .../Sending & Receiving/MessageSenderGroupsSpec.swift | 4 ++-- .../Sending & Receiving/Pollers/CommunityPollerSpec.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 7a3bcd9aed..e76542712a 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -72,7 +72,7 @@ class MessageReceiverGroupsSpec: QuickSpec { initialSetup: { network in network .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) network .when { $0.getSwarm(for: .any) } .thenReturn([ diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index e12d8bb998..9ee95269e0 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -653,7 +653,7 @@ class MessageSenderGroupsSpec: QuickSpec { it("uploads the image") { mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) MessageSender .createGroup( @@ -692,7 +692,7 @@ class MessageSenderGroupsSpec: QuickSpec { mockLibSessionCache.when { $0.isEmpty }.thenReturn(true) mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) MessageSender .createGroup( diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index ebbbaae296..5fa9303253 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -63,7 +63,7 @@ class CommunityPollerSpec: QuickSpec { network .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn( - MockNetwork.response(with: FileUploadResponse(id: "1")) + MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil)) .delay(for: .seconds(10), scheduler: DispatchQueue.main) .eraseToAnyPublisher() ) From b741393168cee40db31acdfecdb212f0f4bf3834 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 1 Aug 2025 14:09:52 +1000 Subject: [PATCH 015/244] minor fix --- Session/Settings/SettingsViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index f3feb2a026..dd6d9218ca 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -561,7 +561,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl }), icon: (currentFileName != nil ? .pencil : .rightPlus), style: .circular, - showPro: true, + showPro: dependencies[feature: .sessionProEnabled], accessibility: Accessibility( identifier: "Upload", label: "Upload" From 51c04067cf32c797148c5daa5cb591c2922e20d0 Mon Sep 17 00:00:00 2001 From: Aerilym <5667907+Aerilym@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:29:17 +0000 Subject: [PATCH 016/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 569 +++--------------- 1 file changed, 86 insertions(+), 483 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 0a4a817c36..1ed97ebd47 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -172552,7 +172552,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your Display Name is visible to users, groups and communities you interact with." + "value" : "Your Display Name is visible to users, groups, and communities you interact with." } }, "eo" : { @@ -186860,7 +186860,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "You've been using {app_name} for a little
    while, how’s it going? We’d really
    appreciate hearing your thoughts." + "value" : "You've been using {app_name} for a little while, how’s it going? We’d really appreciate hearing your thoughts." } } } @@ -193418,7 +193418,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Sorry to hear your {app_name} experience
    hasn’t been ideal. We'd be grateful if you
    could take a moment to share your
    thoughts in a brief survey" + "value" : "Sorry to hear your {app_name} experience hasn’t been ideal. We'd be grateful if you could take a moment to share your thoughts in a brief survey" } } } @@ -291894,12 +291894,6 @@ "value" : "Trieu un sobrenom per a {name}. Això us apareixerà a les vostres converses individuals i de grup." } }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyberte přezdívku pro {name}. Zobrazí se ve vašich konverzacích jeden na jednoho a ve skupinových." - } - }, "cy" : { "stringUnit" : { "state" : "translated", @@ -332144,478 +332138,10 @@ "passwordErrorLength" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wagwoord moet tussen 6 en 64 karakters lank wees" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "كلمة المرور يجب ان تكون بين 6 و 64 عنصر" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parol 6-64 simvol uzunluğunda olmalıdır" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ 6 تا 64 حرفء درمیان بوت" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль павінен мець даўжыню ў межах ад 6 да 64 сімвалаў" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паролата трябва да е между 6 и 64 символа" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "পাসওয়ার্ড ৬ থেকে ৬৪ অক্ষরের মধ্যে হতে হবে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La contrasenya ha de ser d'entre 6 i 64 caràcters" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Heslo musí mít od 6 do 64 znaků" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rhaid i gyfrinair fod rhwng 6 a 64 nod o hyd" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adgangskoden skal være mellem 6 til 64 tegn" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Passwort muss zwischen 6 und 64 Zeichen lang sein" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασης πρέπει να αποτελείται από 6 έως 64 χαρακτήρες" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Password must be between 6 and 64 characters long" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesas, ke pasvorto estu inter 6 kaj 64 longe" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "La contraseña debe tener entre 6 y 64 caracteres" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "La contraseña debe tener entre 6 y 64 caracteres de longitud" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parool peab olema 6 kuni 64 tähemärki pikk" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pasahitzak 6 eta 64 karaktere bitartekoa izan behar du" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "طول رمز عبور باید بین ۶ تا ۶۴ کاراکتر باشد" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanan tulee olla 6-64 merkkiä pitkä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dapat may haba na 6 hanggang 20 titik ang 'yong password" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le mot de passe doit avoir une longueur comprise entre 6 et 64 caractères" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O contrasinal debe ter entre 6 e 64 caracteres" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kalmar sirri dole ta kasance tsakanin haruffa 6 zuwa 64" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמא חייבת להיות באורך של בין 6 ל-64 תווים" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड 6 से 64 वर्णों के बीच होना चाहिए" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 znakova" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszónak minimum 6 és maximum 64 karakter hosszúságúnak kell lennie" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Գաղտնաբառը պետք է լինի 6-ից 64 նիշ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Panjang kata sandi anda harus diantara 6 dan 64 karakter" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La password deve essere lunga tra i 6 e i 64 caratteri" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードの長さを6文字から64文字にしてください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლი უნდა იყოს 6-64 სიმბოლოს სიგრძის" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យ​សម្ងាត់​ត្រូវ​តែ​មាន​ចន្លោះ​ពី 6 ទៅ 64 តួអក្សរ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಪಾಸ್ವರ್ಡ್ 6.೦ರಿಂದ 64 ಅಕ್ಷರಗಳಷ್ಟು ಉದ್ದವು ಇರಬೇಕು" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호는 반드시 6자에서 12자 사이어야 합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تێپەڕبووی تێپەڕاندەکانی تێپەڕەبە دووبارە بکەوە دەگل ناچێ." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Divê şîfre di navbera dirêjiya 6 û 64 karakteran de be" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akasumulizo kalina kubeera wakati wa ennyukuta 6 ne 64." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password must be between 6 and 64 characters long" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolei jābūt no 6 līdz 64 rakstzīmēm garai" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лозинката мора да содржи помеѓу 6 и 64 карактери" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нууц үг 6-аас 64 тэмдэгттэй байх ёстой" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan mestilah antara 6 hingga 64 aksara panjang" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စကားဝှက်သည်စာလုံးများအကြား ၆ မှ ၆၄ ရှိရမည်" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड ६ देखि ६४ वर्णको बीचमा हुनुपर्छ" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wachtwoord moet tussen de 6 en 64 tekens lang zijn" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chinsinsi chiyenera kutalika pakati pa zilembo 6 ndi 64" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਪਾਸਵਰਡ 6 ਤੋਂ 64 ਅੱਖਰ ਲੰਮਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hasło musi zawierać od 6 do 64 znaków" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورډ باید د 6 او 64 تورو ترمنځ وي" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "A senha deve ter entre 6 e 64 caracteres" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A palavra-passe deve ter entre 6 e 64 carateres" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parola trebuie să aibă între 6 și 64 de caractere lungime" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пароль должен содержать от 6 до 64 символов" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 znakova" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මුරපදය අක්ෂර 6 සහ 64 අතර දිග විය යුතුය" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Heslo musí mať dĺžku 6 až 64 znakov" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geslo mora biti dolgo med 6 in 64 znakov" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi duhet të jetë midis 6 dhe 64 karaktere të gjata" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лозинка мора имати између 6 и 64 карактера" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 karaktera" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lösenordet måste vara mellan 6 och 64 tecken långt" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nywila lazima iwe kati ya herufi 6 na 64" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கடவுச்சொல் 6 முதல் 64 எழுத்துக்களினிடையே இருக்க வேண்டும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "పాస్‌వర్డ్ 6 మరియు 64 అక్షరాల మధ్య ఉండాలి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านต้องมีความยาวตั้งแต่ 6 ถึง 64 ตัวอักษร" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolanız 6 ila 64 karakter uzunluğu aralığında olmalıdır" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пароль має бути довжиною від 6 до 64 символів" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ 6 اور 64 حروف کے درمیان ہونا چاہیے" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sizning parolingiz uzunligi 6 va 64 belgidan iborat bo'lishi mumkin" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu phải dài từ 6 đến 64 ký tự" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi mayibe phakathi kwe-6 ne-64 iimpawu ngokubude" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "密码长度必须在6到64个字符之间" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "密碼必須介於6到64個字元之間。" + "value" : "Password must be between {min} and {max} characters long" } } } @@ -350826,6 +350352,17 @@ } } }, + "proAnimatedDisplayPictureFeature" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animated Display Picture" + } + } + } + }, "proAnimatedDisplayPictureModalDescription" : { "extractionState" : "manual", "localizations" : { @@ -350872,6 +350409,17 @@ } } }, + "proBadge" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Badge" + } + } + } + }, "proCallToActionLongerMessages" : { "extractionState" : "manual", "localizations" : { @@ -353936,6 +353484,61 @@ } } }, + "proGroupActivated" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Group Activated" + } + } + } + }, + "proGroupActivatedDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This group has expanded capacity! It can support up to 300 members because a group admin has" + } + } + } + }, + "proIncreasedAttachmentSizeFeature" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increased Attachment Size" + } + } + } + }, + "proIncreasedMessageLengthFeature" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increased Message Length" + } + } + } + }, + "proMessageInfoFeatures" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This message used the following Session Pro features:" + } + } + } + }, "promote" : { "extractionState" : "manual", "localizations" : { @@ -355926,7 +355529,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Want to use {app_name} to its fullest potential? Upgrade to {app_pro} to gain access to exclusive perks and features" + "value" : "Want to get more out of {app_name}? Upgrade to {app_pro} for a more powerful messaging experience." } } } @@ -359791,7 +359394,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "We're glad you're enjoying {app_name}, if
    you have a moment, rating us in the
    {storevariant} helps others discover
    private, secure messaging!" + "value" : "We're glad you're enjoying {app_name}, if you have a moment, rating us in the {storevariant} helps others discover private, secure messaging!" } } } @@ -375489,7 +375092,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "It looks like you've already reviewed
    {app_name} recently, thanks for your
    feedback!" + "value" : "It looks like you've already reviewed {app_name} recently, thanks for your feedback!" } } } @@ -401994,7 +401597,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "The Account ID of {name} is visible
    based on your previous interactions" + "value" : "The Account ID of {name} is visible based on your previous interactions" } } } @@ -402005,7 +401608,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Blinded IDs are used in communities
    to reduce spam and increase privacy" + "value" : "Blinded IDs are used in communities to reduce spam and increase privacy" } } } From a98c89afa4468a5361ca192586d9c1dd1e480fe1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 4 Aug 2025 16:21:59 +1000 Subject: [PATCH 017/244] Reworked the ImageDataManager to better handle large animated images --- Session.xcodeproj/project.pbxproj | 4 - .../Components/SessionImageView.swift | 72 ++- .../SwiftUI/SessionAsyncImage.swift | 96 +++- SessionUIKit/Types/ImageDataManager.swift | 507 +++++++++++++----- SessionUIKit/Utilities/Data+Utilities.swift | 79 --- 5 files changed, 490 insertions(+), 268 deletions(-) delete mode 100644 SessionUIKit/Utilities/Data+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 742f90920e..a4d9710ba4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -554,7 +554,6 @@ FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E32C35134B004D8A6C /* Data+Utilities.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; FD2272EC2C352155004D8A6C /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272EB2C352155004D8A6C /* Feature.swift */; }; @@ -1876,7 +1875,6 @@ FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Theme.swift"; sourceTree = ""; }; - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIKImageFormat.swift; sourceTree = ""; }; FD2272E92C351CA7004D8A6C /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; FD2272EB2C352155004D8A6C /* Feature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = ""; }; @@ -3241,7 +3239,6 @@ isa = PBXGroup; children = ( 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */, - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */, FD848B9728422F1A000E298B /* Date+Utilities.swift */, FD8A5B282DC060DD004C689B /* Double+Utilities.swift */, 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */, @@ -5967,7 +5964,6 @@ FD8A5B252DC05B16004C689B /* Number+Utilities.swift in Sources */, FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */, FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */, C331FFE72558FB0000070591 /* SNTextField.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index ebdf802c91..d155b262b7 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -6,11 +6,12 @@ import ImageIO public class SessionImageView: UIImageView { private var dataManager: ImageDataManagerType? - private var currentLoadIdentifier: String? + internal /*private*/ var currentLoadIdentifier: String? private var imageLoadTask: Task? + private var streamConsumptionTask: Task? private var displayLink: CADisplayLink? - private var animationFrames: [UIImage]? + private var animationFrames: [UIImage?]? private var animationFrameDurations: [TimeInterval]? public private(set) var currentFrameIndex: Int = 0 public private(set) var accumulatedTime: TimeInterval = 0 @@ -34,6 +35,8 @@ public class SessionImageView: UIImageView { public var shouldAnimateImage: Bool { didSet { + guard oldValue != shouldAnimateImage else { return } + if shouldAnimateImage { startAnimationLoop() } else { @@ -92,6 +95,7 @@ public class SessionImageView: UIImageView { deinit { imageLoadTask?.cancel() + streamConsumptionTask?.cancel() /// The documentation for `CADisplayLink` states: /// ``` @@ -142,7 +146,7 @@ public class SessionImageView: UIImageView { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it - if let frames: [UIImage] = animationFrames, !frames.isEmpty, !isAnimating() { + if let frames: [UIImage?] = animationFrames, !frames.isEmpty, !isAnimating() { startAnimationLoop() } return @@ -155,9 +159,9 @@ public class SessionImageView: UIImageView { switch source { case .image(_, .some(let image)): imageSizeMetadata = image.size - return handleLoadedImageData( - ImageDataManager.ProcessedImageData(type: .staticImage(image)) - ) + handleLoadedImageData(ImageDataManager.ProcessedImageData(type: .staticImage(image))) + onComplete?(true) + return default: break } @@ -190,7 +194,7 @@ public class SessionImageView: UIImageView { public func startAnimationLoop() { guard shouldAnimateImage, - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations: [TimeInterval] = animationFrameDurations, frames.count > 1, frames.count == durations.count @@ -207,6 +211,7 @@ public class SessionImageView: UIImageView { self.image = frames[0] } + stopAnimationLoop() /// Make sude we don't unintentionally create extra `CADisplayLink` instances currentFrameIndex = 0 accumulatedTime = 0 @@ -222,7 +227,7 @@ public class SessionImageView: UIImageView { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, frames.count == durations.count, @@ -267,6 +272,7 @@ public class SessionImageView: UIImageView { @MainActor private func resetState(identifier: String?) { stopAnimationLoop() + streamConsumptionTask?.cancel() self.image = nil currentLoadIdentifier = identifier @@ -303,40 +309,62 @@ public class SessionImageView: UIImageView { case 1...: startAnimationLoop() default: stopAnimationLoop() /// Treat as a static image } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream): + self.image = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + + guard durations.count > 1 else { + stopAnimationLoop() + return + } + + streamConsumptionTask = Task { @MainActor in + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { continue } + + startAnimationLoop() + } + } + } } } @objc private func updateFrame(displayLink: CADisplayLink) { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count else { return stopAnimationLoop() } accumulatedTime += displayLink.duration - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] /// It's possible for a long `CADisplayLink` tick to take longeer than a single frame so try to handle those cases while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) + + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift index 306fcc14e9..e0bdc69f32 100644 --- a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift +++ b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift @@ -6,7 +6,7 @@ import NaturalLanguage public struct SessionAsyncImage: View { @State private var loadedImage: UIImage? = nil - @State private var animationFrames: [UIImage]? + @State private var animationFrames: [UIImage?]? @State private var animationFrameDurations: [TimeInterval]? @State private var isAnimating: Bool = false @@ -16,6 +16,7 @@ public struct SessionAsyncImage: View { private let source: ImageDataManager.DataSource private let dataManager: ImageDataManagerType + private let shouldAnimateImage: Bool private let content: (Image) -> Content private let placeholder: () -> Placeholder @@ -23,11 +24,13 @@ public struct SessionAsyncImage: View { public init( source: ImageDataManager.DataSource, dataManager: ImageDataManagerType, + shouldAnimateImage: Bool = true, @ViewBuilder content: @escaping (Image) -> Content, @ViewBuilder placeholder: @escaping () -> Placeholder ) { self.source = source self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage self.content = content self.placeholder = placeholder } @@ -55,23 +58,22 @@ public struct SessionAsyncImage: View { .task(id: source.identifier) { await loadAndProcessData() } + .onChange(of: shouldAnimateImage) { newValue in + if let frames = animationFrames, !frames.isEmpty { + isAnimating = newValue + } + } } // MARK: - Internal Functions private func loadAndProcessData() async { + /// Reset the state before loading new data + await MainActor.run { resetAnimationState() } + let processedData = await dataManager.load(source) - /// Reset the state before loading new data - await MainActor.run { - self.loadedImage = nil - self.animationFrames = nil - self.animationFrameDurations = nil - self.isAnimating = false - self.currentFrameIndex = 0 - self.accumulatedTime = 0.0 - self.lastFrameDate = .now - } + guard !Task.isCancelled else { return } switch processedData?.type { case .staticImage(let image): @@ -84,13 +86,43 @@ public struct SessionAsyncImage: View { self.animationFrames = frames self.animationFrameDurations = durations self.loadedImage = frames.first - self.isAnimating = true /// Activate the `TimelineView` + + if self.shouldAnimateImage { + self.isAnimating = true /// Activate the `TimelineView` + } + } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream) where durations.count > 1: + await MainActor.run { + self.loadedImage = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + } + + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + await MainActor.run { + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { return } + + self.isAnimating = true + } + } } case .animatedImage(let frames, _): await MainActor.run { self.loadedImage = frames.first } + + case .bufferedAnimatedImage(let firstFrame, _, _): + await MainActor.run { + self.loadedImage = firstFrame + } default: await MainActor.run { @@ -99,41 +131,51 @@ public struct SessionAsyncImage: View { } } + @MainActor + private func resetAnimationState() { + self.loadedImage = nil + self.animationFrames = nil + self.animationFrameDurations = nil + self.isAnimating = false + self.currentFrameIndex = 0 + self.accumulatedTime = 0.0 + self.lastFrameDate = .now + } + private func updateAnimationFrame(at date: Date) { guard isAnimating, - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count, let lastDate = lastFrameDate - else { return } + else { + isAnimating = false + return + } /// Calculate elapsed time since the last frame let elapsed: TimeInterval = date.timeIntervalSince(lastDate) self.lastFrameDate = date accumulatedTime += elapsed - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] // Advance frames if the accumulated time exceeds the current frame's duration while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 8fda02a7cd..9ac91588e6 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -11,6 +11,10 @@ public actor ImageDataManager: ImageDataManagerType { attributes: .concurrent ) + /// Max memory size for a decoded animation to be considered "small" enough to be fully cached + private static let decodedAnimationCacheLimit: Int = 20 * 1024 * 1024 // 20 M + private static let maxAnimatedImageDownscaleDimention: CGFloat = 4096 + /// `NSCache` has more nuanced memory management systems than just listening for `didReceiveMemoryWarningNotification` /// and can clear out values gradually, it can also remove items based on their "cost" so is better suited than our custom `LRUCache` private let cache: NSCache = { @@ -47,8 +51,8 @@ public actor ImageDataManager: ImageDataManagerType { /// Wait for the result then cache and return it let processedData: ProcessedImageData? = await newTask.value - if let data: ProcessedImageData = processedData { - self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCost) + if let data: ProcessedImageData = processedData, data.isCacheable { + self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCacheCost) } self.activeLoadTasks[identifier] = nil @@ -95,8 +99,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `videoUrl` values since it requires thumbnail generation case .videoUrl(let url, let mimeType, let sourceFilename, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -120,12 +131,15 @@ public actor ImageDataManager: ImageDataManagerType { let generator: AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true - guard let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil) else { - return nil - } + guard + let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { return nil } - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -144,8 +158,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `urlThumbnail` generation case .urlThumbnail(let url, let size, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -156,21 +177,23 @@ public actor ImageDataManager: ImageDataManagerType { /// Otherwise we need to generate a new one let maxDimensionInPixels: CGFloat = await size.pixelDimension() let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false, kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels ] guard - let format: SUIKImageFormat = dataSource.dataForGuessingImageFormat?.suiKitGuessedImageFormat, - format != .unknown, - let imageSource: CGImageSource = CGImageSourceCreateWithURL(url as CFURL, nil), - let thumbnail: CGImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) + let source: CGImageSource = dataSource.createImageSource(), + let cgImage: CGImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) else { return nil } - let image: UIImage = UIImage(cgImage: thumbnail) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - /// Since we generated a new thumbnail we should save it to disk saveThumbnailToDisk( image: decodedImage, @@ -185,11 +208,21 @@ public actor ImageDataManager: ImageDataManagerType { case .closureThumbnail(_, _, let imageRetrier): guard let image: UIImage = await imageRetrier() else { return nil } + guard + let cgImage: CGImage = image.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { + return ProcessedImageData( + type: .staticImage(image) + ) + } /// Since there is likely custom (external) logic used to retrieve this thumbnail we don't save it to disk as there /// is no way to know if it _should_ change between generations/launches or not - let decodedImage: UIImage = (image.predecodedImage() ?? image) - return ProcessedImageData( type: .staticImage(decodedImage) ) @@ -197,7 +230,19 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `placeholderIcon` generation case .placeholderIcon(let seed, let text, let size): let image: UIImage = PlaceholderIcon.generate(seed: seed, text: text, size: size) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + + guard + let cgImage: CGImage = image.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { + return ProcessedImageData( + type: .staticImage(image) + ) + } return ProcessedImageData( type: .staticImage(decodedImage) @@ -209,13 +254,17 @@ public actor ImageDataManager: ImageDataManagerType { /// Otherwise load the data as either a static or animated image (do quick validation checks here - other checks /// require loading the image source anyway so don't bother to include them) guard - let imageData: Data = dataSource.imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown, - (imageFormat != .gif || imageData.suiKitHasValidGifSize), - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0 + let source: CGImageSource = dataSource.createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidSize else { return nil } - + + /// Get the umber of frames in the image let count: Int = CGImageSourceGetCount(source) switch count { @@ -224,66 +273,197 @@ public actor ImageDataManager: ImageDataManagerType { /// Static image case 1: - guard let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { - return nil - } - /// Extract image orientation if present var orientation: UIImage.Orientation = .up if - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let rawCgOrientation: UInt32 = imageProperties[kCGImagePropertyOrientation] as? UInt32, + let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation as String] as? UInt32, let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) { orientation = UIImage.Orientation(cgOrientation) } - let image: UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: orientation) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + /// Try to decode the image direct from the `CGImage` + let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] + + guard + let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary), + let decodingContext = createDecodingContext(width: cgImage.width, height: cgImage.height), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext), + let decodedCgImage: CGImage = decodedImage.cgImage + else { return nil } + + let finalImage: UIImage = UIImage(cgImage: decodedCgImage, scale: 1, orientation: orientation) return ProcessedImageData( - type: .staticImage(decodedImage) + type: .staticImage(finalImage) ) /// Animated Image default: - var framesArray: [UIImage] = [] - var durationsArray: [TimeInterval] = [] - - for i in 0.. decodedAnimationCacheLimit else { + var frames: [UIImage] = [decodedFirstFrameImage] - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - let duration: TimeInterval = ImageDataManager.getFrameDuration(from: source, at: i) + for i in 1.. = AsyncStream { continuation in + let task = Task.detached(priority: .userInitiated) { + var (frameIndexesToBuffer, probeFrames) = await self.calculateHeuristicBuffer( + startIndex: 1, /// We have already decoded the first frame so skip it + source: source, + durations: durations, + using: decodingContext + ) + let lastBufferedFrameIndex: Int = ( + frameIndexesToBuffer.max() ?? + probeFrames.count + ) + + /// Immediately yield the frames decoded when calculating the buffer size + for (index, frame) in probeFrames.enumerated() { + if Task.isCancelled { break } + + /// We `+ 1` because the first frame is always manually assigned + continuation.yield(.frame(index: index + 1, frame: frame)) + } + + /// Clear out the `proveFrames` array so we don't use the extra memory + probeFrames.removeAll(keepingCapacity: false) + + /// Load in any additional buffer frames needed + for i in frameIndexesToBuffer { + guard !Task.isCancelled else { + continuation.finish() + return + } + + var decodedFrame: UIImage? + autoreleasepool { + decodedFrame = predecode( + cgImage: CGImageSourceCreateImageAtIndex(source, i, nil), + using: decodingContext + ) + } + + if let frame: UIImage = decodedFrame { + continuation.yield(.frame(index: i, frame: frame)) + } + } + + /// Now that we have buffered enough frames we can start the animation + if !Task.isCancelled { + continuation.yield(.readyToPlay) + } + + /// Start loading the remaining frames (`+ 1` as we want to start from the index after the last buffered index) + if lastBufferedFrameIndex < count { + for i in (lastBufferedFrameIndex + 1).. CGContext? { + guard width > 0 && height > 0 else { return nil } + + return CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: (width * 4), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: (CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) + ) + } + + private static func predecode(cgImage: CGImage?, using context: CGContext) -> UIImage? { + guard let cgImage: CGImage = cgImage else { return nil } + + let width: Int = context.width + let height: Int = context.height + context.clear(CGRect(x: 0, y: 0, width: width, height: height)) + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return context.makeImage().map { UIImage(cgImage: $0) } + } + + private static func getFrameDurations(from imageSource: CGImageSource, count: Int) -> [TimeInterval] { + return (0.. TimeInterval { guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any] else { return 0.1 @@ -328,6 +508,50 @@ public actor ImageDataManager: ImageDataManagerType { return 0.1 /// Fallback } + private static func calculateHeuristicBuffer( + startIndex: Int, + source: CGImageSource, + durations: [TimeInterval], + using context: CGContext + ) async -> (frameIndexesToBuffer: [Int], probeFrames: [UIImage]) { + let probeFrameCount: Int = 5 /// Number of frames to decode in order to calculate the approx. time to load each frame + let safetyMargin: Double = 2 /// Number of extra frames to be buffered just in case + + guard durations.count > (startIndex + probeFrameCount) else { + return (Array(startIndex.. 0.001 else { return ([], probeFrames) } + + let decodeToDisplayRatio: Double = (avgDecodeTime / avgDisplayDuration) + let calculatedBufferSize: Double = ceil(decodeToDisplayRatio) + safetyMargin + let finalFramesToBuffer: Int = Int(max(Double(probeFrameCount), min(calculatedBufferSize, 60.0))) + + guard finalFramesToBuffer > (startIndex + probeFrameCount) else { return ([], probeFrames) } + + return (Array((startIndex + probeFrameCount).. CGImageSource? { + let options = [kCGImageSourceShouldCache: false] as CFDictionary + + switch self { + case .url(let url, _): return CGImageSourceCreateWithURL(url as CFURL, options) + case .data(_, let data, _): return CGImageSourceCreateWithData(data as CFData, options) + case .closure(_, let dataRetriever): + guard let data = dataRetriever() else { return nil } + + return CGImageSourceCreateWithData(data as CFData, options) + + default: return nil + } + } + + public static func == (lhs: DataSource, rhs: DataSource) -> Bool { switch (lhs, rhs) { - case (.url(let lhsUrl), .url(let rhsUrl)): return (lhsUrl == rhsUrl) - case (.data(let lhsIdentifier, let lhsData), .data(let rhsIdentifier, let rhsData)): + case (.url(let lhsUrl, let lhsSize), .url(let rhsUrl, let rhsSize)): + return ( + lhsUrl == rhsUrl && + lhsSize == rhsSize + ) + + case (.data(let lhsIdentifier, let lhsData, let lhsSize), .data(let rhsIdentifier, let rhsData, let rhsSize)): return ( lhsIdentifier == rhsIdentifier && - lhsData == rhsData + lhsData == rhsData && + lhsSize == rhsSize ) + case (.image(let lhsIdentifier, _), .image(let rhsIdentifier, _)): /// `UIImage` is not _really_ equatable so we need to use a separate identifier to use instead return (lhsIdentifier == rhsIdentifier) @@ -465,10 +721,14 @@ public extension ImageDataManager { public func hash(into hasher: inout Hasher) { switch self { - case .url(let url): url.hash(into: &hasher) - case .data(let identifier, let data): + case .url(let url, let size): + url.hash(into: &hasher) + size?.hash(into: &hasher) + + case .data(let identifier, let data, let size): identifier.hash(into: &hasher) data.hash(into: &hasher) + size?.hash(into: &hasher) case .image(let identifier, _): /// `UIImage` is not actually hashable so we need to provide a separate identifier to use instead @@ -504,7 +764,17 @@ public extension ImageDataManager { public extension ImageDataManager { enum DataType { case staticImage(UIImage) - case animatedImage(frames: [UIImage], frameDurations: [TimeInterval]) + case animatedImage(frames: [UIImage], durations: [TimeInterval]) + case bufferedAnimatedImage( + firstFrame: UIImage, + durations: [TimeInterval], + bufferedFrameStream: AsyncStream + ) + } + + enum BufferedFrameStreamEvent { + case frame(index: Int, frame: UIImage) + case readyToPlay } } @@ -526,7 +796,14 @@ public extension ImageDataManager { class ProcessedImageData: @unchecked Sendable { public let type: DataType public let frameCount: Int - public let estimatedCost: Int + public let estimatedCacheCost: Int + + public var isCacheable: Bool { + switch type { + case .staticImage, .animatedImage: return true + case .bufferedAnimatedImage: return false + } + } init(type: DataType) { self.type = type @@ -534,11 +811,15 @@ public extension ImageDataManager { switch type { case .staticImage(let image): frameCount = 1 - estimatedCost = ProcessedImageData.calculateCost(for: [image]) + estimatedCacheCost = ProcessedImageData.calculateCost(for: [image]) case .animatedImage(let frames, _): frameCount = frames.count - estimatedCost = ProcessedImageData.calculateCost(for: frames) + estimatedCacheCost = ProcessedImageData.calculateCost(for: frames) + + case .bufferedAnimatedImage(_, let durations, _): + frameCount = durations.count + estimatedCacheCost = 0 } } @@ -560,44 +841,6 @@ public extension ImageDataManager { /// Needed for `actor` usage (ie. assume safe access) extension UIImage: @unchecked Sendable {} -extension UIImage { - /// When loading an image the OS doesn't immediately decompress the entire image in order to be efficient but since that - /// decompressing could happen on the main thread it would defeat the purpose of our background processing potentially - /// re-introducing the jitteriness this class was designed to resolve, so instead this function will decompress the image directly - func predecodedImage() -> UIImage? { - guard let cgImage = self.cgImage else { return self } - - let width: Int = cgImage.width - let height: Int = cgImage.height - - /// Avoid `CGBitmapContextCreate` error with 0 dimension - guard width > 0 && height > 0 else { return self } - - let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo: UInt32 = ( - CGImageAlphaInfo.premultipliedFirst.rawValue | - CGBitmapInfo.byteOrder32Little.rawValue - ) - - guard - let context: CGContext = CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: (width * 4), - space: colorSpace, - bitmapInfo: bitmapInfo - ) - else { return self } - - context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) - guard let drawnImage: CGImage = context.makeImage() else { return self } - - return UIImage(cgImage: drawnImage, scale: self.scale, orientation: self.imageOrientation) - } -} - extension AVAsset { var isValidVideo: Bool { var maxTrackSize = CGSize.zero @@ -618,6 +861,9 @@ extension AVAsset { } public extension ImageDataManager.DataSource { + /// We need to ensure that the image size is "reasonable", otherwise trying to load it could cause out-of-memory crashes + fileprivate static let maxValidSize: Int = 1 << 18 // 262,144 pixels + @MainActor var sizeFromMetadata: CGSize? { /// There are a number of types which have fixed sizes, in those cases we should return the target size rather than try to @@ -641,28 +887,17 @@ public extension ImageDataManager.DataSource { /// Since we don't have a direct size, try to extract it from the data guard - let imageData: Data = imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown - else { return nil } - - /// We can extract the size of a `GIF` directly so do that - if imageFormat == .gif, let gifSize: CGSize = imageData.suiKitGifSize { - guard gifSize.suiKitIsValidGifSize else { return nil } - - return gifSize - } - - guard - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0, - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int, - let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int, - pixelWidth > 0, - pixelHeight > 0 + let source: CGImageSource = createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidSize else { return nil } - return CGSize(width: pixelWidth, height: pixelHeight) + return CGSize(width: sourceWidth, height: sourceHeight) } } diff --git a/SessionUIKit/Utilities/Data+Utilities.swift b/SessionUIKit/Utilities/Data+Utilities.swift deleted file mode 100644 index ade5ca6593..0000000000 --- a/SessionUIKit/Utilities/Data+Utilities.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -/// **Note:** The below code **MUST** match the equivalent in `SessionUtilitiesKit.Data+Utilities` -internal extension Data { - var suiKitGuessedImageFormat: SUIKImageFormat { - let twoBytesLength: Int = 2 - - guard count > twoBytesLength else { return .unknown } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. bufferLength else { return nil } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: bufferLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. 0 && - Int(width) < maxValidSize && - Int(height) > 0 && - Int(height) < maxValidSize - ) - } -} From 4095b53ede6e351d6473bcfaad7dd14a380c44bd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 5 Aug 2025 09:59:53 +1000 Subject: [PATCH 018/244] Fixed a couple of minor issues with the SessionImageView --- SessionUIKit/Components/SessionImageView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index d155b262b7..8c0f2cf686 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -124,7 +124,7 @@ public class SessionImageView: UIImageView { case .none: pauseAnimationLoop() /// Pause when not visible case .some: /// Resume only if it has animation data and was meant to be animating - if let frames = animationFrames, frames.count > 1 { + if let frames = animationFrames, !frames.isEmpty, frames[0] != nil { resumeAnimationLoop() } } @@ -146,7 +146,7 @@ public class SessionImageView: UIImageView { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it - if let frames: [UIImage?] = animationFrames, !frames.isEmpty, !isAnimating() { + if let frames: [UIImage?] = animationFrames, !frames.isEmpty, frames[0] != nil, !isAnimating() { startAnimationLoop() } return @@ -207,7 +207,7 @@ public class SessionImageView: UIImageView { } /// Just to be safe set the initial frame - if self.image == nil, frames.indices.contains(0) { + if self.image == nil, !frames.isEmpty, frames[0] != nil { self.image = frames[0] } From f8985643c2d896bbcbc78ca1217f94a8664dfe7c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 5 Aug 2025 15:40:59 +1000 Subject: [PATCH 019/244] Animated image handling improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Cleaned up some code handling specific cases which could be generalised • Tweaks to try to reuse some resources when processing animated image frames • Fixed an issue where animated images would reset to the start if they ran out of frames while loading in the background • Fixed an issue where animated images would wait until all frames were loaded before playing (will now try to buffer the animation and start ASAP) • Fixed an issue where animated images would use 3x the memory they should (now only use 2x but that's a CoreAnimation limitation) • Fixed an issue where excessively large animated images would attempt to be loaded into the cache, clearing out all existing cached images (now they skip the cache) • Fixed a couple of edge-cases which could result in incorrect handling of animated images --- Session.xcodeproj/project.pbxproj | 20 +- .../Utilities/ImageLoading+Convenience.swift | 28 - .../Components/SessionImageView.swift | 90 +++- .../SwiftUI/SessionAsyncImage.swift | 98 +++- SessionUIKit/Types/ImageDataManager.swift | 490 +++++++++++++----- SessionUIKit/Utilities/Data+Utilities.swift | 79 --- 6 files changed, 498 insertions(+), 307 deletions(-) delete mode 100644 SessionUIKit/Utilities/Data+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c7a238e98e..5ed0e0cdfc 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -549,7 +549,6 @@ FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E32C35134B004D8A6C /* Data+Utilities.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; FD2272EC2C352155004D8A6C /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272EB2C352155004D8A6C /* Feature.swift */; }; @@ -1911,7 +1910,6 @@ FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Theme.swift"; sourceTree = ""; }; - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIKImageFormat.swift; sourceTree = ""; }; FD2272E92C351CA7004D8A6C /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; FD2272EB2C352155004D8A6C /* Feature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = ""; }; @@ -3314,7 +3312,6 @@ isa = PBXGroup; children = ( 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */, - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */, FD848B9728422F1A000E298B /* Date+Utilities.swift */, FD8A5B282DC060DD004C689B /* Double+Utilities.swift */, 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */, @@ -6076,7 +6073,6 @@ FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */, FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */, FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */, C331FFE72558FB0000070591 /* SNTextField.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, @@ -8156,7 +8152,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 623; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8196,7 +8192,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8237,7 +8233,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 623; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8272,7 +8268,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8718,7 +8714,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 623; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8757,7 +8753,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9305,7 +9301,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 623; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9338,7 +9334,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", diff --git a/Session/Utilities/ImageLoading+Convenience.swift b/Session/Utilities/ImageLoading+Convenience.swift index 7378040c4a..08e4ffd061 100644 --- a/Session/Utilities/ImageLoading+Convenience.swift +++ b/Session/Utilities/ImageLoading+Convenience.swift @@ -96,34 +96,6 @@ public extension ImageDataManagerType { load(source, onComplete: onComplete) } - // TODO: Is this needeed???? - func cachedImage( - attachment: Attachment, - using dependencies: Dependencies - ) -> UIImage? { - guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( - attachment: attachment, - using: dependencies - ) else { return nil } - - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - var result: ImageDataManager.ProcessedImageData? = nil - - load(source) { imageData in - result = imageData - semaphore.signal() - } - - /// We don't really want to wait at all but it's async logic so give it a very time timeout so it has the chance - /// to deal with other logic running - _ = semaphore.wait(timeout: .now() + .milliseconds(10)) - - switch result?.type { - case .staticImage(let image): return image - case .animatedImage(let frames, _): return frames.first - case .none: return nil - } - } } // MARK: - SessionImageView Convenience diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index 7b50a4d73b..70c8fc0d9b 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -8,9 +8,10 @@ public class SessionImageView: UIImageView { private var currentLoadIdentifier: String? private var imageLoadTask: Task? + private var streamConsumptionTask: Task? private var displayLink: CADisplayLink? - private var animationFrames: [UIImage]? + private var animationFrames: [UIImage?]? private var animationFrameDurations: [TimeInterval]? public private(set) var currentFrameIndex: Int = 0 public private(set) var accumulatedTime: TimeInterval = 0 @@ -32,6 +33,18 @@ public class SessionImageView: UIImageView { } } + public var shouldAnimateImage: Bool = true { + didSet { + guard oldValue != shouldAnimateImage else { return } + + if shouldAnimateImage { + startAnimationLoop() + } else { + stopAnimationLoop() + } + } + } + // MARK: - Initialization /// Use the `init(dataManager:)` initializer where possible to avoid explicitly needing to add the `dataManager` instance @@ -78,6 +91,7 @@ public class SessionImageView: UIImageView { deinit { imageLoadTask?.cancel() + streamConsumptionTask?.cancel() /// The documentation for `CADisplayLink` states: /// ``` @@ -128,7 +142,7 @@ public class SessionImageView: UIImageView { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it - if let frames: [UIImage] = animationFrames, !frames.isEmpty, !isAnimating() { + if let frames: [UIImage?] = animationFrames, !frames.isEmpty, frames[0] != nil, !isAnimating() { startAnimationLoop() } return @@ -141,9 +155,9 @@ public class SessionImageView: UIImageView { switch source { case .image(_, .some(let image)): imageSizeMetadata = image.size - return handleLoadedImageData( - ImageDataManager.ProcessedImageData(type: .staticImage(image)) - ) + handleLoadedImageData(ImageDataManager.ProcessedImageData(type: .staticImage(image))) + onComplete?(true) + return default: break } @@ -175,10 +189,11 @@ public class SessionImageView: UIImageView { @MainActor public func startAnimationLoop() { guard - let frames: [UIImage] = animationFrames, + shouldAnimateImage, + let frames: [UIImage?] = animationFrames, let durations: [TimeInterval] = animationFrameDurations, - frames.count > 1, - frames.count == durations.count + !frames.isEmpty, + !durations.isEmpty else { return stopAnimationLoop() } /// If it's already running (or paused) then no need to start the animation loop @@ -188,10 +203,11 @@ public class SessionImageView: UIImageView { } /// Just to be safe set the initial frame - if self.image == nil, frames.indices.contains(0) { + if self.image == nil, !frames.isEmpty, frames[0] != nil { self.image = frames[0] } + stopAnimationLoop() /// Make sure we don't unintentionally create extra `CADisplayLink` instances currentFrameIndex = 0 accumulatedTime = 0 @@ -207,7 +223,7 @@ public class SessionImageView: UIImageView { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, frames.count == durations.count, @@ -252,6 +268,7 @@ public class SessionImageView: UIImageView { @MainActor private func resetState(identifier: String?) { stopAnimationLoop() + streamConsumptionTask?.cancel() self.image = nil currentLoadIdentifier = identifier @@ -284,44 +301,69 @@ public class SessionImageView: UIImageView { self.currentFrameIndex = 0 self.accumulatedTime = 0 + guard self.shouldAnimateImage else { return } + switch frames.count { case 1...: startAnimationLoop() default: stopAnimationLoop() /// Treat as a static image } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream): + self.image = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + + guard durations.count > 1 else { + stopAnimationLoop() + return + } + + streamConsumptionTask = Task { @MainActor in + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { continue } + + startAnimationLoop() + } + } + } } } @objc private func updateFrame(displayLink: CADisplayLink) { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count else { return stopAnimationLoop() } accumulatedTime += displayLink.duration - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] /// It's possible for a long `CADisplayLink` tick to take longeer than a single frame so try to handle those cases while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count + - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) + + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift index 306fcc14e9..59ddb996e9 100644 --- a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift +++ b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift @@ -6,7 +6,7 @@ import NaturalLanguage public struct SessionAsyncImage: View { @State private var loadedImage: UIImage? = nil - @State private var animationFrames: [UIImage]? + @State private var animationFrames: [UIImage?]? @State private var animationFrameDurations: [TimeInterval]? @State private var isAnimating: Bool = false @@ -16,6 +16,7 @@ public struct SessionAsyncImage: View { private let source: ImageDataManager.DataSource private let dataManager: ImageDataManagerType + private let shouldAnimateImage: Bool private let content: (Image) -> Content private let placeholder: () -> Placeholder @@ -23,11 +24,13 @@ public struct SessionAsyncImage: View { public init( source: ImageDataManager.DataSource, dataManager: ImageDataManagerType, + shouldAnimateImage: Bool = true, @ViewBuilder content: @escaping (Image) -> Content, @ViewBuilder placeholder: @escaping () -> Placeholder ) { self.source = source self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage self.content = content self.placeholder = placeholder } @@ -55,23 +58,22 @@ public struct SessionAsyncImage: View { .task(id: source.identifier) { await loadAndProcessData() } + .onChange(of: shouldAnimateImage) { newValue in + if let frames = animationFrames, !frames.isEmpty { + isAnimating = newValue + } + } } // MARK: - Internal Functions private func loadAndProcessData() async { + /// Reset the state before loading new data + await MainActor.run { resetAnimationState() } + let processedData = await dataManager.load(source) - /// Reset the state before loading new data - await MainActor.run { - self.loadedImage = nil - self.animationFrames = nil - self.animationFrameDurations = nil - self.isAnimating = false - self.currentFrameIndex = 0 - self.accumulatedTime = 0.0 - self.lastFrameDate = .now - } + guard !Task.isCancelled else { return } switch processedData?.type { case .staticImage(let image): @@ -84,13 +86,43 @@ public struct SessionAsyncImage: View { self.animationFrames = frames self.animationFrameDurations = durations self.loadedImage = frames.first - self.isAnimating = true /// Activate the `TimelineView` + + if self.shouldAnimateImage { + self.isAnimating = true /// Activate the `TimelineView` + } + } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream) where durations.count > 1: + await MainActor.run { + self.loadedImage = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + } + + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + await MainActor.run { + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { return } + + self.isAnimating = true + } + } } case .animatedImage(let frames, _): await MainActor.run { self.loadedImage = frames.first } + + case .bufferedAnimatedImage(let firstFrame, _, _): + await MainActor.run { + self.loadedImage = firstFrame + } default: await MainActor.run { @@ -99,41 +131,53 @@ public struct SessionAsyncImage: View { } } + @MainActor + private func resetAnimationState() { + self.loadedImage = nil + self.animationFrames = nil + self.animationFrameDurations = nil + self.isAnimating = false + self.currentFrameIndex = 0 + self.accumulatedTime = 0.0 + self.lastFrameDate = .now + } + private func updateAnimationFrame(at date: Date) { guard isAnimating, - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count, let lastDate = lastFrameDate - else { return } + else { + isAnimating = false + return + } /// Calculate elapsed time since the last frame let elapsed: TimeInterval = date.timeIntervalSince(lastDate) self.lastFrameDate = date accumulatedTime += elapsed - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] // Advance frames if the accumulated time exceeds the current frame's duration while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } + + /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 84472c7ad0..46104f7982 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -11,6 +11,10 @@ public actor ImageDataManager: ImageDataManagerType { attributes: .concurrent ) + /// Max memory size for a decoded animation to be considered "small" enough to be fully cached + private static let decodedAnimationCacheLimit: Int = 20 * 1024 * 1024 // 20 M + private static let maxAnimatedImageDownscaleDimention: CGFloat = 4096 + /// `NSCache` has more nuanced memory management systems than just listening for `didReceiveMemoryWarningNotification` /// and can clear out values gradually, it can also remove items based on their "cost" so is better suited than our custom `LRUCache` private let cache: NSCache = { @@ -47,8 +51,8 @@ public actor ImageDataManager: ImageDataManagerType { /// Wait for the result then cache and return it let processedData: ProcessedImageData? = await newTask.value - if let data: ProcessedImageData = processedData { - self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCost) + if let data: ProcessedImageData = processedData, data.isCacheable { + self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCacheCost) } self.activeLoadTasks[identifier] = nil @@ -95,8 +99,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `videoUrl` values since it requires thumbnail generation case .videoUrl(let url, let mimeType, let sourceFilename, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -120,12 +131,15 @@ public actor ImageDataManager: ImageDataManagerType { let generator: AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true - guard let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil) else { - return nil - } + guard + let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { return nil } - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -144,8 +158,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `urlThumbnail` generation case .urlThumbnail(let url, let size, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -156,21 +177,23 @@ public actor ImageDataManager: ImageDataManagerType { /// Otherwise we need to generate a new one let maxDimensionInPixels: CGFloat = await size.pixelDimension() let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false, kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels ] guard - let format: SUIKImageFormat = dataSource.dataForGuessingImageFormat?.suiKitGuessedImageFormat, - format != .unknown, - let imageSource: CGImageSource = CGImageSourceCreateWithURL(url as CFURL, nil), - let thumbnail: CGImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) + let source: CGImageSource = dataSource.createImageSource(options: options), + let cgImage: CGImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) else { return nil } - let image: UIImage = UIImage(cgImage: thumbnail) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - /// Since we generated a new thumbnail we should save it to disk saveThumbnailToDisk( image: decodedImage, @@ -185,11 +208,21 @@ public actor ImageDataManager: ImageDataManagerType { case .closureThumbnail(_, _, let imageRetrier): guard let image: UIImage = await imageRetrier() else { return nil } + guard + let cgImage: CGImage = image.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { + return ProcessedImageData( + type: .staticImage(image) + ) + } /// Since there is likely custom (external) logic used to retrieve this thumbnail we don't save it to disk as there /// is no way to know if it _should_ change between generations/launches or not - let decodedImage: UIImage = (image.predecodedImage() ?? image) - return ProcessedImageData( type: .staticImage(decodedImage) ) @@ -197,7 +230,19 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `placeholderIcon` generation case .placeholderIcon(let seed, let text, let size): let image: UIImage = PlaceholderIcon.generate(seed: seed, text: text, size: size) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + + guard + let cgImage: CGImage = image.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { + return ProcessedImageData( + type: .staticImage(image) + ) + } return ProcessedImageData( type: .staticImage(decodedImage) @@ -209,13 +254,17 @@ public actor ImageDataManager: ImageDataManagerType { /// Otherwise load the data as either a static or animated image (do quick validation checks here - other checks /// require loading the image source anyway so don't bother to include them) guard - let imageData: Data = dataSource.imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown, - (imageFormat != .gif || imageData.suiKitHasValidGifSize), - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0 + let source: CGImageSource = dataSource.createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidSize else { return nil } - + + /// Get the number of frames in the image let count: Int = CGImageSourceGetCount(source) switch count { @@ -224,66 +273,197 @@ public actor ImageDataManager: ImageDataManagerType { /// Static image case 1: - guard let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { - return nil - } - /// Extract image orientation if present var orientation: UIImage.Orientation = .up if - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let rawCgOrientation: UInt32 = imageProperties[kCGImagePropertyOrientation] as? UInt32, + let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation as String] as? UInt32, let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) { orientation = UIImage.Orientation(cgOrientation) } - let image: UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: orientation) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + /// Try to decode the image direct from the `CGImage` + let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] + + guard + let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary), + let decodingContext = createDecodingContext(width: cgImage.width, height: cgImage.height), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext), + let decodedCgImage: CGImage = decodedImage.cgImage + else { return nil } + + let finalImage: UIImage = UIImage(cgImage: decodedCgImage, scale: 1, orientation: orientation) return ProcessedImageData( - type: .staticImage(decodedImage) + type: .staticImage(finalImage) ) /// Animated Image default: - var framesArray: [UIImage] = [] - var durationsArray: [TimeInterval] = [] - - for i in 0.. decodedAnimationCacheLimit else { + var frames: [UIImage] = [decodedFirstFrameImage] - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - let duration: TimeInterval = ImageDataManager.getFrameDuration(from: source, at: i) + for i in 1.. = AsyncStream { continuation in + let task = Task.detached(priority: .userInitiated) { + var (frameIndexesToBuffer, probeFrames) = await self.calculateHeuristicBuffer( + startIndex: 1, /// We have already decoded the first frame so skip it + source: source, + durations: durations, + using: decodingContext + ) + let lastBufferedFrameIndex: Int = ( + frameIndexesToBuffer.max() ?? + probeFrames.count + ) + + /// Immediately yield the frames decoded when calculating the buffer size + for (index, frame) in probeFrames.enumerated() { + if Task.isCancelled { break } + + /// We `+ 1` because the first frame is always manually assigned + continuation.yield(.frame(index: index + 1, frame: frame)) + } + + /// Clear out the `proveFrames` array so we don't use the extra memory + probeFrames.removeAll(keepingCapacity: false) + + /// Load in any additional buffer frames needed + for i in frameIndexesToBuffer { + guard !Task.isCancelled else { + continuation.finish() + return + } + + var decodedFrame: UIImage? + autoreleasepool { + decodedFrame = predecode( + cgImage: CGImageSourceCreateImageAtIndex(source, i, nil), + using: decodingContext + ) + } + + if let frame: UIImage = decodedFrame { + continuation.yield(.frame(index: i, frame: frame)) + } + } + + /// Now that we have buffered enough frames we can start the animation + if !Task.isCancelled { + continuation.yield(.readyToPlay) + } + + /// Start loading the remaining frames (`+ 1` as we want to start from the index after the last buffered index) + if lastBufferedFrameIndex < count { + for i in (lastBufferedFrameIndex + 1).. CGContext? { + guard width > 0 && height > 0 else { return nil } + + return CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: (width * 4), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: (CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) + ) + } + + private static func predecode(cgImage: CGImage?, using context: CGContext) -> UIImage? { + guard let cgImage: CGImage = cgImage else { return nil } + + let width: Int = context.width + let height: Int = context.height + context.clear(CGRect(x: 0, y: 0, width: width, height: height)) + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return context.makeImage().map { UIImage(cgImage: $0) } + } + + private static func getFrameDurations(from imageSource: CGImageSource, count: Int) -> [TimeInterval] { + return (0.. TimeInterval { guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any] else { return 0.1 @@ -328,6 +508,50 @@ public actor ImageDataManager: ImageDataManagerType { return 0.1 /// Fallback } + private static func calculateHeuristicBuffer( + startIndex: Int, + source: CGImageSource, + durations: [TimeInterval], + using context: CGContext + ) async -> (frameIndexesToBuffer: [Int], probeFrames: [UIImage]) { + let probeFrameCount: Int = 5 /// Number of frames to decode in order to calculate the approx. time to load each frame + let safetyMargin: Double = 2 /// Number of extra frames to be buffered just in case + + guard durations.count > (startIndex + probeFrameCount) else { + return (Array(startIndex.. 0.001 else { return ([], probeFrames) } + + let decodeToDisplayRatio: Double = (avgDecodeTime / avgDisplayDuration) + let calculatedBufferSize: Double = ceil(decodeToDisplayRatio) + safetyMargin + let finalFramesToBuffer: Int = Int(max(Double(probeFrameCount), min(calculatedBufferSize, 60.0))) + + guard finalFramesToBuffer > (startIndex + probeFrameCount) else { return ([], probeFrames) } + + return (Array((startIndex + probeFrameCount).. CGImageSource? { + let finalOptions: CFDictionary = ( + options ?? + [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] + ) as CFDictionary + switch self { - case .image(_, let image): return image - default: return nil + case .url(let url): return CGImageSourceCreateWithURL(url as CFURL, finalOptions) + case .data(_, let data): return CGImageSourceCreateWithData(data as CFData, finalOptions) + case .urlThumbnail(let url, _, _): return CGImageSourceCreateWithURL(url as CFURL, finalOptions) + + // These cases have special handling which doesn't use `createImageSource` + case .image, .videoUrl, .closureThumbnail, .placeholderIcon: return nil } } @@ -419,6 +647,7 @@ public extension ImageDataManager { lhsIdentifier == rhsIdentifier && lhsData == rhsData ) + case (.image(let lhsIdentifier, _), .image(let rhsIdentifier, _)): /// `UIImage` is not _really_ equatable so we need to use a separate identifier to use instead return (lhsIdentifier == rhsIdentifier) @@ -491,7 +720,29 @@ public extension ImageDataManager { public extension ImageDataManager { enum DataType { case staticImage(UIImage) - case animatedImage(frames: [UIImage], frameDurations: [TimeInterval]) + case animatedImage(frames: [UIImage], durations: [TimeInterval]) + case bufferedAnimatedImage( + firstFrame: UIImage, + durations: [TimeInterval], + bufferedFrameStream: AsyncStream + ) + } + + enum BufferedFrameStreamEvent { + case frame(index: Int, frame: UIImage) + case readyToPlay + } +} + +// MARK: - ImageDataManager.isAnimatedImage + +public extension ImageDataManager { + static func isAnimatedImage(_ imageData: Data?) -> Bool { + guard let data: Data = imageData, let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { + return false + } + let frameCount = CGImageSourceGetCount(imageSource) + return frameCount > 1 } } @@ -501,7 +752,14 @@ public extension ImageDataManager { class ProcessedImageData: @unchecked Sendable { public let type: DataType public let frameCount: Int - public let estimatedCost: Int + public let estimatedCacheCost: Int + + public var isCacheable: Bool { + switch type { + case .staticImage, .animatedImage: return true + case .bufferedAnimatedImage: return false + } + } init(type: DataType) { self.type = type @@ -509,11 +767,15 @@ public extension ImageDataManager { switch type { case .staticImage(let image): frameCount = 1 - estimatedCost = ProcessedImageData.calculateCost(for: [image]) + estimatedCacheCost = ProcessedImageData.calculateCost(for: [image]) case .animatedImage(let frames, _): frameCount = frames.count - estimatedCost = ProcessedImageData.calculateCost(for: frames) + estimatedCacheCost = ProcessedImageData.calculateCost(for: frames) + + case .bufferedAnimatedImage(_, let durations, _): + frameCount = durations.count + estimatedCacheCost = 0 } } @@ -535,44 +797,6 @@ public extension ImageDataManager { /// Needed for `actor` usage (ie. assume safe access) extension UIImage: @unchecked Sendable {} -extension UIImage { - /// When loading an image the OS doesn't immediately decompress the entire image in order to be efficient but since that - /// decompressing could happen on the main thread it would defeat the purpose of our background processing potentially - /// re-introducing the jitteriness this class was designed to resolve, so instead this function will decompress the image directly - func predecodedImage() -> UIImage? { - guard let cgImage = self.cgImage else { return self } - - let width: Int = cgImage.width - let height: Int = cgImage.height - - /// Avoid `CGBitmapContextCreate` error with 0 dimension - guard width > 0 && height > 0 else { return self } - - let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo: UInt32 = ( - CGImageAlphaInfo.premultipliedFirst.rawValue | - CGBitmapInfo.byteOrder32Little.rawValue - ) - - guard - let context: CGContext = CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: (width * 4), - space: colorSpace, - bitmapInfo: bitmapInfo - ) - else { return self } - - context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) - guard let drawnImage: CGImage = context.makeImage() else { return self } - - return UIImage(cgImage: drawnImage, scale: self.scale, orientation: self.imageOrientation) - } -} - extension AVAsset { var isValidVideo: Bool { var maxTrackSize = CGSize.zero @@ -593,6 +817,9 @@ extension AVAsset { } public extension ImageDataManager.DataSource { + /// We need to ensure that the image size is "reasonable", otherwise trying to load it could cause out-of-memory crashes + fileprivate static let maxValidSize: Int = 1 << 18 // 262,144 pixels + @MainActor var sizeFromMetadata: CGSize? { /// There are a number of types which have fixed sizes, in those cases we should return the target size rather than try to @@ -614,28 +841,17 @@ public extension ImageDataManager.DataSource { /// Since we don't have a direct size, try to extract it from the data guard - let imageData: Data = imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown - else { return nil } - - /// We can extract the size of a `GIF` directly so do that - if imageFormat == .gif, let gifSize: CGSize = imageData.suiKitGifSize { - guard gifSize.suiKitIsValidGifSize else { return nil } - - return gifSize - } - - guard - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0, - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int, - let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int, - pixelWidth > 0, - pixelHeight > 0 + let source: CGImageSource = createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidSize else { return nil } - return CGSize(width: pixelWidth, height: pixelHeight) + return CGSize(width: sourceWidth, height: sourceHeight) } } diff --git a/SessionUIKit/Utilities/Data+Utilities.swift b/SessionUIKit/Utilities/Data+Utilities.swift deleted file mode 100644 index ade5ca6593..0000000000 --- a/SessionUIKit/Utilities/Data+Utilities.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -/// **Note:** The below code **MUST** match the equivalent in `SessionUtilitiesKit.Data+Utilities` -internal extension Data { - var suiKitGuessedImageFormat: SUIKImageFormat { - let twoBytesLength: Int = 2 - - guard count > twoBytesLength else { return .unknown } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. bufferLength else { return nil } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: bufferLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. 0 && - Int(width) < maxValidSize && - Int(height) > 0 && - Int(height) < maxValidSize - ) - } -} From f368521d03894221b1e91cc6e5313ef6caea2d10 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 5 Aug 2025 16:33:54 +1000 Subject: [PATCH 020/244] Minor improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the PhotoLibrary to support rendering animated thumbnails of files • Replaced the `closureThumbnail` with `asyncSource` (which is more reusable) • Reduced the size of the thumbnails generated for the All Media grid and quotes (the `small` size is already bigger than what we are rendering so we should use that for performance) • Added the `gif` label to animated thumbnails for the PhotoLibrary picker for consistency --- .../Content Views/QuoteView.swift | 2 +- .../SwiftUI/QuoteView_SwiftUI.swift | 2 +- .../MediaTileViewController.swift | 2 +- .../PhotoLibrary.swift | 105 ++++++++++++------ SessionUIKit/Types/ImageDataManager.swift | 58 ++++------ 5 files changed, 92 insertions(+), 77 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index 452fd78e2e..590395a6c8 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -131,7 +131,7 @@ final class QuoteView: UIView { } // Generate the thumbnail if needed - imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak imageView] success in + imageView.loadThumbnail(size: .small, attachment: attachment, using: dependencies) { [weak imageView] success in guard success else { return } imageView?.contentMode = .scaleAspectFill diff --git a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift index ac22e57bdb..9f92551551 100644 --- a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift +++ b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift @@ -83,7 +83,7 @@ struct QuoteView_SwiftUI: View { SessionAsyncImage( attachment: attachment, - thumbnailSize: .medium, + thumbnailSize: .small, using: dependencies ) { image in image diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index de5111095b..5b0eb922d2 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -893,7 +893,7 @@ class GalleryGridCellItem: PhotoGridItem { var source: ImageDataManager.DataSource { ImageDataManager.DataSource.thumbnailFrom( attachment: galleryItem.attachment, - size: .medium, + size: .small, using: dependencies ) ?? .image("", nil) } diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index eca4eca67b..6aee5cfb2a 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -52,13 +52,15 @@ class PhotoPickerAssetItem: PhotoGridItem { return .video } - // TODO show GIF badge? + if asset.utType?.isAnimated == true { + return .animated + } return .photo } var source: ImageDataManager.DataSource { - return .closureThumbnail(self.asset.localIdentifier, size) { [photoCollectionContents, asset, size, pixelDimension] in + return .asyncSource(self.asset.localIdentifier) { [photoCollectionContents, asset, size, pixelDimension] in await photoCollectionContents.requestThumbnail( for: asset, size: size, @@ -148,44 +150,70 @@ class PhotoCollectionContents { // MARK: ImageManager - func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> UIImage? { + func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> ImageDataManager.DataSource? { var hasResumed: Bool = false - return await withCheckedContinuation { [imageManager] continuation in - let options = PHImageRequestOptions() - - switch size { - case .small: options.deliveryMode = .opportunistic - case .medium, .large: options.deliveryMode = .highQualityFormat - } - - imageManager.requestImage( - for: asset, - targetSize: thumbnailSize, - contentMode: .aspectFill, - options: options - ) { image, info in - guard !hasResumed else { return } - guard - info?[PHImageErrorKey] == nil, - (info?[PHImageCancelledKey] as? Bool) != true - else { - hasResumed = true - return continuation.resume(returning: nil) + /// The `requestImage` function will always return a static thumbnail so if it's an animated image then we need custom + /// handling (the default PhotoKit resizing can't resize animated images so we need to return the original file) + switch asset.utType?.isAnimated { + case .some(true): + return await withCheckedContinuation { [imageManager] continuation in + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + + imageManager.requestImageDataAndOrientation(for: asset, options: options) { data, uti, orientation, info in + guard !hasResumed else { return } + + guard let data = data, info?[PHImageErrorKey] == nil else { + hasResumed = true + continuation.resume(returning: nil) + return + } + + // Successfully fetched the data, resume with the animated result + hasResumed = true + continuation.resume(returning: .data(asset.localIdentifier, data)) + } } - switch size { - case .small: break // We want the first image, whether it is degraded or not - case .medium, .large: - // For medium and large thumbnails we want the full image so ignore any - // degraded images - guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return } - + default: + return await withCheckedContinuation { [imageManager] continuation in + let options = PHImageRequestOptions() + + switch size { + case .small: options.deliveryMode = .opportunistic + case .medium, .large: options.deliveryMode = .highQualityFormat + } + + imageManager.requestImage( + for: asset, + targetSize: thumbnailSize, + contentMode: .aspectFill, + options: options + ) { image, info in + guard !hasResumed else { return } + guard + info?[PHImageErrorKey] == nil, + (info?[PHImageCancelledKey] as? Bool) != true + else { + hasResumed = true + return continuation.resume(returning: nil) + } + + switch size { + case .small: break // We want the first image, whether it is degraded or not + case .medium, .large: + // For medium and large thumbnails we want the full image so ignore any + // degraded images + guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return } + + } + + continuation.resume(returning: .image("\(asset.localIdentifier)-\(size)", image)) + hasResumed = true + } } - - continuation.resume(returning: image) - hasResumed = true - } } } @@ -482,3 +510,10 @@ class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver { return collections } } + +private extension PHAsset { + var utType: UTType? { + return (value(forKey: "uniformTypeIdentifier") as? String) // stringlint:ignore + .map { UTType($0) } + } +} diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 46104f7982..eda34211bc 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -206,27 +206,6 @@ public actor ImageDataManager: ImageDataManagerType { type: .staticImage(decodedImage) ) - case .closureThumbnail(_, _, let imageRetrier): - guard let image: UIImage = await imageRetrier() else { return nil } - guard - let cgImage: CGImage = image.cgImage, - let decodingContext: CGContext = createDecodingContext( - width: cgImage.width, - height: cgImage.height - ), - let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) - else { - return ProcessedImageData( - type: .staticImage(image) - ) - } - - /// Since there is likely custom (external) logic used to retrieve this thumbnail we don't save it to disk as there - /// is no way to know if it _should_ change between generations/launches or not - return ProcessedImageData( - type: .staticImage(decodedImage) - ) - /// Custom handle `placeholderIcon` generation case .placeholderIcon(let seed, let text, let size): let image: UIImage = PlaceholderIcon.generate(seed: seed, text: text, size: size) @@ -248,6 +227,11 @@ public actor ImageDataManager: ImageDataManagerType { type: .staticImage(decodedImage) ) + case .asyncSource(_, let sourceRetriever): + guard let source: DataSource = await sourceRetriever() else { return nil } + + return await processSource(source) + default: break } @@ -576,8 +560,8 @@ public extension ImageDataManager { case image(String, UIImage?) case videoUrl(URL, String, String?, ThumbnailManager) case urlThumbnail(URL, ImageDataManager.ThumbnailSize, ThumbnailManager) - case closureThumbnail(String, ImageDataManager.ThumbnailSize, @Sendable () async -> UIImage?) case placeholderIcon(seed: String, text: String, size: CGFloat) + case asyncSource(String, @Sendable () async -> DataSource?) public var identifier: String { switch self { @@ -588,9 +572,6 @@ public extension ImageDataManager { case .urlThumbnail(let url, let size, _): return "\(url.absoluteString)-\(size)" - case .closureThumbnail(let identifier, let size, _): - return "\(identifier)-\(size)" - case .placeholderIcon(let seed, let text, let size): let content: (intSeed: Int, initials: String) = PlaceholderIcon.content( seed: seed, @@ -598,6 +579,9 @@ public extension ImageDataManager { ) return "\(seed)-\(content.initials)-\(Int(floor(size)))" + + /// We will use the identifier from the loaded source for caching purposes + case .asyncSource(let identifier, _): return identifier } } @@ -608,8 +592,8 @@ public extension ImageDataManager { case .image(_, let image): return image?.pngData() case .videoUrl: return nil case .urlThumbnail: return nil - case .closureThumbnail: return nil case .placeholderIcon: return nil + case .asyncSource: return nil } } @@ -635,7 +619,7 @@ public extension ImageDataManager { case .urlThumbnail(let url, _, _): return CGImageSourceCreateWithURL(url as CFURL, finalOptions) // These cases have special handling which doesn't use `createImageSource` - case .image, .videoUrl, .closureThumbnail, .placeholderIcon: return nil + case .image, .videoUrl, .placeholderIcon, .asyncSource: return nil } } @@ -665,12 +649,6 @@ public extension ImageDataManager { lhsSize == rhsSize ) - case (.closureThumbnail(let lhsIdentifier, let lhsSize, _), .closureThumbnail(let rhsIdentifier, let rhsSize, _)): - return ( - lhsIdentifier == rhsIdentifier && - lhsSize == rhsSize - ) - case (.placeholderIcon(let lhsSeed, let lhsText, let lhsSize), .placeholderIcon(let rhsSeed, let rhsText, let rhsSize)): return ( lhsSeed == rhsSeed && @@ -678,6 +656,9 @@ public extension ImageDataManager { lhsSize == rhsSize ) + case (.asyncSource(let lhsIdentifier, _), .asyncSource(let rhsIdentifier, _)): + return (lhsIdentifier == rhsIdentifier) + default: return false } } @@ -702,14 +683,13 @@ public extension ImageDataManager { url.hash(into: &hasher) size.hash(into: &hasher) - case .closureThumbnail(let identifier, let size, _): - identifier.hash(into: &hasher) - size.hash(into: &hasher) - case .placeholderIcon(let seed, let text, let size): seed.hash(into: &hasher) text.hash(into: &hasher) size.hash(into: &hasher) + + case .asyncSource(let identifier, _): + identifier.hash(into: &hasher) } } } @@ -830,13 +810,13 @@ public extension ImageDataManager.DataSource { return image.size - case .urlThumbnail(_, let size, _), .closureThumbnail(_, let size, _): + case .urlThumbnail(_, let size, _): let dimension: CGFloat = size.pixelDimension() return CGSize(width: dimension, height: dimension) case .placeholderIcon(_, _, let size): return CGSize(width: size, height: size) - case .url, .data, .videoUrl: break + case .url, .data, .videoUrl, .asyncSource: break } /// Since we don't have a direct size, try to extract it from the data From 5adc58bf085819b5850d1e239eff38424cf1ac92 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 6 Aug 2025 14:15:06 +1000 Subject: [PATCH 021/244] Fix issues after merge --- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../ProfilePictureView+Convenience.swift | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index dab2aa6c2c..131ce7fee5 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1768,7 +1768,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob return dependencies[singleton: .displayPictureManager] .prepareAndUploadDisplayPicture(imageData: data) .showingBlockingLoading(in: self?.navigatableState) - .map { url, filePath, key -> DisplayPictureManager.Update in + .map { url, filePath, key, _ -> DisplayPictureManager.Update in .groupUpdateTo(url: url, key: key, filePath: filePath) } .mapError { $0 as Error } diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index fff1736d22..8a30918feb 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -52,7 +52,9 @@ public extension ProfilePictureView { /// If we are given an explicit `displayPictureUrl` then only use that return (Info( source: .url(URL(fileURLWithPath: path)), - icon: profileIcon + shouldAnimated: (threadVariant == .community), + isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), + icon: profileIcon, ), nil) case (_, _, .community): @@ -65,6 +67,8 @@ public extension ProfilePictureView { case .hero: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), + shouldAnimated: true, + isCurrentUser: false, inset: UIEdgeInsets( top: 12, left: 12, @@ -101,7 +105,12 @@ public extension ProfilePictureView { }() return ( - Info(source: source, icon: profileIcon), + Info( + source: source, + shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), + icon: profileIcon + ), additionalProfile .map { other in let source: ImageDataManager.DataSource = { @@ -120,11 +129,18 @@ public extension ProfilePictureView { return ImageDataManager.DataSource.url(URL(fileURLWithPath: path)) }() - return Info(source: source, icon: additionalProfileIcon) + return Info( + source: source, + shouldAnimated: other.shoudAnimateProfilePicture(using: dependencies), + isCurrentUser: (other.id == dependencies[cache: .general].sessionId.hexString), + icon: additionalProfileIcon + ) } .defaulting( to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), + shouldAnimated: false, + isCurrentUser: false, renderingMode: .alwaysTemplate, themeTintColor: .white, inset: UIEdgeInsets( @@ -156,7 +172,14 @@ public extension ProfilePictureView { return ImageDataManager.DataSource.url(URL(fileURLWithPath: path)) }() - return (Info(source: source, icon: profileIcon), nil) + return ( + Info( + source: source, + shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), + icon: profileIcon), + nil + ) } } } @@ -192,7 +215,8 @@ public extension ProfilePictureSwiftUI { size: size, info: info, additionalInfo: additionalInfo, - dataManager: dependencies[singleton: .imageDataManager] + dataManager: dependencies[singleton: .imageDataManager], + sessionProState: dependencies[singleton: .sessionProState] ) } } From 972ee619a452d3470cb840aae4a92ef22b2fada7 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 6 Aug 2025 16:12:56 +1000 Subject: [PATCH 022/244] manually positioning animated profile image in Pro CTA --- Session.xcodeproj/project.pbxproj | 18 ++---- .../AnimatedProfileCTAAnimation.webp | Bin 1131500 -> 0 bytes .../AnimatedProfileCTAAnimationCropped.webp | Bin 0 -> 211410 bytes .../Meta/WebPImages/GenericCTAAnimation.webp | Bin 722072 -> 0 bytes .../Components/SwiftUI/ProCTAModal.swift | 59 ++++++++++++------ 5 files changed, 45 insertions(+), 32 deletions(-) delete mode 100644 Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp create mode 100644 Session/Meta/WebPImages/AnimatedProfileCTAAnimationCropped.webp delete mode 100644 Session/Meta/WebPImages/GenericCTAAnimation.webp diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 69b6558f23..78bfc7d05b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -197,13 +197,13 @@ 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */; }; 94AAB1572E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; - 94AAB15D2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; - 94AAB15F2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */; }; 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94AAB1622E28742300A6FA18 /* _030_AddProfileProProof.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1612E28742200A6FA18 /* _030_AddProfileProProof.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B6BAF52E30A88800E718BB /* SessionProState.swift */; }; + 94B6BB062E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */; }; + 94B6BB072E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BC2E0908340097754D /* LibSession+Pro.swift */; }; @@ -214,9 +214,7 @@ 94CD96322E1B88C20097754D /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD96312E1B88C20097754D /* ExpandingAttachmentsButton.swift */; }; 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; - 94CD96422E1BABE90097754D /* GenericCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */; }; 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; - 94CD96442E1BAC0F0097754D /* GenericCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */; }; 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -1575,10 +1573,10 @@ 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShineButton.swift; sourceTree = ""; }; 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = PinnedConversationsCTA.webp; sourceTree = ""; }; 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; - 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimation.webp; sourceTree = ""; }; 94AAB1612E28742200A6FA18 /* _030_AddProfileProProof.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_AddProfileProProof.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; 94B6BAF52E30A88800E718BB /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; + 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimationCropped.webp; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; 94CD95BC2E0908340097754D /* LibSession+Pro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Pro.swift"; sourceTree = ""; }; @@ -1588,7 +1586,6 @@ 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; 94CD96312E1B88C20097754D /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = ""; }; 94CD963C2E1BABE90097754D /* GenericCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTA.webp; sourceTree = ""; }; - 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTAAnimation.webp; sourceTree = ""; }; 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = HigherCharLimitCTA.webp; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; @@ -2890,11 +2887,10 @@ 94CD963F2E1BABE90097754D /* WebPImages */ = { isa = PBXGroup; children = ( + 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */, 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */, - 94AAB15C2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp */, 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */, 94CD963C2E1BABE90097754D /* GenericCTA.webp */, - 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */, 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */, ); path = WebPImages; @@ -5638,11 +5634,10 @@ buildActionMask = 2147483647; files = ( 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */, - 94AAB15D2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */, 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */, - 94CD96442E1BAC0F0097754D /* GenericCTAAnimation.webp in Resources */, 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */, 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */, + 94B6BB062E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */, 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */, B8D07406265C683A00F77E07 /* ElegantIcons.ttf in Resources */, FD86FDA42BC51C5400EC251B /* PrivacyInfo.xcprivacy in Resources */, @@ -5726,8 +5721,8 @@ C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */, FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */, B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */, + 94B6BB072E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */, 45B74A7E2044AAB600CD42F8 /* complete.aifc in Resources */, - 94AAB15F2E24C97400A6FA18 /* AnimatedProfileCTAAnimation.webp in Resources */, 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */, 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */, B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */, @@ -5749,7 +5744,6 @@ C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */, 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */, 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */, - 94CD96422E1BABE90097754D /* GenericCTAAnimation.webp in Resources */, 45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */, 45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */, 45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */, diff --git a/Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp b/Session/Meta/WebPImages/AnimatedProfileCTAAnimation.webp deleted file mode 100644 index c217e67c79b52934392b27f8abe7e6cff3c06d72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1131500 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

    tz#Fm;HQm(B0^g8-|t&vM>;gE34p-!oXv0!D6#> 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

  • >OeB;}?FReoj!Mxh?Fjio%X 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%0Ag6hW7|#?#mk>3xHU!SelH^pT`n7Gp0z3*-Qie)r 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 v?@93z}nD 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

    AxJpcfbQ0|2e#yJ17UvFeP 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

  • -y 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 nI1^CPkO4?CZ~<`=lQ2{qqyqvxfE0)arc Date: Thu, 7 Aug 2025 10:23:08 +1000 Subject: [PATCH 023/244] Fixed an incorrect path in the libSession build script --- Scripts/build_libSession_util.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index 2f9cbbba6e..df2d64b325 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -37,7 +37,7 @@ fi if [ "${COMPILE_LIB_SESSION}" != "YES" ]; then echo "Restoring original headers to Xcode Indexer cache from backup..." rm -rf "${INDEX_DIR}/include" - rsync -rt --exclude='.DS_Store' "${PRE_BUILT_FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" "${INDEX_DIR}/include" + rsync -rt --exclude='.DS_Store' "${PRE_BUILT_FRAMEWORK_DIR}/${FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" "${INDEX_DIR}/include" echo "Using pre-packaged SessionUtil" exit 0 From 97daeec50d99ad45d02c41591864c54a8bf2bdc3 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 7 Aug 2025 10:25:29 +1000 Subject: [PATCH 024/244] wip: fix profile image not animating --- SessionMessagingKit/Database/Models/Profile.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 804c7ef4a6..5268979f59 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -321,6 +321,8 @@ public extension Profile { public extension Profile { func shoudAnimateProfilePicture(using dependencies: Dependencies) -> Bool { + guard dependencies.hasSet(feature: .sessionProEnabled) && dependencies[feature: .sessionProEnabled] else { return true } + guard self.id == dependencies[cache: .general].sessionId.hexString else { return dependencies.mutate(cache: .libSession, { $0.validateProProof(for: self) }) } From 86469ab50cab05127b38dfd383276ba7865046cd Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 7 Aug 2025 10:46:55 +1000 Subject: [PATCH 025/244] fix ui issue on profile picture view --- .../Utilities/ProfilePictureView+Convenience.swift | 10 ++++++++-- .../Components/Modals & Toast/ConfirmationModal.swift | 6 +++--- SessionUIKit/Components/ProfilePictureView.swift | 9 +++++++-- SessionUIKit/Components/SwiftUI/ProCTAModal.swift | 3 ++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 8a30918feb..4f39cce866 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -49,10 +49,16 @@ public extension ProfilePictureView { switch (explicitPath, publicKey.isEmpty, threadVariant) { case (.some(let path), _, _): + let shouldAnimated: Bool = { + guard let profile: Profile = profile else { + return threadVariant == .community + } + return profile.shoudAnimateProfilePicture(using: dependencies) + }() /// If we are given an explicit `displayPictureUrl` then only use that return (Info( source: .url(URL(fileURLWithPath: path)), - shouldAnimated: (threadVariant == .community), + shouldAnimated: shouldAnimated, isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), icon: profileIcon, ), nil) @@ -64,7 +70,7 @@ public extension ProfilePictureView { switch size { case .navigation, .message: return .image("SessionWhite16", #imageLiteral(resourceName: "SessionWhite16")) case .list: return .image("SessionWhite24", #imageLiteral(resourceName: "SessionWhite24")) - case .hero: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) + case .hero, .modal: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), shouldAnimated: true, diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 30a8f7fbe7..822ca029c4 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -214,7 +214,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { }() private lazy var profileView: ProfilePictureView = ProfilePictureView( - size: .hero, + size: .modal, dataManager: nil, sessionProState: nil ) @@ -372,8 +372,8 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { imageViewContainer.addSubview(profileView) profileView.center(.horizontal, in: imageViewContainer) - profileView.pin(.top, to: .top, of: imageViewContainer) - profileView.pin(.bottom, to: .bottom, of: imageViewContainer) + profileView.pin(.top, to: .top, of: imageViewContainer, withInset: 20) + profileView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -20) proImageStackViewContainer.addSubview(proImageStackView) proImageStackView.center(.horizontal, in: proImageStackViewContainer) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index f12c6b0312..8c8568da50 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -44,12 +44,14 @@ public final class ProfilePictureView: UIView { case message case list case hero + case modal public var viewSize: CGFloat { switch self { case .navigation, .message: return 26 case .list: return 46 case .hero: return 110 + case .modal: return 90 } } @@ -57,7 +59,8 @@ public final class ProfilePictureView: UIView { switch self { case .navigation, .message: return 26 case .list: return 46 - case .hero: return 90 + case .hero: return 80 + case .modal: return 90 } } @@ -65,7 +68,8 @@ public final class ProfilePictureView: UIView { switch self { case .navigation, .message: return 18 // Shouldn't be used case .list: return 32 - case .hero: return 90 + case .hero: return 80 + case .modal: return 90 } } @@ -74,6 +78,7 @@ public final class ProfilePictureView: UIView { case .navigation, .message: return 10 // Intentionally not a multiple of 4 case .list: return 16 case .hero: return 24 + case .modal: return 24 // Shouldn't be used } } } diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 4e9b60e457..9545ee17e1 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -35,7 +35,8 @@ public struct ProCTAModal: View { default: return nil } } - + /// Note: This is a hack to manually position the animated avatar in the CTA background image to prevent heavy loading for the + /// animated webp. public var animatedAvatarImagePadding: (leading: CGFloat, top: CGFloat) { switch self { case .generic: From 65e4d7f2ba30318b58a3f9260ab86e6cc88d7849 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Aug 2025 13:13:06 +1000 Subject: [PATCH 026/244] Renamed SessionSnodeKit to SessionNetworkingKit --- Session.xcodeproj/project.pbxproj | 146 +++++++++--------- .../xcshareddata/xcschemes/Session.xcscheme | 4 +- ...xcscheme => SessionNetworkingKit.xcscheme} | 8 +- .../Calls/Call Management/SessionCall.swift | 2 +- Session/Calls/WebRTC/WebRTCSession.swift | 2 +- .../Closed Groups/EditGroupViewModel.swift | 2 +- .../ContextMenuVC+ActionView.swift | 2 +- .../ConversationVC+Interaction.swift | 2 +- .../Conversations/ConversationViewModel.swift | 2 +- .../DisappearingMessageTimerView.swift | 2 +- ...isappearingMessagesSettingsViewModel.swift | 2 +- .../ThreadNotificationSettingsViewModel.swift | 2 +- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../New Conversation/NewMessageScreen.swift | 2 +- .../GIFs/GifPickerCell.swift | 2 +- .../GIFs/GiphyAPI.swift | 2 +- .../MediaPageViewController.swift | 2 +- .../MessageInfoScreen.swift | 2 +- .../PhotoCapture.swift | 2 +- Session/Meta/AppDelegate.swift | 2 +- Session/Meta/Session+SNUIKit.swift | 2 +- .../NotificationActionHandler.swift | 2 +- Session/Notifications/SyncPushTokensJob.swift | 2 +- Session/Onboarding/LoadingScreen.swift | 2 +- Session/Onboarding/Onboarding.swift | 2 +- Session/Onboarding/PNModeScreen.swift | 2 +- Session/Path/PathStatusView.swift | 2 +- Session/Path/PathVC.swift | 2 +- .../Settings/DeveloperSettingsViewModel.swift | 2 +- Session/Settings/NukeDataModal.swift | 2 +- .../SessionNetworkScreen+ViewModel.swift | 2 +- Session/Utilities/BackgroundPoller.swift | 2 +- Session/Utilities/IP2Country.swift | 2 +- .../Crypto/Crypto+SessionMessagingKit.swift | 2 +- .../Migrations/_002_SetupStandardJobs.swift | 2 +- .../Migrations/_004_RemoveLegacyYDB.swift | 2 +- .../_022_GroupsRebuildChanges.swift | 2 +- .../Database/Models/Attachment.swift | 2 +- .../Database/Models/ClosedGroup.swift | 2 +- .../Database/Models/ConfigDump.swift | 2 +- .../DisappearingMessageConfiguration.swift | 2 +- .../Database/Models/Interaction.swift | 2 +- .../Database/Models/LinkPreview.swift | 2 +- .../Database/Models/SessionThread.swift | 2 +- .../Jobs/AttachmentDownloadJob.swift | 2 +- .../Jobs/AttachmentUploadJob.swift | 2 +- .../Jobs/CheckForAppUpdatesJob.swift | 2 +- .../Jobs/ConfigMessageReceiveJob.swift | 2 +- .../Jobs/ConfigurationSyncJob.swift | 2 +- .../Jobs/DisappearingMessagesJob.swift | 2 +- .../Jobs/DisplayPictureDownloadJob.swift | 2 +- .../Jobs/ExpirationUpdateJob.swift | 2 +- .../Jobs/GarbageCollectionJob.swift | 2 +- .../Jobs/GetExpirationJob.swift | 2 +- .../Jobs/GroupInviteMemberJob.swift | 2 +- .../Jobs/GroupLeavingJob.swift | 2 +- .../Jobs/GroupPromoteMemberJob.swift | 2 +- SessionMessagingKit/Jobs/MessageSendJob.swift | 2 +- ...ProcessPendingGroupMemberRemovalsJob.swift | 2 +- .../Jobs/SendReadReceiptsJob.swift | 2 +- .../LibSession+GroupInfo.swift | 2 +- .../LibSession+GroupMembers.swift | 2 +- .../Config Handling/LibSession+Shared.swift | 2 +- .../LibSession+SharedGroup.swift | 2 +- .../LibSession+UserGroups.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 2 +- .../LibSession/Types/Config.swift | 2 +- .../Messages/Message+Destination.swift | 2 +- .../Message+DisappearingMessages.swift | 2 +- .../Messages/Message+Origin.swift | 2 +- SessionMessagingKit/Messages/Message.swift | 2 +- .../Open Groups/Models/SOGSMessage.swift | 2 +- .../Open Groups/OpenGroupAPI.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 2 +- .../Types/HTTPHeader+OpenGroup.swift | 2 +- .../Types/HTTPQueryParam+OpenGroup.swift | 2 +- .../Types/Request+OpenGroupAPI.swift | 2 +- .../Open Groups/Types/SOGSEndpoint.swift | 2 +- .../MessageReceiver+Calls.swift | 2 +- ...eReceiver+DataExtractionNotification.swift | 2 +- .../MessageReceiver+Groups.swift | 2 +- .../MessageReceiver+LegacyClosedGroups.swift | 2 +- .../MessageReceiver+LibSession.swift | 2 +- .../MessageReceiver+MessageRequests.swift | 2 +- .../MessageReceiver+UnsendRequests.swift | 2 +- .../MessageReceiver+VisibleMessages.swift | 2 +- .../MessageSender+Groups.swift | 2 +- .../Sending & Receiving/MessageReceiver.swift | 2 +- .../MessageSender+Convenience.swift | 2 +- .../Sending & Receiving/MessageSender.swift | 2 +- .../Models/LegacyUnsubscribeRequest.swift | 2 +- .../Models/NotificationMetadata.swift | 2 +- .../Models/SubscribeRequest.swift | 2 +- .../Models/UnsubscribeRequest.swift | 2 +- .../Notifications/PushNotificationAPI.swift | 2 +- .../Types/PushNotificationAPIEndpoint.swift | 2 +- .../Types/Request+PushNotificationAPI.swift | 2 +- .../Pollers/CommunityPoller.swift | 2 +- .../Pollers/CurrentUserPoller.swift | 2 +- .../Pollers/GroupPoller.swift | 2 +- .../Pollers/PollerType.swift | 2 +- .../Pollers/SwarmPoller.swift | 2 +- .../Typing Indicators/TypingIndicators.swift | 2 +- .../MessageViewModel+DeletionActions.swift | 2 +- .../Authentication+SessionMessagingKit.swift | 2 +- .../Utilities/DisplayPictureManager.swift | 2 +- .../Utilities/MessageWrapper.swift | 2 +- .../SNProtoEnvelope+Conversion.swift | 2 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 2 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 2 +- .../LibSession/LibSessionGroupInfoSpec.swift | 6 +- .../LibSessionGroupMembersSpec.swift | 2 +- .../LibSession/LibSessionSpec.swift | 2 +- .../Open Groups/Models/SOGSMessageSpec.swift | 2 +- .../Open Groups/OpenGroupAPISpec.swift | 2 +- .../Open Groups/OpenGroupManagerSpec.swift | 2 +- .../Open Groups/Types/SOGSEndpointSpec.swift | 2 +- .../MessageReceiverGroupsSpec.swift | 4 +- .../MessageSenderGroupsSpec.swift | 2 +- .../MessageSenderSpec.swift | 2 +- .../Pollers/CommunityPollerSpec.swift | 2 +- .../_TestUtilities/MockPoller.swift | 2 +- .../_TestUtilities/MockSwarmPoller.swift | 2 +- .../Configuration.swift | 4 +- .../Crypto/Crypto+SessionNetworkingKit.swift | 0 .../_001_InitialSetupMigration.swift | 2 +- .../Migrations/_002_SetupStandardJobs.swift | 2 +- .../Migrations/_003_YDBToGRDBMigration.swift | 2 +- ...04_FlagMessageHashAsDeletedOrInvalid.swift | 2 +- ...ddSnodeReveivedMessageInfoPrimaryKey.swift | 2 +- .../Migrations/_006_DropSnodeCache.swift | 2 +- .../_007_SplitSnodeReceivedMessageInfo.swift | 2 +- .../_008_ResetUserConfigLastHashes.swift | 2 +- .../Models/SnodeReceivedMessageInfo.swift | 0 .../LibSession/LibSession+Networking.swift | 0 .../Meta/Info.plist | 0 .../Meta/SessionNetworkingKit.h | 4 + .../Models/AppVersionResponse.swift | 0 .../Models/DeleteAllBeforeRequest.swift | 0 .../Models/DeleteAllBeforeResponse.swift | 0 .../Models/DeleteAllMessagesRequest.swift | 0 .../Models/DeleteAllMessagesResponse.swift | 0 .../Models/DeleteMessagesRequest.swift | 0 .../Models/DeleteMessagesResponse.swift | 0 .../Models/FileUploadResponse.swift | 0 .../Models/GetExpiriesRequest.swift | 0 .../Models/GetExpiriesResponse.swift | 0 .../Models/GetMessagesRequest.swift | 0 .../Models/GetMessagesResponse.swift | 0 .../Models/GetNetworkTimestampResponse.swift | 0 .../Models/LegacyGetMessagesRequest.swift | 0 .../Models/LegacySendMessageRequest.swift | 0 .../Models/ONSResolveRequest.swift | 0 .../Models/ONSResolveResponse.swift | 0 .../Models/OxenDaemonRPCRequest.swift | 0 .../Models/RevokeSubaccountRequest.swift | 0 .../Models/RevokeSubaccountResponse.swift | 0 .../Models/SendMessageRequest.swift | 0 .../Models/SendMessageResponse.swift | 0 .../SnodeAuthenticatedRequestBody.swift | 0 .../Models/SnodeBatchRequest.swift | 0 .../Models/SnodeMessage.swift | 0 .../Models/SnodeReceivedMessage.swift | 0 .../Models/SnodeRecursiveResponse.swift | 0 .../Models/SnodeRequest.swift | 0 .../Models/SnodeResponse.swift | 0 .../Models/SnodeSwarmItem.swift | 0 .../Models/UnrevokeSubaccountRequest.swift | 0 .../Models/UnrevokeSubaccountResponse.swift | 0 .../Models/UpdateExpiryAllRequest.swift | 0 .../Models/UpdateExpiryAllResponse.swift | 0 .../Models/UpdateExpiryRequest.swift | 0 .../Models/UpdateExpiryResponse.swift | 0 .../Networking/SnodeAPI.swift | 0 .../HTTPHeader+SessionNetwork.swift | 0 .../SessionNetworkAPI+Database.swift | 0 .../SessionNetworkAPI+Models.swift | 0 .../SessionNetworkAPI+Network.swift | 4 +- .../SessionNetworkAPI/SessionNetworkAPI.swift | 0 .../SnodeAPI/Request+SnodeAPI.swift | 0 .../SnodeAPI/ResponseInfo+SnodeAPI.swift | 0 .../SnodeAPI/SnodeAPI.swift | 0 .../SnodeAPI/SnodeAPIEndpoint.swift | 0 .../SnodeAPI/SnodeAPIError.swift | 0 .../SnodeAPI/SnodeAPINamespace.swift | 0 .../Types/BatchRequest.swift | 0 .../Types/BatchResponse.swift | 0 .../Types/BencodeResponse.swift | 0 .../Types/ContentProxy.swift | 0 .../Types/Destination.swift | 0 .../Types/HTTPHeader.swift | 0 .../Types/HTTPMethod.swift | 0 .../Types/HTTPQueryParam.swift | 0 .../Types/IPv4.swift | 0 .../Types/JSON.swift | 0 .../Types/Network.swift | 0 .../Types/NetworkError.swift | 0 .../Types/PreparedRequest+Sending.swift | 0 .../Types/PreparedRequest.swift | 0 .../Types/ProxiedContentDownloader.swift | 0 .../Types/Request.swift | 0 .../Types/RequestCategory.swift | 0 .../Types/ResponseInfo.swift | 0 .../HTTPHeader+SessionNetwork.swift | 9 ++ .../SessionNetworkAPI+Database.swift | 32 ++++ .../SessionNetworkAPI+Models.swift | 75 +++++++++ .../SessionNetworkAPI+Network.swift | 107 +++++++++++++ .../SessionNetworkAPI/SessionNetworkAPI.swift | 131 ++++++++++++++++ .../Types/SwarmDrainBehaviour.swift | 0 .../Types/UpdatableTimestamp.swift | 0 .../Types/ValidatableResponse.swift | 0 .../Utilities/Data+Utilities.swift | 0 .../Utilities/Publisher+Utilities.swift | 0 .../Utilities/RetryWithDependencies.swift | 0 .../Utilities/String+Trimming.swift | 0 .../Utilities/URLResponse+Utilities.swift | 0 .../Models/FileUploadResponseSpec.swift | 2 +- .../Models/SnodeRequestSpec.swift | 2 +- .../SessionNetworkingKit.xctestplan | 2 +- .../Types/BatchRequestSpec.swift | 2 +- .../Types/BatchResponseSpec.swift | 2 +- .../Types/BencodeResponseSpec.swift | 2 +- .../Types/DestinationSpec.swift | 2 +- .../Types/HeaderSpec.swift | 2 +- .../Types/PreparedRequestSendingSpec.swift | 2 +- .../Types/PreparedRequestSpec.swift | 2 +- .../Types/RequestSpec.swift | 2 +- .../CommonSSKMockExtensions.swift | 2 +- .../_TestUtilities/MockNetwork.swift | 2 +- .../_TestUtilities/MockSnodeAPICache.swift | 2 +- .../NotificationServiceExtension.swift | 2 +- .../ShareNavController.swift | 2 +- SessionShareExtension/ThreadPickerVC.swift | 2 +- SessionSnodeKit/Meta/SessionSnodeKit.h | 4 - SessionSnodeKit/Utilities/Threading+SSK.swift | 6 - ...eadDisappearingMessagesViewModelSpec.swift | 4 +- ...eadNotificationSettingsViewModelSpec.swift | 4 +- .../ThreadSettingsViewModelSpec.swift | 4 +- SessionTests/Database/DatabaseSpec.swift | 6 +- SessionTests/Session.xctestplan | 4 +- .../NotificationContentViewModelSpec.swift | 4 +- .../Database/Types/TargetMigrations.swift | 2 +- SessionUtilitiesKit/General/Logging.swift | 139 +++++++++++++---- .../LibSession/LibSession.swift | 59 ++++++- SignalUtilitiesKit/Utilities/AppSetup.swift | 4 +- 245 files changed, 760 insertions(+), 300 deletions(-) rename Session.xcodeproj/xcshareddata/xcschemes/{SessionSnodeKit.xcscheme => SessionNetworkingKit.xcscheme} (91%) rename {SessionSnodeKit => SessionNetworkingKit}/Configuration.swift (89%) rename SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift => SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_001_InitialSetupMigration.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_002_SetupStandardJobs.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_003_YDBToGRDBMigration.swift (88%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift (93%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift (97%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_006_DropSnodeCache.swift (94%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift (98%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_008_ResetUserConfigLastHashes.swift (94%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Models/SnodeReceivedMessageInfo.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/LibSession/LibSession+Networking.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Meta/Info.plist (100%) create mode 100644 SessionNetworkingKit/Meta/SessionNetworkingKit.h rename {SessionSnodeKit => SessionNetworkingKit}/Models/AppVersionResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllBeforeRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllBeforeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/FileUploadResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetExpiriesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetExpiriesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetNetworkTimestampResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/LegacyGetMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/LegacySendMessageRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/ONSResolveRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/ONSResolveResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/OxenDaemonRPCRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/RevokeSubaccountRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/RevokeSubaccountResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SendMessageRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SendMessageResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeAuthenticatedRequestBody.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeBatchRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeMessage.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeReceivedMessage.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeRecursiveResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeSwarmItem.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UnrevokeSubaccountRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UnrevokeSubaccountResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryAllRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryAllResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Networking/SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Database.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Models.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Network.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/Request+SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/ResponseInfo+SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPIEndpoint.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPIError.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPINamespace.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BatchRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BatchResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BencodeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ContentProxy.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Destination.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPHeader.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPMethod.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPQueryParam.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/IPv4.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/JSON.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Network.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/NetworkError.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/PreparedRequest+Sending.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/PreparedRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ProxiedContentDownloader.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Request.swift (100%) create mode 100644 SessionNetworkingKit/Types/RequestCategory.swift rename {SessionSnodeKit => SessionNetworkingKit}/Types/ResponseInfo.swift (100%) create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift rename {SessionSnodeKit => SessionNetworkingKit}/Types/SwarmDrainBehaviour.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/UpdatableTimestamp.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ValidatableResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/Data+Utilities.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/Publisher+Utilities.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/RetryWithDependencies.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/String+Trimming.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/URLResponse+Utilities.swift (100%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Models/FileUploadResponseSpec.swift (96%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Models/SnodeRequestSpec.swift (98%) rename SessionSnodeKitTests/SessionSnodeKit.xctestplan => SessionNetworkingKitTests/SessionNetworkingKit.xctestplan (90%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BatchRequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BatchResponseSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BencodeResponseSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/DestinationSpec.swift (98%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/HeaderSpec.swift (93%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/PreparedRequestSendingSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/PreparedRequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/RequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/CommonSSKMockExtensions.swift (96%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/MockNetwork.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/MockSnodeAPICache.swift (97%) delete mode 100644 SessionSnodeKit/Meta/SessionSnodeKit.h delete mode 100644 SessionSnodeKit/Utilities/Threading+SSK.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5ed0e0cdfc..88ed056459 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */; }; B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A26825E4A2C200C1835E /* Onboarding.swift */; }; B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; @@ -286,7 +286,7 @@ C300A5F22554B09800555489 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5F12554B09800555489 /* MessageSender.swift */; }; C300A60D2554B31900555489 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; }; C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C302093D25DCBF07001F572D /* MentionSelectionView.swift */; }; - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */; }; @@ -314,7 +314,7 @@ C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */; }; C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; @@ -329,7 +329,7 @@ C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */; }; C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; @@ -373,16 +373,15 @@ C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; }; C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C3C2A74425539EB700C340D1 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74325539EB700C340D1 /* Message.swift */; }; C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */; }; C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */; }; @@ -423,7 +422,7 @@ FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */; }; - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; platformFilter = ios; }; + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; platformFilter = ios; }; FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; FD0150542CA24471005B08A1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150532CA24471005B08A1 /* Nimble */; }; FD0150582CA27DF3005B08A1 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */; }; @@ -739,7 +738,7 @@ FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EE2C2A641200762359 /* DifferenceKit */; }; @@ -1057,7 +1056,7 @@ FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */; }; FDE754DF2C9BAF8A002A2623 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D92C9BAF89002A2623 /* KeyPair.swift */; }; FDE754E02C9BAF8A002A2623 /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754DA2C9BAF8A002A2623 /* Hex.swift */; }; - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */; }; + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */; }; FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E42C9BB012002A2623 /* BezierPathView.swift */; }; FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E62C9BB051002A2623 /* OWSViewController.swift */; }; FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */; }; @@ -1358,7 +1357,7 @@ files = ( C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */, C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */, - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */, + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */, C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */, C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */, ); @@ -1742,13 +1741,12 @@ C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3ADC66026426688005F1414 /* ShareNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareNavController.swift; sourceTree = ""; }; C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = ""; }; - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = ""; }; + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionNetworkingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionNetworkingKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SSK.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2044,7 +2042,7 @@ FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionNetworkingKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; @@ -2207,7 +2205,7 @@ FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateDeleteMemberContentMessage.swift; sourceTree = ""; }; FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Groups.swift"; sourceTree = ""; }; FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreparedRequest+Sending.swift"; sourceTree = ""; }; - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionSnodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionNetworkingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSendingSpec.swift; sourceTree = ""; }; FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; @@ -2324,7 +2322,7 @@ FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = ""; }; FDE754D92C9BAF89002A2623 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; FDE754DA2C9BAF8A002A2623 /* Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionSnodeKit.swift"; sourceTree = ""; }; + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE754E42C9BB012002A2623 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; FDE754E62C9BB051002A2623 /* OWSViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = ""; }; FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+Attachments.swift"; sourceTree = ""; }; @@ -2436,7 +2434,7 @@ buildActionMask = 2147483647; files = ( FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */, - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */, + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */, B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */, C3D90A5C25773A25002C9DF5 /* SessionUtilitiesKit.framework in Frameworks */, C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */, @@ -2450,7 +2448,7 @@ files = ( B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */, FD8A5B182DBF47E9004C689B /* SessionUIKit.framework in Frameworks */, - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */, + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */, B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */, C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */, ); @@ -2473,7 +2471,7 @@ FD6A39222C2AA91D00762359 /* NVActivityIndicatorView in Frameworks */, FD22866F2C38D42300BC06F7 /* DifferenceKit in Frameworks */, C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */, - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */, + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */, C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2508,7 +2506,7 @@ FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */, FD2286732C38D43900BC06F7 /* DifferenceKit in Frameworks */, FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */, + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */, FD6A39132C2A946A00762359 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2522,7 +2520,7 @@ B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */, B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */, FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */, - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */, + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */, C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */, 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, @@ -2561,7 +2559,7 @@ files = ( FD0150542CA24471005B08A1 /* Nimble in Frameworks */, FD0150522CA2446D005B08A1 /* Quick in Frameworks */, - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */, + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3668,27 +3666,27 @@ path = Utilities; sourceTree = ""; }; - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXGroup; children = ( - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, C3C2A5B0255385C700C340D1 /* Meta */, FDE754E22C9BAFF4002A2623 /* Crypto */, FD17D79D27F40CAA00122BE0 /* Database */, + FD7F74682BAB8A5D006DDFD8 /* LibSession */, FDF8489929405C5A007DCAE5 /* Models */, - FDF8488F29405C13007DCAE5 /* Types */, + 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, FD2272842C33E28D004D8A6C /* SnodeAPI */, - FD7F74682BAB8A5D006DDFD8 /* LibSession */, + FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, ); - path = SessionSnodeKit; + path = SessionNetworkingKit; sourceTree = ""; }; C3C2A5B0255385C700C340D1 /* Meta */ = { isa = PBXGroup; children = ( - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */, + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */, C3C2A5A2255385C100C340D1 /* Info.plist */, ); path = Meta; @@ -3701,7 +3699,6 @@ FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */, FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */, - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */, FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */, ); path = Utilities; @@ -3858,13 +3855,13 @@ 7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */, C331FF1C2558F9D300070591 /* SessionUIKit */, C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */, FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */, + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FDE7214E287E50D50093DF33 /* Scripts */, D221A08C169C9E5E00537ABF /* Frameworks */, @@ -3879,7 +3876,7 @@ D221A089169C9E5E00537ABF /* Session.app */, 453518681FC635DD00210559 /* SessionShareExtension.appex */, 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */, - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */, + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */, C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */, C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */, C331FF1B2558F9D300070591 /* SessionUIKit.framework */, @@ -3887,7 +3884,7 @@ FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */, FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, FD71160928D00BAE00B47552 /* SessionTests.xctest */, - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */, + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -4749,15 +4746,15 @@ path = "Group Update Messages"; sourceTree = ""; }; - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */ = { + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */ = { isa = PBXGroup; children = ( - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */, + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */, FD3765DD2AD8F02300DC1489 /* _TestUtilities */, FDAA16792AC28E2200DDBF77 /* Models */, FD2272C52C34E9D1004D8A6C /* Types */, ); - path = SessionSnodeKitTests; + path = SessionNetworkingKitTests; sourceTree = ""; }; FDC13D4E2A16EE41007267C7 /* Types */ = { @@ -4963,7 +4960,7 @@ FDE754E22C9BAFF4002A2623 /* Crypto */ = { isa = PBXGroup; children = ( - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */, + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */, ); path = Crypto; sourceTree = ""; @@ -5146,7 +5143,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */, + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5267,9 +5264,9 @@ productReference = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; productType = "com.apple.product-type.framework"; }; - C3C2A59E255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXNativeTarget; - buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */; + buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */; buildPhases = ( C3C2A59A255385C100C340D1 /* Headers */, C3C2A59B255385C100C340D1 /* Sources */, @@ -5282,12 +5279,12 @@ FDB348822BE86A4400B716C2 /* PBXTargetDependency */, FD7F74622BAAA4C7006DDFD8 /* PBXTargetDependency */, ); - name = SessionSnodeKit; + name = SessionNetworkingKit; packageProductDependencies = ( FD6673F72D7021F200041530 /* SessionUtil */, ); productName = SessionSnodeKit; - productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; + productReference = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; productType = "com.apple.product-type.framework"; }; C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = { @@ -5425,9 +5422,9 @@ productReference = FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */ = { + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */; + buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */; buildPhases = ( FDB5DAF62A981C42002C8721 /* Sources */, FD01504F2CA2445E005B08A1 /* Frameworks */, @@ -5438,9 +5435,9 @@ dependencies = ( FDB5DB002A981C43002C8721 /* PBXTargetDependency */, ); - name = SessionSnodeKitTests; + name = SessionNetworkingKitTests; productName = SessionSnodeKitTests; - productReference = FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */; + productReference = FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */ = { @@ -5611,11 +5608,11 @@ C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1A2558F9D300070591 /* SessionUIKit */, C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, - C3C2A59E255385C100C340D1 /* SessionSnodeKit */, + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */, C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, FD71160828D00BAE00B47552 /* SessionTests */, FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */, + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, ); }; @@ -5760,7 +5757,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */, + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6202,7 +6199,6 @@ FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */, FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, @@ -6224,7 +6220,7 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */, - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */, + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */, FD2272AD2C33E337004D8A6C /* Network.swift in Sources */, FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */, FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, @@ -7121,7 +7117,7 @@ }; B8D64FB825BA78270029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FB725BA78270029CFC0 /* PBXContainerItemProxy */; }; B8D64FBA25BA78270029CFC0 /* PBXTargetDependency */ = { @@ -7136,7 +7132,7 @@ }; B8D64FC425BA784A0029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FC325BA784A0029CFC0 /* PBXContainerItemProxy */; }; B8D64FC625BA784A0029CFC0 /* PBXTargetDependency */ = { @@ -7156,7 +7152,7 @@ }; C3C2A5A5255385C100C340D1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */; }; C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = { @@ -7218,7 +7214,7 @@ FDB5DB002A981C43002C8721 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = FDB5DAFF2A981C43002C8721 /* PBXContainerItemProxy */; }; FDC4389427B9FFC700C60D73 /* PBXTargetDependency */ = { @@ -7773,7 +7769,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7784,7 +7780,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -7846,7 +7842,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7857,7 +7853,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -8382,8 +8378,8 @@ ENABLE_MODULE_VERIFIER = NO; GCC_DYNAMIC_NO_PIC = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug; }; @@ -8392,8 +8388,8 @@ buildSettings = { ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release; }; @@ -8581,8 +8577,8 @@ isa = XCBuildConfiguration; buildSettings = { GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug_Compile_LibSession; }; @@ -8590,8 +8586,8 @@ isa = XCBuildConfiguration; buildSettings = { GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release_Compile_LibSession; }; @@ -9103,7 +9099,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9114,7 +9110,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -9822,7 +9818,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9833,7 +9829,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -10139,7 +10135,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */ = { + C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */ = { isa = XCConfigurationList; buildConfigurations = ( C3C2A5A8255385C100C340D1 /* Debug */, @@ -10194,7 +10190,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */ = { + FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( FD2272502C32910F004D8A6C /* Debug */, diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index ad1576e055..e20af5e4f5 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -77,8 +77,8 @@ @@ -55,8 +55,8 @@ diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index d747fbcdfd..f08656626c 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -9,7 +9,7 @@ import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { private let dependencies: Dependencies diff --git a/Session/Calls/WebRTC/WebRTCSession.swift b/Session/Calls/WebRTC/WebRTCSession.swift index 99e48d1e25..d579d8a1a5 100644 --- a/Session/Calls/WebRTC/WebRTCSession.swift +++ b/Session/Calls/WebRTC/WebRTCSession.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import WebRTC -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 01916f533c..8309d70282 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -5,7 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 54ae63cbc3..de93e57637 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -3,7 +3,7 @@ import UIKit import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension ContextMenuVC { final class ActionView: UIView { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 62baf8fb45..c7021b4186 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -14,7 +14,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit import SwiftUI -import SessionSnodeKit +import SessionNetworkingKit extension ConversationVC: InputViewDelegate, diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3c8ede79ed..fe501f887c 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -6,7 +6,7 @@ import UniformTypeIdentifiers import Lucide import GRDB import DifferenceKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SessionUIKit diff --git a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift index b0f17cc31e..b835fe5e86 100644 --- a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift +++ b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import UIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit class DisappearingMessageTimerView: UIView { diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index b7df60ec3c..7ef4514e03 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { typealias TableItem = String diff --git a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift index e89a98303b..f5b51cf455 100644 --- a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadNotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index c2462e2193..5045825fb5 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index c478eb8331..725fa06ed8 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -5,7 +5,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index bf01d591f7..b8a69f704f 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -4,7 +4,7 @@ import UIKit import Combine import UniformTypeIdentifiers import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 2e9b72936b..eb2cd33430 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -5,7 +5,7 @@ import Combine import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 51523207fb..f00db791d2 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -6,7 +6,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController { class DynamicallySizedView: UIView { diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index e614b81b5f..f4c447dd46 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -2,7 +2,7 @@ import SwiftUI import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index d4e1bc8d2d..30b38d6a73 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -6,7 +6,7 @@ import Foundation import Combine import AVFoundation import CoreServices -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 46cafadd53..eea5572f6d 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -8,7 +8,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift index 862f7a9eb3..2c7a2aedfe 100644 --- a/Session/Meta/Session+SNUIKit.swift +++ b/Session/Meta/Session+SNUIKit.swift @@ -3,7 +3,7 @@ import UIKit import AVFoundation import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SessionSNUIKitConfig diff --git a/Session/Notifications/NotificationActionHandler.swift b/Session/Notifications/NotificationActionHandler.swift index 5c0999824b..083aa94356 100644 --- a/Session/Notifications/NotificationActionHandler.swift +++ b/Session/Notifications/NotificationActionHandler.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 6a885dfd2c..7cf9efb5a4 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 4fffd9fe46..b731acafbd 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 52da3682fa..4ca9d056dd 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Cache diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index d4976596a1..40100bf54e 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 907947605f..33108de585 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -3,7 +3,7 @@ import UIKit import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index ab37d115f3..130224d297 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -5,7 +5,7 @@ import Combine import NVActivityIndicatorView import SessionMessagingKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class PathVC: BaseVC { diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e61c5570c0..d007a2097e 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -8,7 +8,7 @@ import Compression import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 4ade2b621d..d4162bb02c 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift index 2446f852f2..d7795ea74f 100644 --- a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift +++ b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index bb30cb2599..b03942a719 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 8ef98ce322..56b9102bba 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index bea6331051..8621e3a2fa 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -4,7 +4,7 @@ import Foundation import CryptoKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift index bfcdbea5d3..b9b3e31588 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` diff --git a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift b/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift index 07db8962d8..c5c4c4a4d8 100644 --- a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift +++ b/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration used to remove the legacy YapDatabase files (the old logic has been removed and is no longer supported so it now does nothing) enum _004_RemoveLegacyYDB: Migration { diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift index 3f60a24d4a..d3ad1f25be 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift @@ -5,7 +5,7 @@ import Foundation import UIKit.UIImage import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit enum _022_GroupsRebuildChanges: Migration { diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 3f1f5854d9..a5a8279dff 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index e525398357..b2a680ed7a 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -5,7 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ClosedGroup: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/ConfigDump.swift b/SessionMessagingKit/Database/Models/ConfigDump.swift index b264051429..0b429793ad 100644 --- a/SessionMessagingKit/Database/Models/ConfigDump.swift +++ b/SessionMessagingKit/Database/Models/ConfigDump.swift @@ -4,7 +4,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 22c7d9a580..d9f804ded0 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -5,7 +5,7 @@ import GRDB import SessionUIKit import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "disappearingMessagesConfiguration" } diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 59373a3201..47590f8d3a 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct Interaction: Codable, Identifiable, Equatable, Hashable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "interaction" } diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index faa84197cc..d0a32ca8db 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "linkPreview" } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 3e069e9268..1684d94c27 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct SessionThread: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, IdentifiableTableRecord { public static var databaseTableName: String { "thread" } diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index 0127ebc746..b52dbe95d5 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum AttachmentDownloadJob: JobExecutor { public static var maxFailureCount: Int = 3 diff --git a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift index d1dc85ddea..a3cd20c22a 100644 --- a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index aba703204e..45022e0421 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index 5720b9acfb..f0ba83de12 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index dc37ac0c02..136b736815 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift index 0f7e16f812..31a09b3b8a 100644 --- a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index ec9ee87bc3..c5257be82b 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift index 0ffd051f5a..03e602be04 100644 --- a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum ExpirationUpdateJob: JobExecutor { public static var maxFailureCount: Int = -1 diff --git a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift index b799931f85..2d2b0f87ce 100644 --- a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index bf15ef97b7..a70bfc5d0a 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum GetExpirationJob: JobExecutor { diff --git a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift index b40827c6e8..e652f75f13 100644 --- a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 475a5ea13d..92448ad51b 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift index 10a934451b..d46639ac78 100644 --- a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 590a660a46..49948e7bac 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift index 3decb55cf9..096d6729ff 100644 --- a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift +++ b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift @@ -6,7 +6,7 @@ import GRDB import SessionUtil import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift index 1da0c5a95c..9196301bfc 100644 --- a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum SendReadReceiptsJob: JobExecutor { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index ee77869c1e..eec8fbd631 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 51215e9344..8774ca003d 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 522957def1..ce4d1986dd 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift index 54908b4470..64a1f5260a 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 27f7bf7fdf..c9724f15b1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index f90e4f37b6..be68896f71 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift index 52c8057ef5..7d503d57f0 100644 --- a/SessionMessagingKit/LibSession/Types/Config.swift +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension LibSession { diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index b7d393a02c..e6e7a98934 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { diff --git a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift index d511a374fc..4212f74901 100644 --- a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift +++ b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtilitiesKit diff --git a/SessionMessagingKit/Messages/Message+Origin.swift b/SessionMessagingKit/Messages/Message+Origin.swift index 4da490c326..540c6eb5d5 100644 --- a/SessionMessagingKit/Messages/Message+Origin.swift +++ b/SessionMessagingKit/Messages/Message+Origin.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index c640978d0d..1f775614c2 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// Abstract base class for `VisibleMessage` and `ControlMessage`. diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 219021f442..f50966a7ac 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension OpenGroupAPI { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 78b97e3f08..9f80cbe964 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum OpenGroupAPI { diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4c6d534abe..9cd978e457 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton diff --git a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift index 0b3dbc54ab..29189d3cef 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit public extension HTTPHeader { static let sogsPubKey: HTTPHeader = "X-SOGS-Pubkey" diff --git a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift index a9af9824ad..4eb7f6c206 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit public extension HTTPQueryParam { static let publicKey: HTTPQueryParam = "public_key" diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 6242a05447..5c8d72187f 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: Request - OpenGroupAPI diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 9da8faf919..e5e0b9bd34 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension OpenGroupAPI { public enum Endpoint: EndpointType { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 9670210fdc..cdd2921bcf 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -5,7 +5,7 @@ import AVFAudio import GRDB import WebRTC import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift index 4186c65e0b..4c5f597204 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 95a548578b..e771404d9a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift index 8396652b28..ec2ecadc46 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleNewLegacyClosedGroup( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift index acfe478541..f209de30ed 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index b5d2893a4d..1ad39c5a05 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleMessageRequestResponse( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index ae75d068a5..a30f849c81 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index e096a96a9d..6c03cc8ad9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 58caa31955..c22b57d576 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageSender { private typealias PreparedGroupData = ( diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 059032f9c6..13a32bb3dd 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index bb681dd1b5..507a201df1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageSender { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 510819e3f4..a31872dccb 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift index 663bafb174..f29520550b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension PushNotificationAPI { struct LegacyUnsubscribeRequest: Codable { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift index fefdbad9de..2267c9e130 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension PushNotificationAPI { public struct NotificationMetadata: Codable, Equatable { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift index 5138d5d8f5..971b969560 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift index 1d29c882d8..4f4b7da70c 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 5c4bcd9c7c..2b4201f5a1 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - KeychainStorage diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift index 36ed02e3e2..a5d92afb4c 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index 78000ad2ce..c63988f5d1 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: Request - PushNotificationAPI diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 835329ec9c..39b2d0956f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index e9290f1800..804ed95182 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 667ec2909c..1ec82b4124 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 8cdacd5c99..91ef8cf1b9 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -4,7 +4,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index b632d6d87d..2f3ca1906a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SwarmPollerType diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index 3f5da3a998..3486db29e6 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 05553088ed..89d21fdc30 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension MessageViewModel { diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 4a1069c1ca..745e1e4418 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Authentication Types diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index f544f6eb3e..a96b8634fc 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Utilities/MessageWrapper.swift b/SessionMessagingKit/Utilities/MessageWrapper.swift index 1e7f97ba3c..3ff9796b7b 100644 --- a/SessionMessagingKit/Utilities/MessageWrapper.swift +++ b/SessionMessagingKit/Utilities/MessageWrapper.swift @@ -1,7 +1,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum MessageWrapper { diff --git a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift index 98d331cd1d..ed46bc066c 100644 --- a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift +++ b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension SNProtoEnvelope { diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 0ca3ed5345..85d0809b20 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 39d2bff89d..a226aab1df 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index f9de94bac5..07bda1e05f 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -4,12 +4,12 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupInfoSpec: QuickSpec { @@ -31,7 +31,7 @@ class LibSessionGroupInfoSpec: QuickSpec { migrationTargets: [ SNUtilitiesKit.self, SNMessagingKit.self, - SNSnodeKit.self + SNNetworkingKit.self ], using: dependencies, initialData: { db in diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 5e2884cd3e..8001ede737 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupMembersSpec: QuickSpec { diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index d135064fcf..f23341efdf 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionSpec: QuickSpec { diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index db9aa0df35..ee3f4d94ac 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index fa13eeea02..73ae2a3cc6 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 556a2adee0..24635b39af 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift index 88e568f21b..b9e37aca22 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index debc33045b..be86b9ed76 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -9,7 +9,7 @@ import SessionUtil import SessionUtilitiesKit import SessionUIKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class MessageReceiverGroupsSpec: QuickSpec { @@ -31,7 +31,7 @@ class MessageReceiverGroupsSpec: QuickSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self ], using: dependencies, diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 1bd4a1b614..2f55e63806 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -10,7 +10,7 @@ import Quick import Nimble @testable import SessionMessagingKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MessageSenderGroupsSpec: QuickSpec { override class func spec() { diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index 5e67562442..6ddb79b9bf 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index 742f51d783..6f966ebf9c 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -3,7 +3,7 @@ import UIKit import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index b3f044e3bd..794fa81829 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift index 43ab428f96..d20f1da875 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionSnodeKit/Configuration.swift b/SessionNetworkingKit/Configuration.swift similarity index 89% rename from SessionSnodeKit/Configuration.swift rename to SessionNetworkingKit/Configuration.swift index af6ed9cc8d..18a73d2615 100644 --- a/SessionSnodeKit/Configuration.swift +++ b/SessionNetworkingKit/Configuration.swift @@ -4,10 +4,10 @@ import Foundation import GRDB import SessionUtilitiesKit -public enum SNSnodeKit: MigratableTarget { // Just to make the external API nice +public enum SNNetworkingKit: MigratableTarget { // Just to make the external API nice public static func migrations() -> TargetMigrations { return TargetMigrations( - identifier: .snodeKit, + identifier: .networkingKit, migrations: [ [ _001_InitialSetupMigration.self, diff --git a/SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift similarity index 100% rename from SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift rename to SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift diff --git a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift similarity index 96% rename from SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift index 02f160fe0c..9473c323f8 100644 --- a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -7,7 +7,7 @@ import GRDB import SessionUtilitiesKit enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift similarity index 96% rename from SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift index e92355cc9e..845a42fe16 100644 --- a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift similarity index 88% rename from SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 4e826bc308..382874faf3 100644 --- a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -5,7 +5,7 @@ import GRDB import SessionUtilitiesKit enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift b/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift similarity index 93% rename from SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift rename to SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift index 486665167c..989df981c8 100644 --- a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift +++ b/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit /// ignore their hashes when subsequently trying to fetch new messages (which results in the storage server returning /// messages from the beginning of time) enum _004_FlagMessageHashAsDeletedOrInvalid: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift b/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift similarity index 97% rename from SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift rename to SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift index acc361590d..6565fc40d1 100644 --- a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift +++ b/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration adds a primary key to `SnodeReceivedMessageInfo` based on the key and hash to speed up lookup enum _005_AddSnodeReveivedMessageInfoPrimaryKey: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "AddSnodeReveivedMessageInfoPrimaryKey" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift b/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift similarity index 94% rename from SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift rename to SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift index b2a3d41bd2..6cc16ea4ef 100644 --- a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift +++ b/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration drops the current `SnodePool` and `SnodeSet` and their associated jobs as they are handled by `libSession` now enum _006_DropSnodeCache: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "DropSnodeCache" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift similarity index 98% rename from SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift rename to SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift index aa74f45ff4..779eecee5e 100644 --- a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift +++ b/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration splits the old `key` structure used for `SnodeReceivedMessageInfo` into separate columns for more efficient querying enum _007_SplitSnodeReceivedMessageInfo: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "SplitSnodeReceivedMessageInfo" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift b/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift similarity index 94% rename from SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift rename to SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift index 468ee6999c..1eb3e6d265 100644 --- a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift +++ b/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit /// This migration resets the `lastHash` value for all user config namespaces to force the app to fetch the latest config /// messages in case there are multi-part config message we had previously seen and failed to merge enum _008_ResetUserConfigLastHashes: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "ResetUserConfigLastHashes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift similarity index 100% rename from SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift rename to SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift similarity index 100% rename from SessionSnodeKit/LibSession/LibSession+Networking.swift rename to SessionNetworkingKit/LibSession/LibSession+Networking.swift diff --git a/SessionSnodeKit/Meta/Info.plist b/SessionNetworkingKit/Meta/Info.plist similarity index 100% rename from SessionSnodeKit/Meta/Info.plist rename to SessionNetworkingKit/Meta/Info.plist diff --git a/SessionNetworkingKit/Meta/SessionNetworkingKit.h b/SessionNetworkingKit/Meta/SessionNetworkingKit.h new file mode 100644 index 0000000000..dd9ec08864 --- /dev/null +++ b/SessionNetworkingKit/Meta/SessionNetworkingKit.h @@ -0,0 +1,4 @@ +#import + +FOUNDATION_EXPORT double SessionNetworkingKitVersionNumber; +FOUNDATION_EXPORT const unsigned char SessionNetworkingKitVersionString[]; diff --git a/SessionSnodeKit/Models/AppVersionResponse.swift b/SessionNetworkingKit/Models/AppVersionResponse.swift similarity index 100% rename from SessionSnodeKit/Models/AppVersionResponse.swift rename to SessionNetworkingKit/Models/AppVersionResponse.swift diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllBeforeRequest.swift rename to SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift diff --git a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift b/SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllBeforeResponse.swift rename to SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllMessagesRequest.swift rename to SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift diff --git a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift b/SessionNetworkingKit/Models/DeleteAllMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllMessagesResponse.swift rename to SessionNetworkingKit/Models/DeleteAllMessagesResponse.swift diff --git a/SessionSnodeKit/Models/DeleteMessagesRequest.swift b/SessionNetworkingKit/Models/DeleteMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteMessagesRequest.swift rename to SessionNetworkingKit/Models/DeleteMessagesRequest.swift diff --git a/SessionSnodeKit/Models/DeleteMessagesResponse.swift b/SessionNetworkingKit/Models/DeleteMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteMessagesResponse.swift rename to SessionNetworkingKit/Models/DeleteMessagesResponse.swift diff --git a/SessionSnodeKit/Models/FileUploadResponse.swift b/SessionNetworkingKit/Models/FileUploadResponse.swift similarity index 100% rename from SessionSnodeKit/Models/FileUploadResponse.swift rename to SessionNetworkingKit/Models/FileUploadResponse.swift diff --git a/SessionSnodeKit/Models/GetExpiriesRequest.swift b/SessionNetworkingKit/Models/GetExpiriesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/GetExpiriesRequest.swift rename to SessionNetworkingKit/Models/GetExpiriesRequest.swift diff --git a/SessionSnodeKit/Models/GetExpiriesResponse.swift b/SessionNetworkingKit/Models/GetExpiriesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetExpiriesResponse.swift rename to SessionNetworkingKit/Models/GetExpiriesResponse.swift diff --git a/SessionSnodeKit/Models/GetMessagesRequest.swift b/SessionNetworkingKit/Models/GetMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/GetMessagesRequest.swift rename to SessionNetworkingKit/Models/GetMessagesRequest.swift diff --git a/SessionSnodeKit/Models/GetMessagesResponse.swift b/SessionNetworkingKit/Models/GetMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetMessagesResponse.swift rename to SessionNetworkingKit/Models/GetMessagesResponse.swift diff --git a/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift b/SessionNetworkingKit/Models/GetNetworkTimestampResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetNetworkTimestampResponse.swift rename to SessionNetworkingKit/Models/GetNetworkTimestampResponse.swift diff --git a/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift b/SessionNetworkingKit/Models/LegacyGetMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/LegacyGetMessagesRequest.swift rename to SessionNetworkingKit/Models/LegacyGetMessagesRequest.swift diff --git a/SessionSnodeKit/Models/LegacySendMessageRequest.swift b/SessionNetworkingKit/Models/LegacySendMessageRequest.swift similarity index 100% rename from SessionSnodeKit/Models/LegacySendMessageRequest.swift rename to SessionNetworkingKit/Models/LegacySendMessageRequest.swift diff --git a/SessionSnodeKit/Models/ONSResolveRequest.swift b/SessionNetworkingKit/Models/ONSResolveRequest.swift similarity index 100% rename from SessionSnodeKit/Models/ONSResolveRequest.swift rename to SessionNetworkingKit/Models/ONSResolveRequest.swift diff --git a/SessionSnodeKit/Models/ONSResolveResponse.swift b/SessionNetworkingKit/Models/ONSResolveResponse.swift similarity index 100% rename from SessionSnodeKit/Models/ONSResolveResponse.swift rename to SessionNetworkingKit/Models/ONSResolveResponse.swift diff --git a/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift b/SessionNetworkingKit/Models/OxenDaemonRPCRequest.swift similarity index 100% rename from SessionSnodeKit/Models/OxenDaemonRPCRequest.swift rename to SessionNetworkingKit/Models/OxenDaemonRPCRequest.swift diff --git a/SessionSnodeKit/Models/RevokeSubaccountRequest.swift b/SessionNetworkingKit/Models/RevokeSubaccountRequest.swift similarity index 100% rename from SessionSnodeKit/Models/RevokeSubaccountRequest.swift rename to SessionNetworkingKit/Models/RevokeSubaccountRequest.swift diff --git a/SessionSnodeKit/Models/RevokeSubaccountResponse.swift b/SessionNetworkingKit/Models/RevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/RevokeSubaccountResponse.swift rename to SessionNetworkingKit/Models/RevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/SendMessageRequest.swift b/SessionNetworkingKit/Models/SendMessageRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SendMessageRequest.swift rename to SessionNetworkingKit/Models/SendMessageRequest.swift diff --git a/SessionSnodeKit/Models/SendMessageResponse.swift b/SessionNetworkingKit/Models/SendMessageResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SendMessageResponse.swift rename to SessionNetworkingKit/Models/SendMessageResponse.swift diff --git a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift b/SessionNetworkingKit/Models/SnodeAuthenticatedRequestBody.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift rename to SessionNetworkingKit/Models/SnodeAuthenticatedRequestBody.swift diff --git a/SessionSnodeKit/Models/SnodeBatchRequest.swift b/SessionNetworkingKit/Models/SnodeBatchRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeBatchRequest.swift rename to SessionNetworkingKit/Models/SnodeBatchRequest.swift diff --git a/SessionSnodeKit/Models/SnodeMessage.swift b/SessionNetworkingKit/Models/SnodeMessage.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeMessage.swift rename to SessionNetworkingKit/Models/SnodeMessage.swift diff --git a/SessionSnodeKit/Models/SnodeReceivedMessage.swift b/SessionNetworkingKit/Models/SnodeReceivedMessage.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeReceivedMessage.swift rename to SessionNetworkingKit/Models/SnodeReceivedMessage.swift diff --git a/SessionSnodeKit/Models/SnodeRecursiveResponse.swift b/SessionNetworkingKit/Models/SnodeRecursiveResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeRecursiveResponse.swift rename to SessionNetworkingKit/Models/SnodeRecursiveResponse.swift diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionNetworkingKit/Models/SnodeRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeRequest.swift rename to SessionNetworkingKit/Models/SnodeRequest.swift diff --git a/SessionSnodeKit/Models/SnodeResponse.swift b/SessionNetworkingKit/Models/SnodeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeResponse.swift rename to SessionNetworkingKit/Models/SnodeResponse.swift diff --git a/SessionSnodeKit/Models/SnodeSwarmItem.swift b/SessionNetworkingKit/Models/SnodeSwarmItem.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeSwarmItem.swift rename to SessionNetworkingKit/Models/SnodeSwarmItem.swift diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift b/SessionNetworkingKit/Models/UnrevokeSubaccountRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift rename to SessionNetworkingKit/Models/UnrevokeSubaccountRequest.swift diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift b/SessionNetworkingKit/Models/UnrevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift rename to SessionNetworkingKit/Models/UnrevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift b/SessionNetworkingKit/Models/UpdateExpiryAllRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryAllRequest.swift rename to SessionNetworkingKit/Models/UpdateExpiryAllRequest.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift b/SessionNetworkingKit/Models/UpdateExpiryAllResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryAllResponse.swift rename to SessionNetworkingKit/Models/UpdateExpiryAllResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryRequest.swift b/SessionNetworkingKit/Models/UpdateExpiryRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryRequest.swift rename to SessionNetworkingKit/Models/UpdateExpiryRequest.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryResponse.swift b/SessionNetworkingKit/Models/UpdateExpiryResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryResponse.swift rename to SessionNetworkingKit/Models/UpdateExpiryResponse.swift diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionNetworkingKit/Networking/SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/Networking/SnodeAPI.swift rename to SessionNetworkingKit/Networking/SnodeAPI.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift rename to SessionNetworkingKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift similarity index 96% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift index fb26688ac1..79feb74a36 100644 --- a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -21,7 +21,7 @@ extension SessionNetworkAPI { public func initialize(using dependencies: Dependencies) { self.dependencies = dependencies cancellable = getInfo(using: dependencies) - .subscribe(on: Threading.workQueue, using: dependencies) + .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) .receive(on: SessionNetworkAPI.workQueue) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } @@ -32,7 +32,7 @@ extension SessionNetworkAPI { let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { return Just(()) - .delay(for: .milliseconds(500), scheduler: Threading.workQueue) + .delay(for: .milliseconds(500), scheduler: SessionNetworkAPI.workQueue) .setFailureType(to: Error.self) .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/ResponseInfo+SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/ResponseInfo+SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPIEndpoint.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPIEndpoint.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIError.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPIError.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPINamespace.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPINamespace.swift diff --git a/SessionSnodeKit/Types/BatchRequest.swift b/SessionNetworkingKit/Types/BatchRequest.swift similarity index 100% rename from SessionSnodeKit/Types/BatchRequest.swift rename to SessionNetworkingKit/Types/BatchRequest.swift diff --git a/SessionSnodeKit/Types/BatchResponse.swift b/SessionNetworkingKit/Types/BatchResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BatchResponse.swift rename to SessionNetworkingKit/Types/BatchResponse.swift diff --git a/SessionSnodeKit/Types/BencodeResponse.swift b/SessionNetworkingKit/Types/BencodeResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BencodeResponse.swift rename to SessionNetworkingKit/Types/BencodeResponse.swift diff --git a/SessionSnodeKit/Types/ContentProxy.swift b/SessionNetworkingKit/Types/ContentProxy.swift similarity index 100% rename from SessionSnodeKit/Types/ContentProxy.swift rename to SessionNetworkingKit/Types/ContentProxy.swift diff --git a/SessionSnodeKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift similarity index 100% rename from SessionSnodeKit/Types/Destination.swift rename to SessionNetworkingKit/Types/Destination.swift diff --git a/SessionSnodeKit/Types/HTTPHeader.swift b/SessionNetworkingKit/Types/HTTPHeader.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPHeader.swift rename to SessionNetworkingKit/Types/HTTPHeader.swift diff --git a/SessionSnodeKit/Types/HTTPMethod.swift b/SessionNetworkingKit/Types/HTTPMethod.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPMethod.swift rename to SessionNetworkingKit/Types/HTTPMethod.swift diff --git a/SessionSnodeKit/Types/HTTPQueryParam.swift b/SessionNetworkingKit/Types/HTTPQueryParam.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPQueryParam.swift rename to SessionNetworkingKit/Types/HTTPQueryParam.swift diff --git a/SessionSnodeKit/Types/IPv4.swift b/SessionNetworkingKit/Types/IPv4.swift similarity index 100% rename from SessionSnodeKit/Types/IPv4.swift rename to SessionNetworkingKit/Types/IPv4.swift diff --git a/SessionSnodeKit/Types/JSON.swift b/SessionNetworkingKit/Types/JSON.swift similarity index 100% rename from SessionSnodeKit/Types/JSON.swift rename to SessionNetworkingKit/Types/JSON.swift diff --git a/SessionSnodeKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift similarity index 100% rename from SessionSnodeKit/Types/Network.swift rename to SessionNetworkingKit/Types/Network.swift diff --git a/SessionSnodeKit/Types/NetworkError.swift b/SessionNetworkingKit/Types/NetworkError.swift similarity index 100% rename from SessionSnodeKit/Types/NetworkError.swift rename to SessionNetworkingKit/Types/NetworkError.swift diff --git a/SessionSnodeKit/Types/PreparedRequest+Sending.swift b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest+Sending.swift rename to SessionNetworkingKit/Types/PreparedRequest+Sending.swift diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest.swift rename to SessionNetworkingKit/Types/PreparedRequest.swift diff --git a/SessionSnodeKit/Types/ProxiedContentDownloader.swift b/SessionNetworkingKit/Types/ProxiedContentDownloader.swift similarity index 100% rename from SessionSnodeKit/Types/ProxiedContentDownloader.swift rename to SessionNetworkingKit/Types/ProxiedContentDownloader.swift diff --git a/SessionSnodeKit/Types/Request.swift b/SessionNetworkingKit/Types/Request.swift similarity index 100% rename from SessionSnodeKit/Types/Request.swift rename to SessionNetworkingKit/Types/Request.swift diff --git a/SessionNetworkingKit/Types/RequestCategory.swift b/SessionNetworkingKit/Types/RequestCategory.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/SessionSnodeKit/Types/ResponseInfo.swift b/SessionNetworkingKit/Types/ResponseInfo.swift similarity index 100% rename from SessionSnodeKit/Types/ResponseInfo.swift rename to SessionNetworkingKit/Types/ResponseInfo.swift diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift new file mode 100644 index 0000000000..464a3b1b9e --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift @@ -0,0 +1,9 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +public extension HTTPHeader { + static let tokenServerPubKey: HTTPHeader = "X-FS-Pubkey" + static let tokenServerTimestamp: HTTPHeader = "X-FS-Timestamp" + static let tokenServerSignature: HTTPHeader = "X-FS-Signature" +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift new file mode 100644 index 0000000000..9316b1a440 --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift @@ -0,0 +1,32 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import AudioToolbox +import GRDB +import DifferenceKit +import SessionUtilitiesKit + +public extension KeyValueStore.StringKey { + static let contractAddress: KeyValueStore.StringKey = "contractAddress" +} + +public extension KeyValueStore.DoubleKey { + static let tokenUsd: KeyValueStore.DoubleKey = "tokenUsd" + static let marketCapUsd: KeyValueStore.DoubleKey = "marketCapUsd" + static let stakingRequirement: KeyValueStore.DoubleKey = "stakingRequirement" + static let stakingRewardPool: KeyValueStore.DoubleKey = "stakingRewardPool" + static let networkStakedTokens: KeyValueStore.DoubleKey = "networkStakedTokens" + static let networkStakedUSD: KeyValueStore.DoubleKey = "networkStakedUSD" +} + +public extension KeyValueStore.IntKey { + static let networkSize: KeyValueStore.IntKey = "networkSize" +} + +public extension KeyValueStore.Int64Key { + static let lastUpdatedTimestampMs: KeyValueStore.Int64Key = "lastUpdatedTimestampMs" + static let staleTimestampMs: KeyValueStore.Int64Key = "staleTimestampMs" + static let priceTimestampMs: KeyValueStore.Int64Key = "priceTimestampMs" +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift new file mode 100644 index 0000000000..cd1cfec84c --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift @@ -0,0 +1,75 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +extension SessionNetworkAPI { + + // MARK: - Price + + public struct Price: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case tokenUsd = "usd" + case marketCapUsd = "usd_market_cap" + case priceTimestamp = "t_price" + case staleTimestamp = "t_stale" + } + + public let tokenUsd: Double? // Current token price (USD) + public let marketCapUsd: Double? // Current market cap value in (USD) + public let priceTimestamp: Int64? // The timestamp the price data is accurate at. (seconds) + public let staleTimestamp: Int64? // Stale timestamp for the price data. (seconds) + } + + // MARK: - Token + + public struct Token: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case stakingRequirement = "staking_requirement" + case stakingRewardPool = "staking_reward_pool" + case contractAddress = "contract_address" + } + + public let stakingRequirement: Double? // The number of tokens required to stake a node. This is the effective "token amount" per node (SESH) + public let stakingRewardPool: Double? // The number of tokens in the staking reward pool (SESH) + public let contractAddress: String? // Token contract address (42 char Hexadecimal - Including 0x prefix) + } + + + // MARK: - Network Info + + public struct NetworkInfo: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case networkSize = "network_size" // The number of nodes in the Session Network (integer) + case networkStakedTokens = "network_staked_tokens" // + case networkStakedUSD = "network_staked_usd" // + } + + public let networkSize: Int? + public let networkStakedTokens: Double? + public let networkStakedUSD: Double? + } + + // MARK: - Info + + public struct Info: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case timestamp = "t" + case statusCode = "status_code" + case price + case token + case network + } + + public let timestamp: Int64? // Request timestamp. (seconds) + public let statusCode: Int? // Status code of the request. + public let price: Price? + public let token: Token? + public let network: NetworkInfo? + } +} + diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift new file mode 100644 index 0000000000..947e55d15d --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -0,0 +1,107 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +// MARK: - Log.Category + +public extension Log.Category { + static let sessionNetwork: Log.Category = .create("SessionNetwork", defaultLevel: .info) +} + +extension SessionNetworkAPI { + public final class HTTPClient { + private var cancellable: AnyCancellable? + private var dependencies: Dependencies? + + public func initialize(using dependencies: Dependencies) { + self.dependencies = dependencies + cancellable = getInfo(using: dependencies) + .subscribe(on: Threading.workQueue, using: dependencies) + .receive(on: SessionNetworkAPI.workQueue) + .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) + } + + public func getInfo(using dependencies: Dependencies) -> AnyPublisher { + cancellable?.cancel() + + let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) + guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { + return Just(()) + .delay(for: .milliseconds(500), scheduler: Threading.workQueue) + .setFailureType(to: Error.self) + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return true + } + .eraseToAnyPublisher() + } + + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + try SessionNetworkAPI + .prepareInfo( + db, + using: dependencies + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _, info in info } + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + // Token info + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + db[.tokenUsd] = info.price?.tokenUsd + db[.marketCapUsd] = info.price?.marketCapUsd + if let priceTimestamp = info.price?.priceTimestamp { + db[.priceTimestampMs] = priceTimestamp * 1000 + } else { + db[.priceTimestampMs] = nil + } + if let staleTimestamp = info.price?.staleTimestamp { + db[.staleTimestampMs] = staleTimestamp * 1000 + } else { + db[.staleTimestampMs] = nil + } + db[.stakingRequirement] = info.token?.stakingRequirement + db[.stakingRewardPool] = info.token?.stakingRewardPool + db[.contractAddress] = info.token?.contractAddress + // Network info + db[.networkSize] = info.network?.networkSize + db[.networkStakedTokens] = info.network?.networkStakedTokens + db[.networkStakedUSD] = info.network?.networkStakedUSD + + return true + } + .catch { error -> AnyPublisher in + Log.error(.sessionNetwork, "Failed to fetch token info due to error: \(error).") + return self.cleanUpSessionNetworkPageData(using: dependencies) + .map { _ in false } + .eraseToAnyPublisher() + + } + .eraseToAnyPublisher() + } + + private func cleanUpSessionNetworkPageData(using dependencies: Dependencies) -> AnyPublisher { + dependencies[singleton: .storage].writePublisher { db in + // Token info + db[.lastUpdatedTimestampMs] = nil + db[.tokenUsd] = nil + db[.marketCapUsd] = nil + db[.priceTimestampMs] = nil + db[.staleTimestampMs] = nil + db[.stakingRequirement] = nil + db[.stakingRewardPool] = nil + db[.contractAddress] = nil + // Network info + db[.networkSize] = nil + db[.networkStakedTokens] = nil + db[.networkStakedUSD] = nil + } + } + } +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift new file mode 100644 index 0000000000..1d1891a51d --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift @@ -0,0 +1,131 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +public enum SessionNetworkAPI { + public static let workQueue = DispatchQueue(label: "SessionNetworkAPI.workQueue", qos: .userInitiated) + public static let client = HTTPClient() + + // MARK: - Info + + /// General token info. This endpoint combines the `/price` and `/token` endpoint information. + /// + /// `GET/info` + + public static func prepareInfo( + _ db: Database, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: Network.NetworkAPI.Endpoint.info, + destination: .server( + method: .get, + server: Network.NetworkAPI.networkAPIServer, + queryParameters: [:], + x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey + ) + ), + responseType: Info.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + .signed(db, with: SessionNetworkAPI.signRequest, using: dependencies) + } + + // MARK: - Authentication + + fileprivate static func signatureHeaders( + _ db: Database, + url: URL, + method: HTTPMethod, + body: Data?, + using dependencies: Dependencies + ) throws -> [HTTPHeader: String] { + let timestamp: UInt64 = UInt64(floor(dependencies.dateNow.timeIntervalSince1970)) + let path: String = url.path + .appending(url.query.map { value in "?\(value)" }) + + let signResult: (publicKey: String, signature: [UInt8]) = try sign( + db, + timestamp: timestamp, + method: method.rawValue, + path: path, + body: body, + using: dependencies + ) + + return [ + HTTPHeader.tokenServerPubKey: signResult.publicKey, + HTTPHeader.tokenServerTimestamp: "\(timestamp)", + HTTPHeader.tokenServerSignature: signResult.signature.toBase64() + ] + } + + private static func sign( + _ db: Database, + timestamp: UInt64, + method: String, + path: String, + body: Data?, + using dependencies: Dependencies + ) throws -> (publicKey: String, signature: [UInt8]) { + let bodyString: String? = { + guard let bodyData: Data = body else { return nil } + return String(data: bodyData, encoding: .utf8) + }() + + guard + let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db), + let blinded07KeyPair: KeyPair = dependencies[singleton: .crypto].generate( + .versionBlinded07KeyPair(ed25519SecretKey: userEdKeyPair.secretKey) + ), + let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( + .signatureVersionBlind07( + timestamp: timestamp, + method: method, + path: path, + body: bodyString, + ed25519SecretKey: userEdKeyPair.secretKey + ) + ) + else { throw NetworkError.signingFailed } + + return ( + publicKey: SessionId(.versionBlinded07, publicKey: blinded07KeyPair.publicKey).hexString, + signature: signatureResult + ) + } + + private static func signRequest( + _ db: Database, + preparedRequest: Network.PreparedRequest, + using dependencies: Dependencies + ) throws -> Network.Destination { + guard let url: URL = preparedRequest.destination.url else { + throw NetworkError.signingFailed + } + + guard case let .server(info) = preparedRequest.destination else { + throw NetworkError.signingFailed + } + + return .server( + info: info.updated( + with: try signatureHeaders( + db, + url: url, + method: preparedRequest.method, + body: preparedRequest.body, + using: dependencies + ) + ) + ) + } +} + diff --git a/SessionSnodeKit/Types/SwarmDrainBehaviour.swift b/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift similarity index 100% rename from SessionSnodeKit/Types/SwarmDrainBehaviour.swift rename to SessionNetworkingKit/Types/SwarmDrainBehaviour.swift diff --git a/SessionSnodeKit/Types/UpdatableTimestamp.swift b/SessionNetworkingKit/Types/UpdatableTimestamp.swift similarity index 100% rename from SessionSnodeKit/Types/UpdatableTimestamp.swift rename to SessionNetworkingKit/Types/UpdatableTimestamp.swift diff --git a/SessionSnodeKit/Types/ValidatableResponse.swift b/SessionNetworkingKit/Types/ValidatableResponse.swift similarity index 100% rename from SessionSnodeKit/Types/ValidatableResponse.swift rename to SessionNetworkingKit/Types/ValidatableResponse.swift diff --git a/SessionSnodeKit/Utilities/Data+Utilities.swift b/SessionNetworkingKit/Utilities/Data+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Data+Utilities.swift rename to SessionNetworkingKit/Utilities/Data+Utilities.swift diff --git a/SessionSnodeKit/Utilities/Publisher+Utilities.swift b/SessionNetworkingKit/Utilities/Publisher+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Publisher+Utilities.swift rename to SessionNetworkingKit/Utilities/Publisher+Utilities.swift diff --git a/SessionSnodeKit/Utilities/RetryWithDependencies.swift b/SessionNetworkingKit/Utilities/RetryWithDependencies.swift similarity index 100% rename from SessionSnodeKit/Utilities/RetryWithDependencies.swift rename to SessionNetworkingKit/Utilities/RetryWithDependencies.swift diff --git a/SessionSnodeKit/Utilities/String+Trimming.swift b/SessionNetworkingKit/Utilities/String+Trimming.swift similarity index 100% rename from SessionSnodeKit/Utilities/String+Trimming.swift rename to SessionNetworkingKit/Utilities/String+Trimming.swift diff --git a/SessionSnodeKit/Utilities/URLResponse+Utilities.swift b/SessionNetworkingKit/Utilities/URLResponse+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/URLResponse+Utilities.swift rename to SessionNetworkingKit/Utilities/URLResponse+Utilities.swift diff --git a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift similarity index 96% rename from SessionSnodeKitTests/Models/FileUploadResponseSpec.swift rename to SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift index 1454fbe383..4339984b89 100644 --- a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift +++ b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class FileUploadResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift similarity index 98% rename from SessionSnodeKitTests/Models/SnodeRequestSpec.swift rename to SessionNetworkingKitTests/Models/SnodeRequestSpec.swift index 3d6836709b..0405d71e7c 100644 --- a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift +++ b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class SnodeRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/SessionSnodeKit.xctestplan b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan similarity index 90% rename from SessionSnodeKitTests/SessionSnodeKit.xctestplan rename to SessionNetworkingKitTests/SessionNetworkingKit.xctestplan index ea699b6efd..52ef80ee0c 100644 --- a/SessionSnodeKitTests/SessionSnodeKit.xctestplan +++ b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan @@ -17,7 +17,7 @@ "target" : { "containerPath" : "container:Session.xcodeproj", "identifier" : "FDB5DAF92A981C42002C8721", - "name" : "SessionSnodeKitTests" + "name" : "SessionNetworkingKitTests" } } ], diff --git a/SessionSnodeKitTests/Types/BatchRequestSpec.swift b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchRequestSpec.swift rename to SessionNetworkingKitTests/Types/BatchRequestSpec.swift index 36b33bd18a..05554a2be1 100644 --- a/SessionSnodeKitTests/Types/BatchRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BatchResponseSpec.swift b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchResponseSpec.swift rename to SessionNetworkingKitTests/Types/BatchResponseSpec.swift index 3fe9ea5575..eac736ac8b 100644 --- a/SessionSnodeKitTests/Types/BatchResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BencodeResponseSpec.swift rename to SessionNetworkingKitTests/Types/BencodeResponseSpec.swift index e0e6add5f9..0bd213f4ec 100644 --- a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BencodeResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/DestinationSpec.swift b/SessionNetworkingKitTests/Types/DestinationSpec.swift similarity index 98% rename from SessionSnodeKitTests/Types/DestinationSpec.swift rename to SessionNetworkingKitTests/Types/DestinationSpec.swift index 6e17460ac5..e226a25f21 100644 --- a/SessionSnodeKitTests/Types/DestinationSpec.swift +++ b/SessionNetworkingKitTests/Types/DestinationSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class DestinationSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/HeaderSpec.swift b/SessionNetworkingKitTests/Types/HeaderSpec.swift similarity index 93% rename from SessionSnodeKitTests/Types/HeaderSpec.swift rename to SessionNetworkingKitTests/Types/HeaderSpec.swift index ef25b0c5a7..9f73a41944 100644 --- a/SessionSnodeKitTests/Types/HeaderSpec.swift +++ b/SessionNetworkingKitTests/Types/HeaderSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class HeaderSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index a75966aa6b..a1ad6f438b 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSendingSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index 7ba2a676ac..7b949b5fde 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/RequestSpec.swift b/SessionNetworkingKitTests/Types/RequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/RequestSpec.swift rename to SessionNetworkingKitTests/Types/RequestSpec.swift index 9888d30083..0b951cf1d3 100644 --- a/SessionSnodeKitTests/Types/RequestSpec.swift +++ b/SessionNetworkingKitTests/Types/RequestSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class RequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift similarity index 96% rename from SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift rename to SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift index 85e05a67af..d99134ceba 100644 --- a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift @@ -2,7 +2,7 @@ import Foundation -@testable import SessionSnodeKit +@testable import SessionNetworkingKit extension NoResponse: Mocked { static var mock: NoResponse = NoResponse() diff --git a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift similarity index 99% rename from SessionSnodeKitTests/_TestUtilities/MockNetwork.swift rename to SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index efecb6d9ac..1412423914 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -4,7 +4,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit // MARK: - MockNetwork diff --git a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift similarity index 97% rename from SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift rename to SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift index 75111962b8..90be7933ed 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift @@ -6,7 +6,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MockSnodeAPICache: Mock, SnodeAPICacheType { var hardfork: Int { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 70d2273560..c2b842fc1b 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -5,7 +5,7 @@ import CallKit import UserNotifications import SessionUIKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 475489dd60..e977151317 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -7,7 +7,7 @@ import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 34246413a9..b460349b73 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -8,7 +8,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate, ThemedNavigation { diff --git a/SessionSnodeKit/Meta/SessionSnodeKit.h b/SessionSnodeKit/Meta/SessionSnodeKit.h deleted file mode 100644 index 698aa516fd..0000000000 --- a/SessionSnodeKit/Meta/SessionSnodeKit.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -FOUNDATION_EXPORT double SessionSnodeKitVersionNumber; -FOUNDATION_EXPORT const unsigned char SessionSnodeKitVersionString[]; diff --git a/SessionSnodeKit/Utilities/Threading+SSK.swift b/SessionSnodeKit/Utilities/Threading+SSK.swift deleted file mode 100644 index 3424ad438e..0000000000 --- a/SessionSnodeKit/Utilities/Threading+SSK.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import SessionUtilitiesKit - -public extension Threading { - static let workQueue = DispatchQueue(label: "SessionSnodeKit.workQueue", qos: .userInitiated) // It's important that this is a serial queue -} diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index dcf2b6ea26..7028b12544 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index b339cbe090..227c06bf86 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 5f9fdb51ac..ec9d79c0a2 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionUIKit @@ -31,7 +31,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 6e0561e696..5c63cd7fae 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -6,7 +6,7 @@ import Quick import Nimble import SessionUtil import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit @testable import Session @testable import SessionMessagingKit @@ -41,7 +41,7 @@ class DatabaseSpec: QuickSpec { let allMigrations: [Storage.KeyedMigration] = SynchronousStorage.sortedMigrationInfo( migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ] @@ -77,7 +77,7 @@ class DatabaseSpec: QuickSpec { mockStorage.perform( migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Session.xctestplan b/SessionTests/Session.xctestplan index fcfc4d45ad..a9c2927686 100644 --- a/SessionTests/Session.xctestplan +++ b/SessionTests/Session.xctestplan @@ -34,7 +34,7 @@ { "containerPath" : "container:Session.xcodeproj", "identifier" : "C3C2A59E255385C100C340D1", - "name" : "SessionSnodeKit" + "name" : "SessionNetworkingKit" }, { "containerPath" : "container:Session.xcodeproj", @@ -87,7 +87,7 @@ "target" : { "containerPath" : "container:Session.xcodeproj", "identifier" : "FDB5DAF92A981C42002C8721", - "name" : "SessionSnodeKitTests" + "name" : "SessionNetworkingKitTests" } }, { diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index ceda704ea2..65c099d860 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -6,7 +6,7 @@ import Quick import Nimble import SessionUtil import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class NotificationContentViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift index b320516d5c..860647c618 100644 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift @@ -23,7 +23,7 @@ public struct TargetMigrations: Comparable { // changing them will result in the migrations running again case session case utilitiesKit - case snodeKit + case networkingKit = "snodeKit" case messagingKit case _deprecatedUIKit = "uiKit" case test diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 2dff548f87..9bd2646f9e 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -22,6 +22,14 @@ public extension FeatureStorage { ) } + static func logLevel(group: Log.Group) -> FeatureConfig { + return Dependencies.create( + identifier: "\(Log.Group.identifierPrefix)\(group.name)", + groupIdentifier: "logging", + defaultOption: group.defaultLevel + ) + } + static let allLogLevels: FeatureConfig = Dependencies.create( identifier: "allLogLevels", groupIdentifier: "logging" @@ -50,33 +58,66 @@ public enum Log { case off case `default` + + var label: String { + switch self { + case .off: return "off" + case .verbose: return "verbose" + case .debug: return "debug" + case .info: return "info" + case .warn: return "warn" + case .error: return "error" + case .critical: return "critical" + case .default: return "default" + } + } + } + + public struct Group: Hashable { + public let name: String + public let defaultLevel: Log.Level + + fileprivate static let identifierPrefix: String = "group:" + + private init(name: String, defaultLevel: Log.Level) { + self.name = name + + switch AllLoggingCategories.existingGroup(for: name) { + case .some(let existingGroup): self.defaultLevel = existingGroup.defaultLevel + case .none: + self.defaultLevel = defaultLevel + AllLoggingCategories.register(group: self) + } + } + + @discardableResult public static func create( + _ group: String, + defaultLevel: Log.Level + ) -> Log.Group { + return Log.Group(name: group, defaultLevel: defaultLevel) + } } public struct Category: Hashable { public let rawValue: String - fileprivate let customPrefix: String + fileprivate let group: Group? fileprivate let customSuffix: String public let defaultLevel: Log.Level fileprivate static let identifierPrefix: String = "logLevel-" fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" } - private init(rawValue: String, customPrefix: String, customSuffix: String, defaultLevel: Log.Level) { + private init(rawValue: String, group: Group?, customSuffix: String, defaultLevel: Log.Level) { + self.rawValue = rawValue + self.group = group + self.customSuffix = customSuffix + /// If we've already registered this category then assume the original has the correct `defaultLevel` and only /// modify the `customPrefix` value switch AllLoggingCategories.existingCategory(for: rawValue) { - case .some(let existingCategory): - self.rawValue = existingCategory.rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix - self.defaultLevel = existingCategory.defaultLevel - + case .some(let existingCategory): self.defaultLevel = existingCategory.defaultLevel case .none: - self.rawValue = rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix self.defaultLevel = defaultLevel - AllLoggingCategories.register(category: self) } } @@ -86,25 +127,25 @@ public enum Log { self.init( rawValue: identifier.substring(from: Category.identifierPrefix.count), - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .default ) } - public init(rawValue: String, customPrefix: String = "", customSuffix: String = "") { - self.init(rawValue: rawValue, customPrefix: customPrefix, customSuffix: customSuffix, defaultLevel: .default) + public init(rawValue: String, group: Group? = nil, customSuffix: String = "") { + self.init(rawValue: rawValue, group: group, customSuffix: customSuffix, defaultLevel: .default) } @discardableResult public static func create( _ rawValue: String, - customPrefix: String = "", + group: Group? = nil, customSuffix: String = "", defaultLevel: Log.Level ) -> Log.Category { return Log.Category( rawValue: rawValue, - customPrefix: customPrefix, + group: group, customSuffix: customSuffix, defaultLevel: defaultLevel ) @@ -653,12 +694,15 @@ public actor Logger: LoggerType { let defaultLogLevel: Log.Level = dependencies[feature: .logLevel(cat: .default)] let lowestCatLevel: Log.Level = categories .reduce(into: [], { result, next in - guard dependencies[feature: .logLevel(cat: next)] != .default else { - result.append(defaultLogLevel) - return - } + let explicitLevel: Log.Level = dependencies[feature: .logLevel(cat: next)] + let groupLevel: Log.Level? = next.group.map { dependencies[feature: .logLevel(group: $0)] } - result.append(dependencies[feature: .logLevel(cat: next)]) + switch (explicitLevel, groupLevel) { + case (.default, .none): result.append(defaultLogLevel) + case (.default, .default): result.append(defaultLogLevel) + case (_, .none): result.append(explicitLevel) + case (_, .some(let groupLevel)): result.append(min(explicitLevel, groupLevel)) + } }) .min() .defaulting(to: defaultLogLevel) @@ -678,15 +722,15 @@ public actor Logger: LoggerType { /// No point doubling up but we want to allow categories which match the `primaryPrefix` so that we /// have a mechanism for providing a different "default" log level for a specific target .filter { $0.rawValue != primaryPrefix } - .map { "\($0.customPrefix)\($0.rawValue)\($0.customSuffix)" } + .map { "\($0.group.map { "\($0.name):" } ?? "")\($0.rawValue)\($0.customSuffix)" } ) .joined(separator: ", ") - return "[\(prefixes)] " + return "[\(prefixes)]" }() /// Clean up the message if needed (replace double periods with single, trim whitespace, truncate pubkeys) - let logMessage: String = logPrefix + let cleanedMessage: String = logPrefix .appending(message) .replacingOccurrences(of: "...", with: "|||") .replacingOccurrences(of: "..", with: ".") @@ -709,16 +753,17 @@ public actor Logger: LoggerType { return updatedText } - + let ddLogMessage: String = "\(logPrefix) ".appending(cleanedMessage) + let consoleLogMessage: String = "\(logPrefix)[\(level)] ".appending(cleanedMessage) switch level { case .off, .default: return - case .verbose: DDLogVerbose("💙 \(logMessage)", file: file, function: function, line: line) - case .debug: DDLogDebug("💚 \(logMessage)", file: file, function: function, line: line) - case .info: DDLogInfo("💛 \(logMessage)", file: file, function: function, line: line) - case .warn: DDLogWarn("🧡 \(logMessage)", file: file, function: function, line: line) - case .error: DDLogError("❤️ \(logMessage)", file: file, function: function, line: line) - case .critical: DDLogError("🔥 \(logMessage)", file: file, function: function, line: line) + case .verbose: DDLogVerbose("💙 \(ddLogMessage)", file: file, function: function, line: line) + case .debug: DDLogDebug("💚 \(ddLogMessage)", file: file, function: function, line: line) + case .info: DDLogInfo("💛 \(ddLogMessage)", file: file, function: function, line: line) + case .warn: DDLogWarn("🧡 \(ddLogMessage)", file: file, function: function, line: line) + case .error: DDLogError("❤️ \(ddLogMessage)", file: file, function: function, line: line) + case .critical: DDLogError("🔥 \(ddLogMessage)", file: file, function: function, line: line) } let mainCategory: String = (categories.first?.rawValue ?? "General") @@ -730,7 +775,7 @@ public actor Logger: LoggerType { } #if DEBUG - systemLogger?.log(level, logMessage) + systemLogger?.log(level, consoleLogMessage) #endif } } @@ -859,6 +904,7 @@ extension Log.Level: FeatureOption { public struct AllLoggingCategories: FeatureOption { public static let allCases: [AllLoggingCategories] = [] + @ThreadSafeObject private static var registeredGroupDefaults: Set = [] @ThreadSafeObject private static var registeredCategoryDefaults: Set = [] // MARK: - Initialization @@ -866,7 +912,21 @@ public struct AllLoggingCategories: FeatureOption { public let rawValue: Int public init(rawValue: Int) { - self.rawValue = -1 // `0` is a protected value so can't use it + _ = Log.Category.default // Access the `default` log category to ensure it exists + self.rawValue = -1 // `0` is a protected value so can't use it + } + + fileprivate static func register(group: Log.Group) { + guard + !registeredGroupDefaults.contains(where: { existingGroup in + /// **Note:** We only want to use the `rawValue` to distinguish between logging categories + /// as the `defaultLevel` can change via the dev settings and any additional metadata could + /// be file/class specific + group.name == existingGroup.name + }) + else { return } + + _registeredGroupDefaults.performUpdate { $0.inserting(group) } } fileprivate static func register(category: Log.Category) { @@ -882,10 +942,21 @@ public struct AllLoggingCategories: FeatureOption { _registeredCategoryDefaults.performUpdate { $0.inserting(category) } } + fileprivate static func existingGroup(for name: String) -> Log.Group? { + return AllLoggingCategories.registeredGroupDefaults.first(where: { $0.name == name }) + } + fileprivate static func existingCategory(for cat: String) -> Log.Category? { return AllLoggingCategories.registeredCategoryDefaults.first(where: { $0.rawValue == cat }) } + public func currentValues(using dependencies: Dependencies) -> [Log.Group: Log.Level] { + return AllLoggingCategories.registeredGroupDefaults + .reduce(into: [:]) { result, group in + result[group] = dependencies[feature: .logLevel(group: group)] + } + } + public func currentValues(using dependencies: Dependencies) -> [Log.Category: Log.Level] { return AllLoggingCategories.registeredCategoryDefaults .reduce(into: [:]) { result, cat in diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index 916686f2f8..e787e7b4db 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -17,6 +17,10 @@ public extension Log.Category { static let libSession: Log.Category = .create("LibSession", defaultLevel: .info) } +public extension Log.Group { + static let libSession: Log.Group = .create("libSession", defaultLevel: .info) +} + // MARK: - Logging extension LibSession { @@ -29,7 +33,13 @@ extension LibSession { ObservationBuilder.observe(.featureGroup(.allLogLevels), using: dependencies) { [dependencies] _ in let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels] .currentValues(using: dependencies) - let cDefaultLevel: LOG_LEVEL = (currentLogLevels[.default]?.libSession ?? LOG_LEVEL_OFF) + let currentGroupLogLevels: [Log.Group: Log.Level] = dependencies[feature: .allLogLevels] + .currentValues(using: dependencies) + let targetDefault: Log.Level? = min( + (currentLogLevels[.default] ?? .off), + (currentGroupLogLevels[.libSession] ?? .off) + ) + let cDefaultLevel: LOG_LEVEL = (targetDefault?.libSession ?? LOG_LEVEL_OFF) session_logger_set_level_default(cDefaultLevel) session_logger_reset_level(cDefaultLevel) @@ -57,20 +67,45 @@ extension LibSession { DispatchQueue.global(qos: .background).async { /// Logs from libSession come through in the format: /// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}` - /// We want to remove the extra data because it doesn't help the logs + /// + /// We want to simplify the message because our logging already includes category and timestamp information: + /// `[+{lifetime}s] {message}` let processedMessage: String = { - let logParts: [String] = msg.components(separatedBy: "] ") + let trimmedMsg = msg.trimmingCharacters(in: .whitespacesAndNewlines) - guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) } + guard + let timestampRegex: NSRegularExpression = LibSession.timestampRegex, + let messageStartRegex: NSRegularExpression = LibSession.messageStartRegex + else { return trimmedMsg } - let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines) + let fullRange = NSRange(trimmedMsg.startIndex.. Date: Thu, 7 Aug 2025 14:19:04 +1000 Subject: [PATCH 027/244] update protobuf and logic on profile updated timestamp --- .../ConversationVC+Interaction.swift | 4 +- Session/Onboarding/Onboarding.swift | 1 + .../LibSession+GroupMembers.swift | 1 + .../LibSession+UserProfile.swift | 3 +- .../VisibleMessage+Profile.swift | 15 +- .../Protos/Generated/SNProto.swift | 14 + .../Protos/Generated/SessionProtos.pb.swift | 505 ++++-------------- .../Generated/WebSocketResources.pb.swift | 48 +- .../Protos/SessionProtos.proto | 2 + .../MessageReceiver+Groups.swift | 6 + .../MessageReceiver+MessageRequests.swift | 2 + .../MessageReceiver+VisibleMessages.swift | 2 + .../Sending & Receiving/MessageSender.swift | 7 +- .../Utilities/Profile+CurrentUser.swift | 29 +- .../ProfilePictureView+Convenience.swift | 15 +- 15 files changed, 188 insertions(+), 466 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index a06c7c9149..81789ed7c4 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -789,6 +789,7 @@ extension ConversationVC: // FIXME: Remove this once we don't generate unique Profile entries for the current users blinded ids if (try? SessionId.Prefix(from: optimisticData.interaction.authorId)) != .standard { let currentUserProfile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } + let sentTimestamp: TimeInterval = (Double(optimisticData.interaction.timestampMs) / 1000) try? Profile.updateIfNeeded( db, @@ -799,7 +800,8 @@ extension ConversationVC: fallback: .none, using: dependencies ), - sentTimestamp: (Double(optimisticData.interaction.timestampMs) / 1000), + profileUpdateTimestamp: (currentUserProfile.lastNameUpdate ?? sentTimestamp), + sentTimestamp: sentTimestamp, using: dependencies ) } diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 52da3682fa..8bad85d79a 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -411,6 +411,7 @@ extension Onboarding { publicKey: userSessionId.hexString, displayNameUpdate: .currentUserUpdate(displayName), displayPictureUpdate: .none, + profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index bbc0f057f2..6e1c451aa1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -134,6 +134,7 @@ internal extension LibSessionCacheType { publicKey: profile.id, displayNameUpdate: .contactUpdate(profile.name), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), + profileUpdateTimestamp: (profile.displayPictureLastUpdated ?? 0), sentTimestamp: TimeInterval(Double(serverTimestampMs) * 1000), using: dependencies ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 90980f5c00..29b30d108c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -71,9 +71,10 @@ internal extension LibSessionCacheType { url: displayPictureUrl, key: displayPic.get(\.key), filePath: filePath, - sessionProProof: getProProof() // TODO: double check if this is needed + sessionProProof: getProProof() // TODO: double check if this is needed after Pro Proof is implemented ) }(), + profileUpdateTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), sentTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), using: dependencies ) diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 5591fbb0b4..f7a521c937 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -10,6 +10,7 @@ public extension VisibleMessage { public let displayName: String? public let profileKey: Data? public let profilePictureUrl: String? + public let updateTimestampMs: UInt64? public let blocksCommunityMessageRequests: Bool? public let sessionProProof: String? @@ -19,6 +20,7 @@ public extension VisibleMessage { displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil, + updateTimestampMs: UInt64? = nil, blocksCommunityMessageRequests: Bool? = nil, sessionProProof: String? = nil ) { @@ -27,6 +29,7 @@ public extension VisibleMessage { self.displayName = displayName self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) + self.updateTimestampMs = updateTimestampMs self.blocksCommunityMessageRequests = blocksCommunityMessageRequests self.sessionProProof = sessionProProof } @@ -43,6 +46,7 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, + updateTimestampMs: profileProto.profileUpdateTimestamp, blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) @@ -64,6 +68,10 @@ public extension VisibleMessage { profileProto.setProfilePicture(profilePictureUrl) } + if let updateTimestampMs: UInt64 = updateTimestampMs { + profileProto.setProfileUpdateTimestamp(updateTimestampMs) + } + dataMessageProto.setProfile(try profileProto.build()) return dataMessageProto } @@ -92,6 +100,7 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, + updateTimestampMs: profileProto.profileUpdateTimestamp, sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -111,6 +120,9 @@ public extension VisibleMessage { messageRequestResponseProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) } + if let updateTimestampMs: UInt64 = updateTimestampMs { + profileProto.setProfileUpdateTimestamp(updateTimestampMs) + } do { messageRequestResponseProto.setProfile(try profileProto.build()) return try messageRequestResponseProto.build() @@ -127,7 +139,8 @@ public extension VisibleMessage { Profile( displayName: \(displayName ?? "null"), profileKey: \(profileKey?.description ?? "null"), - profilePictureUrl: \(profilePictureUrl ?? "null") + profilePictureUrl: \(profilePictureUrl ?? "null"), + profileUpdateTimestamp: \(updateTimestampMs ?? 0) ) """ } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 14aaa60e99..2419037f94 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -1296,6 +1296,9 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui if let _value = profilePicture { builder.setProfilePicture(_value) } + if hasProfileUpdateTimestamp { + builder.setProfileUpdateTimestamp(profileUpdateTimestamp) + } return builder } @@ -1313,6 +1316,10 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui proto.profilePicture = valueParam } + @objc public func setProfileUpdateTimestamp(_ valueParam: UInt64) { + proto.profileUpdateTimestamp = valueParam + } + @objc public func build() throws -> SNProtoLokiProfile { return try SNProtoLokiProfile.parseProto(proto) } @@ -1344,6 +1351,13 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui return proto.hasProfilePicture } + @objc public var profileUpdateTimestamp: UInt64 { + return proto.profileUpdateTimestamp + } + @objc public var hasProfileUpdateTimestamp: Bool { + return proto.hasProfileUpdateTimestamp + } + private init(proto: SessionProtos_LokiProfile) { self.proto = proto } diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index b6fc8ff3bd..01530127a9 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: SessionProtos.proto @@ -22,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct SessionProtos_Envelope { +struct SessionProtos_Envelope: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -85,30 +86,14 @@ struct SessionProtos_Envelope { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case sessionMessage // = 6 - case closedGroupMessage // = 7 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case sessionMessage = 6 + case closedGroupMessage = 7 init() { self = .sessionMessage } - init?(rawValue: Int) { - switch rawValue { - case 6: self = .sessionMessage - case 7: self = .closedGroupMessage - default: return nil - } - } - - var rawValue: Int { - switch self { - case .sessionMessage: return 6 - case .closedGroupMessage: return 7 - } - } - } init() {} @@ -121,15 +106,7 @@ struct SessionProtos_Envelope { fileprivate var _serverTimestamp: UInt64? = nil } -#if swift(>=4.2) - -extension SessionProtos_Envelope.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_TypingMessage { +struct SessionProtos_TypingMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -156,30 +133,14 @@ struct SessionProtos_TypingMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Action: SwiftProtobuf.Enum { - typealias RawValue = Int - case started // = 0 - case stopped // = 1 + enum Action: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case started = 0 + case stopped = 1 init() { self = .started } - init?(rawValue: Int) { - switch rawValue { - case 0: self = .started - case 1: self = .stopped - default: return nil - } - } - - var rawValue: Int { - switch self { - case .started: return 0 - case .stopped: return 1 - } - } - } init() {} @@ -188,15 +149,7 @@ struct SessionProtos_TypingMessage { fileprivate var _action: SessionProtos_TypingMessage.Action? = nil } -#if swift(>=4.2) - -extension SessionProtos_TypingMessage.Action: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_UnsendRequest { +struct SessionProtos_UnsendRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -229,7 +182,7 @@ struct SessionProtos_UnsendRequest { fileprivate var _author: String? = nil } -struct SessionProtos_MessageRequestResponse { +struct SessionProtos_MessageRequestResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -271,7 +224,7 @@ struct SessionProtos_MessageRequestResponse { fileprivate var _profile: SessionProtos_LokiProfile? = nil } -struct SessionProtos_Content { +struct SessionProtos_Content: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -368,33 +321,15 @@ struct SessionProtos_Content { var unknownFields = SwiftProtobuf.UnknownStorage() - enum ExpirationType: SwiftProtobuf.Enum { - typealias RawValue = Int - case unknown // = 0 - case deleteAfterRead // = 1 - case deleteAfterSend // = 2 + enum ExpirationType: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case unknown = 0 + case deleteAfterRead = 1 + case deleteAfterSend = 2 init() { self = .unknown } - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .deleteAfterRead - case 2: self = .deleteAfterSend - default: return nil - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .deleteAfterRead: return 1 - case .deleteAfterSend: return 2 - } - } - } init() {} @@ -402,15 +337,7 @@ struct SessionProtos_Content { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension SessionProtos_Content.ExpirationType: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_CallMessage { +struct SessionProtos_CallMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -443,42 +370,18 @@ struct SessionProtos_CallMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case preOffer // = 6 - case offer // = 1 - case answer // = 2 - case provisionalAnswer // = 3 - case iceCandidates // = 4 - case endCall // = 5 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case preOffer = 6 + case offer = 1 + case answer = 2 + case provisionalAnswer = 3 + case iceCandidates = 4 + case endCall = 5 init() { self = .preOffer } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .offer - case 2: self = .answer - case 3: self = .provisionalAnswer - case 4: self = .iceCandidates - case 5: self = .endCall - case 6: self = .preOffer - default: return nil - } - } - - var rawValue: Int { - switch self { - case .offer: return 1 - case .answer: return 2 - case .provisionalAnswer: return 3 - case .iceCandidates: return 4 - case .endCall: return 5 - case .preOffer: return 6 - } - } - } init() {} @@ -487,15 +390,7 @@ struct SessionProtos_CallMessage { fileprivate var _uuid: String? = nil } -#if swift(>=4.2) - -extension SessionProtos_CallMessage.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_KeyPair { +struct SessionProtos_KeyPair: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -528,7 +423,7 @@ struct SessionProtos_KeyPair { fileprivate var _privateKey: Data? = nil } -struct SessionProtos_DataExtractionNotification { +struct SessionProtos_DataExtractionNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -554,32 +449,16 @@ struct SessionProtos_DataExtractionNotification { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case screenshot // = 1 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case screenshot = 1 /// timestamp - case mediaSaved // = 2 + case mediaSaved = 2 init() { self = .screenshot } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .screenshot - case 2: self = .mediaSaved - default: return nil - } - } - - var rawValue: Int { - switch self { - case .screenshot: return 1 - case .mediaSaved: return 2 - } - } - } init() {} @@ -588,15 +467,7 @@ struct SessionProtos_DataExtractionNotification { fileprivate var _timestamp: UInt64? = nil } -#if swift(>=4.2) - -extension SessionProtos_DataExtractionNotification.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_LokiProfile { +struct SessionProtos_LokiProfile: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -619,15 +490,26 @@ struct SessionProtos_LokiProfile { /// Clears the value of `profilePicture`. Subsequent reads from it will return its default value. mutating func clearProfilePicture() {self._profilePicture = nil} + /// Timestamp of the last profile update + var profileUpdateTimestamp: UInt64 { + get {return _profileUpdateTimestamp ?? 0} + set {_profileUpdateTimestamp = newValue} + } + /// Returns true if `profileUpdateTimestamp` has been explicitly set. + var hasProfileUpdateTimestamp: Bool {return self._profileUpdateTimestamp != nil} + /// Clears the value of `profileUpdateTimestamp`. Subsequent reads from it will return its default value. + mutating func clearProfileUpdateTimestamp() {self._profileUpdateTimestamp = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} fileprivate var _displayName: String? = nil fileprivate var _profilePicture: String? = nil + fileprivate var _profileUpdateTimestamp: UInt64? = nil } -struct SessionProtos_DataMessage { +struct SessionProtos_DataMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -744,30 +626,16 @@ struct SessionProtos_DataMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: SwiftProtobuf.Enum { - typealias RawValue = Int - case expirationTimerUpdate // = 2 + enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case expirationTimerUpdate = 2 init() { self = .expirationTimerUpdate } - init?(rawValue: Int) { - switch rawValue { - case 2: self = .expirationTimerUpdate - default: return nil - } - } - - var rawValue: Int { - switch self { - case .expirationTimerUpdate: return 2 - } - } - } - struct Quote { + struct Quote: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -805,7 +673,7 @@ struct SessionProtos_DataMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - struct QuotedAttachment { + struct QuotedAttachment: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -848,27 +716,13 @@ struct SessionProtos_DataMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: SwiftProtobuf.Enum { - typealias RawValue = Int - case voiceMessage // = 1 + enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case voiceMessage = 1 init() { self = .voiceMessage } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .voiceMessage - default: return nil - } - } - - var rawValue: Int { - switch self { - case .voiceMessage: return 1 - } - } - } init() {} @@ -886,7 +740,7 @@ struct SessionProtos_DataMessage { fileprivate var _text: String? = nil } - struct Preview { + struct Preview: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -928,7 +782,7 @@ struct SessionProtos_DataMessage { fileprivate var _image: SessionProtos_AttachmentPointer? = nil } - struct Reaction { + struct Reaction: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -974,30 +828,14 @@ struct SessionProtos_DataMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Action: SwiftProtobuf.Enum { - typealias RawValue = Int - case react // = 0 - case remove // = 1 + enum Action: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case react = 0 + case remove = 1 init() { self = .react } - init?(rawValue: Int) { - switch rawValue { - case 0: self = .react - case 1: self = .remove - default: return nil - } - } - - var rawValue: Int { - switch self { - case .react: return 0 - case .remove: return 1 - } - } - } init() {} @@ -1008,7 +846,7 @@ struct SessionProtos_DataMessage { fileprivate var _action: SessionProtos_DataMessage.Reaction.Action? = nil } - struct OpenGroupInvitation { + struct OpenGroupInvitation: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1046,23 +884,7 @@ struct SessionProtos_DataMessage { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension SessionProtos_DataMessage.Flags: CaseIterable { - // Support synthesized by the compiler. -} - -extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: CaseIterable { - // Support synthesized by the compiler. -} - -extension SessionProtos_DataMessage.Reaction.Action: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_ReceiptMessage { +struct SessionProtos_ReceiptMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1081,30 +903,14 @@ struct SessionProtos_ReceiptMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case delivery // = 0 - case read // = 1 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case delivery = 0 + case read = 1 init() { self = .delivery } - init?(rawValue: Int) { - switch rawValue { - case 0: self = .delivery - case 1: self = .read - default: return nil - } - } - - var rawValue: Int { - switch self { - case .delivery: return 0 - case .read: return 1 - } - } - } init() {} @@ -1112,15 +918,7 @@ struct SessionProtos_ReceiptMessage { fileprivate var _type: SessionProtos_ReceiptMessage.TypeEnum? = nil } -#if swift(>=4.2) - -extension SessionProtos_ReceiptMessage.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_AttachmentPointer { +struct SessionProtos_AttachmentPointer: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1236,27 +1034,13 @@ struct SessionProtos_AttachmentPointer { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: SwiftProtobuf.Enum { - typealias RawValue = Int - case voiceMessage // = 1 + enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case voiceMessage = 1 init() { self = .voiceMessage } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .voiceMessage - default: return nil - } - } - - var rawValue: Int { - switch self { - case .voiceMessage: return 1 - } - } - } init() {} @@ -1275,15 +1059,7 @@ struct SessionProtos_AttachmentPointer { fileprivate var _url: String? = nil } -#if swift(>=4.2) - -extension SessionProtos_AttachmentPointer.Flags: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_GroupUpdateMessage { +struct SessionProtos_GroupUpdateMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1367,7 +1143,7 @@ struct SessionProtos_GroupUpdateMessage { fileprivate var _storage = _StorageClass.defaultInstance } -struct SessionProtos_GroupUpdateInviteMessage { +struct SessionProtos_GroupUpdateInviteMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1422,7 +1198,7 @@ struct SessionProtos_GroupUpdateInviteMessage { fileprivate var _adminSignature: Data? = nil } -struct SessionProtos_GroupUpdatePromoteMessage { +struct SessionProtos_GroupUpdatePromoteMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1455,7 +1231,7 @@ struct SessionProtos_GroupUpdatePromoteMessage { fileprivate var _name: String? = nil } -struct SessionProtos_GroupUpdateInfoChangeMessage { +struct SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1500,33 +1276,15 @@ struct SessionProtos_GroupUpdateInfoChangeMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case name // = 1 - case avatar // = 2 - case disappearingMessages // = 3 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case name = 1 + case avatar = 2 + case disappearingMessages = 3 init() { self = .name } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .name - case 2: self = .avatar - case 3: self = .disappearingMessages - default: return nil - } - } - - var rawValue: Int { - switch self { - case .name: return 1 - case .avatar: return 2 - case .disappearingMessages: return 3 - } - } - } init() {} @@ -1537,15 +1295,7 @@ struct SessionProtos_GroupUpdateInfoChangeMessage { fileprivate var _adminSignature: Data? = nil } -#if swift(>=4.2) - -extension SessionProtos_GroupUpdateInfoChangeMessage.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -struct SessionProtos_GroupUpdateMemberChangeMessage { +struct SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1583,33 +1333,15 @@ struct SessionProtos_GroupUpdateMemberChangeMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case added // = 1 - case removed // = 2 - case promoted // = 3 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case added = 1 + case removed = 2 + case promoted = 3 init() { self = .added } - init?(rawValue: Int) { - switch rawValue { - case 1: self = .added - case 2: self = .removed - case 3: self = .promoted - default: return nil - } - } - - var rawValue: Int { - switch self { - case .added: return 1 - case .removed: return 2 - case .promoted: return 3 - } - } - } init() {} @@ -1619,16 +1351,8 @@ struct SessionProtos_GroupUpdateMemberChangeMessage { fileprivate var _adminSignature: Data? = nil } -#if swift(>=4.2) - -extension SessionProtos_GroupUpdateMemberChangeMessage.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - /// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop) -struct SessionProtos_GroupUpdateMemberLeftMessage { +struct SessionProtos_GroupUpdateMemberLeftMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1639,7 +1363,7 @@ struct SessionProtos_GroupUpdateMemberLeftMessage { } /// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop) -struct SessionProtos_GroupUpdateMemberLeftNotificationMessage { +struct SessionProtos_GroupUpdateMemberLeftNotificationMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1649,7 +1373,7 @@ struct SessionProtos_GroupUpdateMemberLeftNotificationMessage { init() {} } -struct SessionProtos_GroupUpdateInviteResponseMessage { +struct SessionProtos_GroupUpdateInviteResponseMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1671,7 +1395,7 @@ struct SessionProtos_GroupUpdateInviteResponseMessage { fileprivate var _isApproved: Bool? = nil } -struct SessionProtos_GroupUpdateDeleteMemberContentMessage { +struct SessionProtos_GroupUpdateDeleteMemberContentMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1696,47 +1420,6 @@ struct SessionProtos_GroupUpdateDeleteMemberContentMessage { fileprivate var _adminSignature: Data? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension SessionProtos_Envelope: @unchecked Sendable {} -extension SessionProtos_Envelope.TypeEnum: @unchecked Sendable {} -extension SessionProtos_TypingMessage: @unchecked Sendable {} -extension SessionProtos_TypingMessage.Action: @unchecked Sendable {} -extension SessionProtos_UnsendRequest: @unchecked Sendable {} -extension SessionProtos_MessageRequestResponse: @unchecked Sendable {} -extension SessionProtos_Content: @unchecked Sendable {} -extension SessionProtos_Content.ExpirationType: @unchecked Sendable {} -extension SessionProtos_CallMessage: @unchecked Sendable {} -extension SessionProtos_CallMessage.TypeEnum: @unchecked Sendable {} -extension SessionProtos_KeyPair: @unchecked Sendable {} -extension SessionProtos_DataExtractionNotification: @unchecked Sendable {} -extension SessionProtos_DataExtractionNotification.TypeEnum: @unchecked Sendable {} -extension SessionProtos_LokiProfile: @unchecked Sendable {} -extension SessionProtos_DataMessage: @unchecked Sendable {} -extension SessionProtos_DataMessage.Flags: @unchecked Sendable {} -extension SessionProtos_DataMessage.Quote: @unchecked Sendable {} -extension SessionProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {} -extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: @unchecked Sendable {} -extension SessionProtos_DataMessage.Preview: @unchecked Sendable {} -extension SessionProtos_DataMessage.Reaction: @unchecked Sendable {} -extension SessionProtos_DataMessage.Reaction.Action: @unchecked Sendable {} -extension SessionProtos_DataMessage.OpenGroupInvitation: @unchecked Sendable {} -extension SessionProtos_ReceiptMessage: @unchecked Sendable {} -extension SessionProtos_ReceiptMessage.TypeEnum: @unchecked Sendable {} -extension SessionProtos_AttachmentPointer: @unchecked Sendable {} -extension SessionProtos_AttachmentPointer.Flags: @unchecked Sendable {} -extension SessionProtos_GroupUpdateMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateInviteMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdatePromoteMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateInfoChangeMessage.TypeEnum: @unchecked Sendable {} -extension SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateMemberChangeMessage.TypeEnum: @unchecked Sendable {} -extension SessionProtos_GroupUpdateMemberLeftMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateMemberLeftNotificationMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateInviteResponseMessage: @unchecked Sendable {} -extension SessionProtos_GroupUpdateDeleteMemberContentMessage: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "SessionProtos" @@ -2003,7 +1686,11 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm var _expirationTimer: UInt32? = nil var _sigTimestamp: UInt64? = nil - static let defaultInstance = _StorageClass() + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() private init() {} @@ -2321,6 +2008,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "displayName"), 2: .same(proto: "profilePicture"), + 3: .same(proto: "profileUpdateTimestamp"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2331,6 +2019,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self._displayName) }() case 2: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self._profileUpdateTimestamp) }() default: break } } @@ -2347,12 +2036,16 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = self._profilePicture { try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() + try { if let v = self._profileUpdateTimestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: SessionProtos_LokiProfile, rhs: SessionProtos_LokiProfile) -> Bool { if lhs._displayName != rhs._displayName {return false} if lhs._profilePicture != rhs._profilePicture {return false} + if lhs._profileUpdateTimestamp != rhs._profileUpdateTimestamp {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2391,7 +2084,11 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _blocksCommunityMessageRequests: Bool? = nil var _groupUpdateMessage: SessionProtos_GroupUpdateMessage? = nil - static let defaultInstance = _StorageClass() + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() private init() {} @@ -3026,7 +2723,11 @@ extension SessionProtos_GroupUpdateMessage: SwiftProtobuf.Message, SwiftProtobuf var _deleteMemberContent: SessionProtos_GroupUpdateDeleteMemberContentMessage? = nil var _memberLeftNotificationMessage: SessionProtos_GroupUpdateMemberLeftNotificationMessage? = nil - static let defaultInstance = _StorageClass() + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() private init() {} @@ -3389,8 +3090,8 @@ extension SessionProtos_GroupUpdateMemberLeftMessage: SwiftProtobuf.Message, Swi static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -3408,8 +3109,8 @@ extension SessionProtos_GroupUpdateMemberLeftNotificationMessage: SwiftProtobuf. static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { diff --git a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift index 2fe1650440..56b33e00e2 100644 --- a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift +++ b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: WebSocketResources.proto @@ -28,7 +29,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct WebSocketProtos_WebSocketRequestMessage { +struct WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -84,7 +85,7 @@ struct WebSocketProtos_WebSocketRequestMessage { fileprivate var _requestID: UInt64? = nil } -struct WebSocketProtos_WebSocketResponseMessage { +struct WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -139,7 +140,7 @@ struct WebSocketProtos_WebSocketResponseMessage { fileprivate var _body: Data? = nil } -struct WebSocketProtos_WebSocketMessage { +struct WebSocketProtos_WebSocketMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -174,33 +175,15 @@ struct WebSocketProtos_WebSocketMessage { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case unknown // = 0 - case request // = 1 - case response // = 2 + enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { + case unknown = 0 + case request = 1 + case response = 2 init() { self = .unknown } - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .request - case 2: self = .response - default: return nil - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .request: return 1 - case .response: return 2 - } - } - } init() {} @@ -210,21 +193,6 @@ struct WebSocketProtos_WebSocketMessage { fileprivate var _response: WebSocketProtos_WebSocketResponseMessage? = nil } -#if swift(>=4.2) - -extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable { - // Support synthesized by the compiler. -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {} -extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {} -extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {} -extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "WebSocketProtos" diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 13ca774aec..74e42824b5 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -115,6 +115,8 @@ message DataExtractionNotification { message LokiProfile { optional string displayName = 1; optional string profilePicture = 2; + + optional uint64 profileUpdateTimestamp = 3; // Timestamp of the last profile update } message DataMessage { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 95a548578b..25b34fadd9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -140,12 +140,14 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, + profileUpdateTimestamp: profileUpdateTimestamp, sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) @@ -244,12 +246,14 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, + profileUpdateTimestamp: profileUpdateTimestamp, sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) @@ -604,12 +608,14 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, + profileUpdateTimestamp: profileUpdateTimestamp, sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index b5d2893a4d..67d0f8a8ec 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -27,12 +27,14 @@ extension MessageReceiver { // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { let messageSentTimestamp: TimeInterval = TimeInterval(Double(message.sentTimestampMs ?? 0) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) try Profile.updateIfNeeded( db, publicKey: senderId, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), + profileUpdateTimestamp: profileUpdateTimestamp, sentTimestamp: messageSentTimestamp, using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 4804c91a27..a28ed01fea 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -37,12 +37,14 @@ extension MessageReceiver { // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, + profileUpdateTimestamp: profileUpdateTimestamp, sentTimestamp: messageSentTimestamp, using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 510819e3f4..f29f9a568a 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -170,7 +170,8 @@ public final class MessageSender { VisibleMessage.VMProfile( displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, - profilePictureUrl: profile.displayPictureUrl + profilePictureUrl: profile.displayPictureUrl, + updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) } ) } } @@ -269,6 +270,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, + updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) }, blocksCommunityMessageRequests: !checkForCommunityMessageRequests ) } @@ -334,7 +336,8 @@ public final class MessageSender { VisibleMessage.VMProfile( displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, - profilePictureUrl: profile.displayPictureUrl + profilePictureUrl: profile.displayPictureUrl, + updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) } ) } diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 6277b15878..d38c4841d3 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -83,6 +83,7 @@ public extension Profile { publicKey: userSessionId.hexString, displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, + profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -106,6 +107,7 @@ public extension Profile { filePath: result.filePath, sessionProProof: sessionProProof ), + profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -130,6 +132,7 @@ public extension Profile { displayNameUpdate: DisplayNameUpdate = .none, displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, + profileUpdateTimestamp: TimeInterval, sentTimestamp: TimeInterval, using dependencies: Dependencies ) throws { @@ -152,13 +155,12 @@ public extension Profile { } // Name - // FIXME: This 'lastNameUpdate' approach is buggy - we should have a timestamp on the ConvoInfoVolatile - switch (displayNameUpdate, isCurrentUser, (sentTimestamp > convertToSections(profile.lastNameUpdate))) { + switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSections(profile.lastNameUpdate))) { case (.none, _, _): break - case (.currentUserUpdate(let name), true, _), (.contactUpdate(let name), false, true): + case (.currentUserUpdate(let name), true, true), (.contactUpdate(let name), false, true): guard let name: String = name, !name.isEmpty, name != profile.name else { break } - profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) + profileChanges.append(Profile.Columns.lastNameUpdate.set(to: profileUpdateTimestamp)) if profile.name != name { profileChanges.append(Profile.Columns.name.set(to: name)) @@ -176,13 +178,13 @@ public extension Profile { } // Profile picture & profile key - switch (displayPictureUpdate, isCurrentUser) { - case (.none, _): break - case (.currentUserUploadImageData, _), (.groupRemove, _), (.groupUpdateTo, _): + switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSections(profile.displayPictureLastUpdated))) { + case (.none, _, _): break + case (.currentUserUploadImageData, _, _), (.groupRemove, _, _), (.groupUpdateTo, _, _): preconditionFailure("Invalid options for this function") - case (.contactRemove, false), (.currentUserRemove, true): - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: sentTimestamp)) + case (.contactRemove, false, true), (.currentUserRemove, true, true): + profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: profileUpdateTimestamp)) if profile.displayPictureEncryptionKey != nil { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: nil)) @@ -193,8 +195,8 @@ public extension Profile { db.addProfileEvent(id: publicKey, change: .displayPictureUrl(nil)) } - case (.contactUpdateTo(let url, let key, let filePath, let proProof), false), - (.currentUserUpdateTo(let url, let key, let filePath, let proProof), true): + case (.contactUpdateTo(let url, let key, let filePath, let proProof), false, true), + (.currentUserUpdateTo(let url, let key, let filePath, let proProof), true, true): /// If we have already downloaded the image then no need to download it again (the database records will be updated /// once the download completes) if !dependencies[singleton: .fileManager].fileExists(atPath: filePath) { @@ -205,7 +207,7 @@ public extension Profile { shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .profile(id: profile.id, url: url, encryptionKey: key), - timestamp: sentTimestamp + timestamp: profileUpdateTimestamp ) ), canStartJob: dependencies[singleton: .appContext].isMainApp @@ -221,7 +223,8 @@ public extension Profile { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: key)) } - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: sentTimestamp)) + profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: profileUpdateTimestamp)) + profileChanges.append(Profile.Columns.sessionProProof.set(to: proProof)) } /// Don't want profiles in messages to modify the current users profile info so ignore those cases diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 4f39cce866..6a533ec77b 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -56,12 +56,15 @@ public extension ProfilePictureView { return profile.shoudAnimateProfilePicture(using: dependencies) }() /// If we are given an explicit `displayPictureUrl` then only use that - return (Info( - source: .url(URL(fileURLWithPath: path)), - shouldAnimated: shouldAnimated, - isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), - icon: profileIcon, - ), nil) + return ( + Info( + source: .url(URL(fileURLWithPath: path)), + shouldAnimated: shouldAnimated, + isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), + icon: profileIcon + ), + nil + ) case (_, _, .community): return ( From badb738549aa50ebbfb4bf9c605a8f2060b76a61 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Aug 2025 15:34:13 +1000 Subject: [PATCH 028/244] Fixed some missing renames --- Session.xcodeproj/project.pbxproj | 4 ---- SessionMessagingKit/Crypto/Crypto+Attachments.swift | 2 +- .../Database/Migrations/_026_MessageDeduplicationTable.swift | 2 +- .../Database/Migrations/_028_RenameAttachments.swift | 2 +- .../Database/Models/MessageDeduplication.swift | 2 +- .../Jobs/RetrieveDefaultOpenGroupRoomsJob.swift | 2 +- .../Sending & Receiving/AttachmentUploader.swift | 2 +- SessionMessagingKit/Utilities/AttachmentManager.swift | 2 +- SessionMessagingKit/Utilities/ExtensionHelper.swift | 2 +- .../Database/Models/MessageDeduplicationSpec.swift | 2 +- SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift | 2 +- .../_TestUtilities/MockExtensionHelper.swift | 2 +- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 13 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 88ed056459..7f35e099db 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1018,7 +1018,6 @@ FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */; }; FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; FDE5218E2E03A06B00061B8E /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218D2E03A06700061B8E /* AttachmentManager.swift */; }; - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */; }; FDE521942E050B1100061B8E /* DismissCallbackAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */; }; FDE5219A2E08DBB800061B8E /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */; }; FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */; }; @@ -2281,7 +2280,6 @@ FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_GroupsExpiredFlag.swift; sourceTree = ""; }; FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; FDE5218D2E03A06700061B8E /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Utilities.swift"; sourceTree = ""; }; FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissCallbackAVPlayerViewController.swift; sourceTree = ""; }; FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAsyncImage.swift; sourceTree = ""; }; @@ -3951,7 +3949,6 @@ isa = PBXGroup; children = ( FDB3DA892E2482A400148F8D /* AVURLAsset+Utilities.swift */, - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */, 94C58AC82D2E036E00609195 /* Permissions.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */, @@ -6342,7 +6339,6 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */, FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, diff --git a/SessionMessagingKit/Crypto/Crypto+Attachments.swift b/SessionMessagingKit/Crypto/Crypto+Attachments.swift index 6caca2f459..ceb7cca191 100644 --- a/SessionMessagingKit/Crypto/Crypto+Attachments.swift +++ b/SessionMessagingKit/Crypto/Crypto+Attachments.swift @@ -4,7 +4,7 @@ import Foundation import CommonCrypto -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Encryption diff --git a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift b/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift index fd993ac86b..5addd66869 100644 --- a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift +++ b/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// The different platforms use different approaches for message deduplication but in the future we want to shift the database logic into /// `libSession` so it makes sense to try to define a longer-term deduplication approach we we can use in `libSession`, additonally diff --git a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift b/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift index 2c92c21d11..a4e8969695 100644 --- a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift +++ b/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift @@ -3,7 +3,7 @@ import Foundation import UniformTypeIdentifiers import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// This migration renames all attachments to use a hash of the download url for the filename instead of a random UUID (means we can diff --git a/SessionMessagingKit/Database/Models/MessageDeduplication.swift b/SessionMessagingKit/Database/Models/MessageDeduplication.swift index 15269d25b2..6a73d52936 100644 --- a/SessionMessagingKit/Database/Models/MessageDeduplication.swift +++ b/SessionMessagingKit/Database/Models/MessageDeduplication.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index a864b6c2fe..06d2b8e3b9 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift index 532c5fe5ec..dec4a6495e 100644 --- a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift +++ b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - AttachmentUploader diff --git a/SessionMessagingKit/Utilities/AttachmentManager.swift b/SessionMessagingKit/Utilities/AttachmentManager.swift index 33f95cdb4c..621a0a6d1e 100644 --- a/SessionMessagingKit/Utilities/AttachmentManager.swift +++ b/SessionMessagingKit/Utilities/AttachmentManager.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index 0b04520659..ed44a08963 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 458c7cc357..bd36080603 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 16d88729ea..ca8c0919b6 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift index e68a787acd..06a4562e50 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e6a4e41559..214fd1d28d 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionUtilitiesKit @testable import Session -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class OnboardingSpec: AsyncSpec { From 69d5544a3bad0f526b86456f711e8e9927627676 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 7 Aug 2025 15:48:09 +1000 Subject: [PATCH 029/244] minor fix on profile update timestamp default value --- Session.xcodeproj/project.pbxproj | 8 ++++++++ .../Message Handling/MessageReceiver+Groups.swift | 6 +++--- .../MessageReceiver+MessageRequests.swift | 5 +++-- .../MessageReceiver+VisibleMessages.swift | 5 +++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 78bfc7d05b..37ee53ca9f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8389,6 +8389,7 @@ FD2272502C32910F004D8A6C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_DYNAMIC_NO_PIC = NO; GENERATE_INFOPLIST_FILE = YES; @@ -8423,6 +8424,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -8514,6 +8516,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -8590,6 +8593,7 @@ FD860CBF2D6E981900BBE29C /* Debug_Compile_LibSession */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = SUQ8J2PCT7; GENERATE_INFOPLIST_FILE = YES; PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; PRODUCT_NAME = SessionSnodeKitTests; @@ -8621,6 +8625,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -9204,6 +9209,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9236,6 +9242,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9267,6 +9274,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 25b34fadd9..9cb37a963e 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -140,7 +140,7 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, @@ -246,7 +246,7 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, @@ -608,7 +608,7 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index 67d0f8a8ec..3e58042901 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -26,8 +26,9 @@ extension MessageReceiver { // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { - let messageSentTimestamp: TimeInterval = TimeInterval(Double(message.sentTimestampMs ?? 0) / 1000) - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) + let messageSentTimestampMs: UInt64 = message.sentTimestampMs ?? 0 + let messageSentTimestamp: TimeInterval = TimeInterval(Double(messageSentTimestampMs) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? messageSentTimestampMs) / 1000) try Profile.updateIfNeeded( db, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a28ed01fea..6c23c7b46c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -31,13 +31,14 @@ extension MessageReceiver { // Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to // seconds to maintain the accuracy) - let messageSentTimestamp: TimeInterval = TimeInterval(Double(message.sentTimestampMs ?? 0) / 1000) + let messageSentTimestampMs: UInt64 = message.sentTimestampMs ?? 0 + let messageSentTimestamp: TimeInterval = TimeInterval(Double(messageSentTimestampMs) / 1000) let isMainAppActive: Bool = dependencies[defaults: .appGroup, key: .isMainAppActive] // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? 0) / 1000) + let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? messageSentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, From a758139b1b4e2e85fc33b34bbccb76d1da2692d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Aug 2025 16:31:14 +1000 Subject: [PATCH 030/244] Fixed a couple of build issues --- .../Utilities/ExtensionHelperSpec.swift | 14 +++++++------- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index ca8c0919b6..5cd9216cab 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -738,7 +738,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2332,7 +2332,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2369,7 +2369,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2385,7 +2385,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2422,7 +2422,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2438,7 +2438,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2480,7 +2480,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 214fd1d28d..e351b5a8b5 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -26,7 +26,7 @@ class OnboardingSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], From 2d8675fcc103665bac6560776617becda8ea89d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 8 Aug 2025 13:04:35 +1000 Subject: [PATCH 031/244] Moved all migrations into one target, simplified migration logic --- Session.xcodeproj/project.pbxproj | 388 ++++++++---------- Session/Meta/AppDelegate.swift | 3 - SessionMessagingKit/Configuration.swift | 99 +++-- .../_001_SUK_InitialSetupMigration.swift | 6 +- .../_002_SUK_SetupStandardJobs.swift | 6 +- .../_003_SUK_YDBToGRDBMigration.swift | 5 +- .../_004_SNK_InitialSetupMigration.swift | 5 +- .../_005_SNK_SetupStandardJobs.swift | 5 +- ...t => _006_SMK_InitialSetupMigration.swift} | 13 +- ...swift => _007_SMK_SetupStandardJobs.swift} | 5 +- .../_008_SNK_YDBToGRDBMigration.swift | 6 +- ...wift => _009_SMK_YDBToGRDBMigration.swift} | 5 +- ...10_FlagMessageHashAsDeletedOrInvalid.swift | 5 +- ...cyYDB.swift => _011_RemoveLegacyYDB.swift} | 5 +- .../Migrations/_012_AddJobPriority.swift | 6 +- ... => _013_FixDeletedMessageReadState.swift} | 5 +- ...ft => _014_FixHiddenModAdminSupport.swift} | 5 +- ...> _015_HomeQueryOptimisationIndexes.swift} | 5 +- .../Migrations/_016_ThemePreferences.swift | 27 +- ...ojiReacts.swift => _017_EmojiReacts.swift} | 5 +- ...n.swift => _018_OpenGroupPermission.swift} | 5 +- ...oFTS.swift => _019_AddThreadIdToFTS.swift} | 7 +- .../Migrations/_020_AddJobUniqueHash.swift | 6 +- ...ddSnodeReveivedMessageInfoPrimaryKey.swift | 6 +- .../Migrations/_022_DropSnodeCache.swift | 5 +- .../_023_SplitSnodeReceivedMessageInfo.swift | 6 +- .../_024_ResetUserConfigLastHashes.swift | 6 +- ...wift => _025_AddPendingReadReceipts.swift} | 5 +- ...Needed.swift => _026_AddFTSIfNeeded.swift} | 7 +- ...es.swift => _027_SessionUtilChanges.swift} | 7 +- ..._028_GenerateInitialUserConfigDumps.swift} | 5 +- ... _029_BlockCommunityMessageRequests.swift} | 5 +- ...MakeBrokenProfileTimestampsNullable.swift} | 5 +- ...ft => _031_RebuildFTSIfNeeded_2_4_5.swift} | 13 +- ...2_DisappearingMessagesConfiguration.swift} | 5 +- ...t => _033_ScheduleAppUpdateCheckJob.swift} | 5 +- ...swift => _034_AddMissingWhisperFlag.swift} | 5 +- ....swift => _035_ReworkRecipientState.swift} | 7 +- ....swift => _036_GroupsRebuildChanges.swift} | 7 +- ...lag.swift => _037_GroupsExpiredFlag.swift} | 5 +- ...=> _038_FixBustedInteractionVariant.swift} | 5 +- ...9_DropLegacyClosedGroupKeyPairTable.swift} | 5 +- ...t => _040_MessageDeduplicationTable.swift} | 9 +- ...41_RenameTableSettingToKeyValueStore.swift | 6 +- ...ft => _042_MoveSettingsToLibSession.swift} | 5 +- ...nts.swift => _043_RenameAttachments.swift} | 5 +- ...lag.swift => _044_AddProMessageFlag.swift} | 5 +- .../Models/MessageDeduplication.swift | 10 +- .../Models/MessageDeduplicationSpec.swift | 4 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 5 +- .../Jobs/MessageSendJobSpec.swift | 5 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 5 +- .../LibSession/LibSessionGroupInfoSpec.swift | 6 +- .../LibSessionGroupMembersSpec.swift | 5 +- .../LibSession/LibSessionSpec.swift | 5 +- .../Open Groups/OpenGroupManagerSpec.swift | 5 +- .../MessageReceiverGroupsSpec.swift | 6 +- .../MessageSenderGroupsSpec.swift | 5 +- .../MessageSenderSpec.swift | 5 +- .../Pollers/CommunityPollerSpec.swift | 5 +- .../Utilities/ExtensionHelperSpec.swift | 5 +- SessionNetworkingKit/Configuration.swift | 35 -- .../ShareNavController.swift | 1 - ...eadDisappearingMessagesViewModelSpec.swift | 7 +- ...eadNotificationSettingsViewModelSpec.swift | 7 +- .../ThreadSettingsViewModelSpec.swift | 7 +- SessionTests/Database/DatabaseSpec.swift | 178 ++++---- SessionTests/Onboarding/OnboardingSpec.swift | 7 +- .../NotificationContentViewModelSpec.swift | 7 +- SessionUtilitiesKit/Configuration.swift | 31 +- SessionUtilitiesKit/Database/Storage.swift | 71 +--- .../Database/Types/Migration.swift | 16 +- .../Database/Types/TargetMigrations.swift | 76 ---- .../DatabaseMigrator+Utilities.swift | 22 - .../Database/Models/IdentitySpec.swift | 4 +- .../JobRunner/JobRunnerSpec.swift | 12 +- SignalUtilitiesKit/Utilities/AppSetup.swift | 10 +- _SharedTestUtilities/SynchronousStorage.swift | 18 +- 78 files changed, 473 insertions(+), 883 deletions(-) rename SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift => SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift (94%) rename SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift => SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift (89%) rename SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift => SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift (70%) rename SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift => SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift (90%) rename SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift => SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift (91%) rename SessionMessagingKit/Database/Migrations/{_001_InitialSetupMigration.swift => _006_SMK_InitialSetupMigration.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_002_SetupStandardJobs.swift => _007_SMK_SetupStandardJobs.swift} (93%) rename SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift => SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift (69%) rename SessionMessagingKit/Database/Migrations/{_003_YDBToGRDBMigration.swift => _009_SMK_YDBToGRDBMigration.swift} (79%) rename SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift => SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift (81%) rename SessionMessagingKit/Database/Migrations/{_004_RemoveLegacyYDB.swift => _011_RemoveLegacyYDB.swift} (78%) rename SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift => SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift (90%) rename SessionMessagingKit/Database/Migrations/{_005_FixDeletedMessageReadState.swift => _013_FixDeletedMessageReadState.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_006_FixHiddenModAdminSupport.swift => _014_FixHiddenModAdminSupport.swift} (85%) rename SessionMessagingKit/Database/Migrations/{_007_HomeQueryOptimisationIndexes.swift => _015_HomeQueryOptimisationIndexes.swift} (81%) rename Session/Database/Migrations/_001_ThemePreferences.swift => SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift (78%) rename SessionMessagingKit/Database/Migrations/{_008_EmojiReacts.swift => _017_EmojiReacts.swift} (91%) rename SessionMessagingKit/Database/Migrations/{_009_OpenGroupPermission.swift => _018_OpenGroupPermission.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_010_AddThreadIdToFTS.swift => _019_AddThreadIdToFTS.swift} (82%) rename SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift => SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift (76%) rename SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift => SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift (91%) rename SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift => SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift (86%) rename SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift => SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift (96%) rename SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift => SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift (84%) rename SessionMessagingKit/Database/Migrations/{_011_AddPendingReadReceipts.swift => _025_AddPendingReadReceipts.swift} (89%) rename SessionMessagingKit/Database/Migrations/{_012_AddFTSIfNeeded.swift => _026_AddFTSIfNeeded.swift} (80%) rename SessionMessagingKit/Database/Migrations/{_013_SessionUtilChanges.swift => _027_SessionUtilChanges.swift} (98%) rename SessionMessagingKit/Database/Migrations/{_014_GenerateInitialUserConfigDumps.swift => _028_GenerateInitialUserConfigDumps.swift} (98%) rename SessionMessagingKit/Database/Migrations/{_015_BlockCommunityMessageRequests.swift => _029_BlockCommunityMessageRequests.swift} (94%) rename SessionMessagingKit/Database/Migrations/{_016_MakeBrokenProfileTimestampsNullable.swift => _030_MakeBrokenProfileTimestampsNullable.swift} (89%) rename SessionMessagingKit/Database/Migrations/{_017_RebuildFTSIfNeeded_2_4_5.swift => _031_RebuildFTSIfNeeded_2_4_5.swift} (85%) rename SessionMessagingKit/Database/Migrations/{_018_DisappearingMessagesConfiguration.swift => _032_DisappearingMessagesConfiguration.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_019_ScheduleAppUpdateCheckJob.swift => _033_ScheduleAppUpdateCheckJob.swift} (84%) rename SessionMessagingKit/Database/Migrations/{_020_AddMissingWhisperFlag.swift => _034_AddMissingWhisperFlag.swift} (81%) rename SessionMessagingKit/Database/Migrations/{_021_ReworkRecipientState.swift => _035_ReworkRecipientState.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_022_GroupsRebuildChanges.swift => _036_GroupsRebuildChanges.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_023_GroupsExpiredFlag.swift => _037_GroupsExpiredFlag.swift} (76%) rename SessionMessagingKit/Database/Migrations/{_024_FixBustedInteractionVariant.swift => _038_FixBustedInteractionVariant.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_025_DropLegacyClosedGroupKeyPairTable.swift => _039_DropLegacyClosedGroupKeyPairTable.swift} (74%) rename SessionMessagingKit/Database/Migrations/{_026_MessageDeduplicationTable.swift => _040_MessageDeduplicationTable.swift} (98%) rename SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift => SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift (68%) rename SessionMessagingKit/Database/Migrations/{_027_MoveSettingsToLibSession.swift => _042_MoveSettingsToLibSession.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_028_RenameAttachments.swift => _043_RenameAttachments.swift} (99%) rename SessionMessagingKit/Database/Migrations/{_029_AddProMessageFlag.swift => _044_AddProMessageFlag.swift} (76%) delete mode 100644 SessionNetworkingKit/Configuration.swift delete mode 100644 SessionUtilitiesKit/Database/Types/TargetMigrations.swift delete mode 100644 SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7f35e099db..f4c84c3d12 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -98,7 +98,7 @@ 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; }; - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */; }; + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */; }; 7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; @@ -107,7 +107,7 @@ 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */; }; 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */; }; @@ -129,7 +129,7 @@ 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */; }; + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */; }; 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */; }; 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */; }; 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */; }; @@ -174,7 +174,6 @@ 9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */; }; 942ADDD42D9F9613006E0BB0 /* NewTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */; }; 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; }; - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */; }; 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; }; @@ -183,7 +182,6 @@ 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */; }; 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */; }; 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */; }; - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */; }; 947D7FE72D51837200E8E413 /* ArrowCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */; }; 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; @@ -202,7 +200,7 @@ 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BC2E0908340097754D /* LibSession+Pro.swift */; }; - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */; }; + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */; }; 94CD962D2E1B85920097754D /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962B2E1B85920097754D /* InputViewButton.swift */; }; 94CD962E2E1B85920097754D /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962A2E1B85920097754D /* InputTextView.swift */; }; 94CD96302E1B88430097754D /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */; }; @@ -375,7 +373,6 @@ C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; @@ -430,7 +427,7 @@ FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */; }; - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */; }; + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */; }; FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0559542E026CC900DC48CE /* ObservingDatabase.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; @@ -452,7 +449,7 @@ FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; }; FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.swift */; }; FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; }; - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; }; + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */; }; FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; @@ -475,18 +472,12 @@ FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; }; FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; }; FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */; }; - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; }; - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */; }; FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F527EAD44C00FF65E7 /* Storage.swift */; }; - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */; }; - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */; }; + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */; }; FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; }; FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; }; - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; }; - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; }; - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */; }; FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; }; - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */; }; FD19363F2ACA66DE004BCF0F /* DatabaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */; }; FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A553D2E14BE0E003761E4 /* PagedData.swift */; }; @@ -494,7 +485,7 @@ FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */; }; FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */; }; FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */; }; FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */; }; @@ -544,9 +535,7 @@ FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */; }; FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */; }; - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; @@ -622,8 +611,8 @@ FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */; }; FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150312CA24310005B08A1 /* RequestSpec.swift */; }; FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */; }; - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */; }; - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; }; + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */; }; + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */; }; FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; @@ -651,13 +640,12 @@ FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0028A60473003AE748 /* UIKit+Theme.swift */; }; FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */; }; FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */; }; - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */; }; + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */; }; FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */; }; FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; @@ -674,7 +662,7 @@ FD428B192B4B576F006D0888 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B182B4B576F006D0888 /* AppContext.swift */; }; FD428B1B2B4B6098006D0888 /* Notifications+Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */; }; FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1E2B4B758B006D0888 /* AppReadiness.swift */; }; - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */; }; + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */; }; FD42ECCE2E287CD4002D03EA /* ThemeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */; }; FD42ECD02E289261002D03EA /* ThemeLinearGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */; }; FD42ECD22E3071DE002D03EA /* ThemeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD12E3071DC002D03EA /* ThemeText.swift */; }; @@ -703,7 +691,7 @@ FD4BB22B2D63F20700D0DC3D /* MigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */; }; FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */ = {isa = PBXBuildFile; }; FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; }; + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB592E12166D00A4DA70 /* OnboardingSpec.swift */; }; @@ -727,7 +715,6 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; }; FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */; }; - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */; }; FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; @@ -751,17 +738,15 @@ FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */; }; + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; }; + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */; }; FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */; }; FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */; }; FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */; }; @@ -815,13 +800,13 @@ FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; }; + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */; }; FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB11A622DD5BDDD00BEF49F /* ImageDataManager+Singleton.swift */; }; FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754CB2C9BAF37002A2623 /* Data+Image.swift */; }; FD78E9F22DDA9EA200D55B50 /* MockImageDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */; }; FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */; }; FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F52DDD43AB00D55B50 /* Mutation.swift */; }; - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */; }; + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */; }; FD78E9FD2DDD97F200D55B50 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9FC2DDD97F000D55B50 /* Setting.swift */; }; FD78EA022DDEBC3200D55B50 /* DebounceTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */; }; FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */; }; @@ -829,7 +814,6 @@ FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0B2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */; }; - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */; }; FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */; }; FD7F745F2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */; }; FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; @@ -850,7 +834,7 @@ FD860CB62D66913F00BBE29C /* ThemePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */; }; FD860CB82D66BC9900BBE29C /* AppIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */; }; FD860CBA2D66BF2A00BBE29C /* AppIconGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */; }; - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */; }; + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */; }; FD860CBE2D6E7DAA00BBE29C /* DeveloperSettingsViewModel+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */; }; FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD860CC82D6ED2ED00BBE29C /* DifferenceKit */; }; FD86FDA32BC5020600EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; @@ -873,13 +857,11 @@ FD8A5B252DC05B16004C689B /* Number+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B242DC05B16004C689B /* Number+Utilities.swift */; }; FD8A5B292DC060E2004C689B /* Double+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B282DC060DD004C689B /* Double+Utilities.swift */; }; FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */; }; - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */; }; - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */; }; + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */; }; FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */; }; - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; }; + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; }; FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; }; - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; }; FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; }; FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */; }; @@ -929,7 +911,7 @@ FDB3DA8D2E24881B00148F8D /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */; }; FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */; }; FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */; }; - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */; }; + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */; }; FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */; }; FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */; }; FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */; }; @@ -953,7 +935,6 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */; }; FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */; }; - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; @@ -1007,15 +988,33 @@ FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */; }; + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */; }; + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */; }; + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */; }; + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */; }; + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */; }; + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */; }; + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */; }; + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */; }; + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */; }; + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */; }; - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */; }; + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */; }; FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; FDE5218E2E03A06B00061B8E /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218D2E03A06700061B8E /* AttachmentManager.swift */; }; FDE521942E050B1100061B8E /* DismissCallbackAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */; }; @@ -1066,7 +1065,7 @@ FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */; }; FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */; }; FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */; }; - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */; }; + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */; }; FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */; }; FDE755062C9BB4EE002A2623 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755042C9BB4ED002A2623 /* Bencode.swift */; }; FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */; }; @@ -1087,7 +1086,7 @@ FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */; }; + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */; }; FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; }; FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */; }; FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */; }; @@ -1101,7 +1100,7 @@ FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; }; FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; }; FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */; }; - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */; }; + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */; }; FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */; }; FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */; }; FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; @@ -1151,7 +1150,7 @@ FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; }; FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; }; FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; }; - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */; }; + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */; }; FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; @@ -1458,7 +1457,7 @@ 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewTitleView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = ""; }; - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _032_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView_SwiftUI.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; @@ -1467,7 +1466,7 @@ 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _015_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = ""; }; @@ -1490,7 +1489,7 @@ 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_OpenGroupPermission.swift; sourceTree = ""; }; + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_OpenGroupPermission.swift; sourceTree = ""; }; 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoView.swift; sourceTree = ""; }; 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Draggable.swift"; sourceTree = ""; }; 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = ""; }; @@ -1542,7 +1541,7 @@ 94367C422C6C828500814252 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+DisappearingMessages.swift"; sourceTree = ""; }; 943C6D832B86B5F1004ACE64 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = ""; }; + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddJobUniqueHash.swift; sourceTree = ""; }; 9471CAA72CACFB4E00090FB7 /* GenerateLicenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateLicenses.swift; sourceTree = ""; }; 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = ""; }; @@ -1554,7 +1553,7 @@ 947D7FD92D5180F200E8E413 /* SessionNetworkScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkScreen.swift; sourceTree = ""; }; 947D7FDA2D5180F200E8E413 /* SessionNetworkScreen+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+Models.swift"; sourceTree = ""; }; 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+ViewModel.swift"; sourceTree = ""; }; - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _041_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowCapsule.swift; sourceTree = ""; }; 947D7FE52D51837200E8E413 /* PopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; @@ -1572,7 +1571,7 @@ 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; 94CD95BC2E0908340097754D /* LibSession+Pro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Pro.swift"; sourceTree = ""; }; - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_AddProMessageFlag.swift; sourceTree = ""; }; + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _044_AddProMessageFlag.swift; sourceTree = ""; }; 94CD962A2E1B85920097754D /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 94CD962B2E1B85920097754D /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = ""; }; 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; @@ -1743,7 +1742,6 @@ C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionNetworkingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionNetworkingKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; @@ -1802,7 +1800,7 @@ FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = ""; }; FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipPayloadKey.swift; sourceTree = ""; }; - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_RenameAttachments.swift; sourceTree = ""; }; + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _043_RenameAttachments.swift; sourceTree = ""; }; FD0559542E026CC900DC48CE /* ObservingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingDatabase.swift; sourceTree = ""; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = ""; }; FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = ""; }; @@ -1821,7 +1819,7 @@ FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.swift; sourceTree = ""; }; FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = ""; }; + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_EmojiReacts.swift; sourceTree = ""; }; FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = ""; }; FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; @@ -1841,19 +1839,17 @@ FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedObservationSource.swift; sourceTree = ""; }; FD12A8482AD63C4700EEBA0D /* SessionNavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNavItem.swift; sourceTree = ""; }; FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilePictureView+Convenience.swift"; sourceTree = ""; }; - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_SMK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_SMK_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_SNK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_SNK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FD17D7B727F51ECA00122BE0 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetMigrations.swift; sourceTree = ""; }; FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressible.swift; sourceTree = ""; }; - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = ""; }; - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_SUK_InitialSetupMigration.swift; sourceTree = ""; }; FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_SUK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSpec.swift; sourceTree = ""; }; FD1A553D2E14BE0E003761E4 /* PagedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedData.swift; sourceTree = ""; }; @@ -1861,7 +1857,7 @@ FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKeyEvent+Utilities.swift"; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; @@ -1943,8 +1939,8 @@ FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwarmPoller.swift; sourceTree = ""; }; FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerSpec.swift; sourceTree = ""; }; FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPoller.swift; sourceTree = ""; }; - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddMissingWhisperFlag.swift; sourceTree = ""; }; - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _034_AddMissingWhisperFlag.swift; sourceTree = ""; }; + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _026_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSnodeAPICache.swift; sourceTree = ""; }; FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSSKMockExtensions.swift; sourceTree = ""; }; @@ -1965,7 +1961,7 @@ FD37E9DA28A244E9003AE748 /* ThemeMessagePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeMessagePreviewView.swift; sourceTree = ""; }; FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryColorSelectionView.swift; sourceTree = ""; }; FD37E9F528A5F106003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_ThemePreferences.swift; sourceTree = ""; }; + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_ThemePreferences.swift; sourceTree = ""; }; FD37E9FE28A5F2CD003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD37EA0028A60473003AE748 /* UIKit+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Theme.swift"; sourceTree = ""; }; FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewModel.swift; sourceTree = ""; }; @@ -1973,13 +1969,13 @@ FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewController.swift; sourceTree = ""; }; FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewModel.swift; sourceTree = ""; }; FD37EA0A28AB12E2003AE748 /* SessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCell.swift; sourceTree = ""; }; - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_FixDeletedMessageReadState.swift; sourceTree = ""; }; + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_FixHiddenModAdminSupport.swift; sourceTree = ""; }; FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = ""; }; FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -1995,7 +1991,7 @@ FD428B182B4B576F006D0888 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notifications+Lifecycle.swift"; sourceTree = ""; }; FD428B1E2B4B758B006D0888 /* AppReadiness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReadiness.swift; sourceTree = ""; }; - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _031_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColor.swift; sourceTree = ""; }; FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeLinearGradient.swift; sourceTree = ""; }; FD42ECD12E3071DC002D03EA /* ThemeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeText.swift; sourceTree = ""; }; @@ -2014,7 +2010,7 @@ FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = ""; }; FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationHelper.swift; sourceTree = ""; }; FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureError.swift; sourceTree = ""; }; - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_ReworkRecipientState.swift; sourceTree = ""; }; + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _035_ReworkRecipientState.swift; sourceTree = ""; }; FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = ""; }; @@ -2037,22 +2033,22 @@ FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = ""; }; - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionNetworkingKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_SNK_SetupStandardJobs.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _026_MessageDeduplicationTable.swift; sourceTree = ""; }; + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _040_MessageDeduplicationTable.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_AddThreadIdToFTS.swift; sourceTree = ""; }; + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_AddThreadIdToFTS.swift; sourceTree = ""; }; FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesSettingsViewModel.swift; sourceTree = ""; }; FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableBarButtonItem.swift; sourceTree = ""; }; FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Combine.swift"; sourceTree = ""; }; @@ -2097,18 +2093,18 @@ FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = ""; }; FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageDataManager.swift; sourceTree = ""; }; FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploader.swift; sourceTree = ""; }; FD78E9F52DDD43AB00D55B50 /* Mutation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutation.swift; sourceTree = ""; }; - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_MoveSettingsToLibSession.swift; sourceTree = ""; }; + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _042_MoveSettingsToLibSession.swift; sourceTree = ""; }; FD78E9FC2DDD97F000D55B50 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceTaskManager.swift; sourceTree = ""; }; FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiTaskManager.swift; sourceTree = ""; }; FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncSequence+Utilities.swift"; sourceTree = ""; }; FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interaction+UI.swift"; sourceTree = ""; }; FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Local.swift"; sourceTree = ""; }; - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_DropSnodeCache.swift; sourceTree = ""; }; + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_DropSnodeCache.swift; sourceTree = ""; }; FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSession.swift; sourceTree = ""; }; FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = ""; }; FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Networking.swift"; sourceTree = ""; }; @@ -2134,7 +2130,7 @@ FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreviewView.swift; sourceTree = ""; }; FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconViewModel.swift; sourceTree = ""; }; FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGridView.swift; sourceTree = ""; }; - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_FixBustedInteractionVariant.swift; sourceTree = ""; }; + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _038_FixBustedInteractionVariant.swift; sourceTree = ""; }; FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeveloperSettingsViewModel+Testing.swift"; sourceTree = ""; }; FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; @@ -2146,12 +2142,12 @@ FD8A5B242DC05B16004C689B /* Number+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Utilities.swift"; sourceTree = ""; }; FD8A5B282DC060DD004C689B /* Double+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Utilities.swift"; sourceTree = ""; }; FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralCacheSpec.swift; sourceTree = ""; }; - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _025_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_ResetUserConfigLastHashes.swift; sourceTree = ""; }; + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _039_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_ResetUserConfigLastHashes.swift; sourceTree = ""; }; FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+SessionMessagingKit.swift"; sourceTree = ""; }; - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = ""; }; + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_SessionUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = ""; }; - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SUK_SetupStandardJobs.swift; sourceTree = ""; }; FD9401CE2ABD04AC003A4834 /* TRANSLATIONS.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = TRANSLATIONS.md; sourceTree = ""; }; FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJobSpec.swift; sourceTree = ""; }; FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = ""; }; @@ -2194,7 +2190,7 @@ FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewError.swift; sourceTree = ""; }; FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Groups.swift"; sourceTree = ""; }; - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_GroupsRebuildChanges.swift; sourceTree = ""; }; + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _036_GroupsRebuildChanges.swift; sourceTree = ""; }; FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInviteMessage.swift; sourceTree = ""; }; FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoChangeMessage.swift; sourceTree = ""; }; FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberChangeMessage.swift; sourceTree = ""; }; @@ -2210,7 +2206,7 @@ FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHeaderCell.swift; sourceTree = ""; }; FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedGroupInvitesAndPromotionsJob.swift; sourceTree = ""; }; - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsConfig.swift; sourceTree = ""; }; FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = ""; }; @@ -2272,12 +2268,12 @@ FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponseSpec.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _033_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchTimeInterval+Utilities.swift"; sourceTree = ""; }; - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_GroupsExpiredFlag.swift; sourceTree = ""; }; + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _037_GroupsExpiredFlag.swift; sourceTree = ""; }; FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; FDE5218D2E03A06700061B8E /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissCallbackAVPlayerViewController.swift; sourceTree = ""; }; @@ -2331,7 +2327,7 @@ FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SMK.swift"; sourceTree = ""; }; FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _011_AddPendingReadReceipts.swift; sourceTree = ""; }; + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _025_AddPendingReadReceipts.swift; sourceTree = ""; }; FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; FDE755042C9BB4ED002A2623 /* Bencode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; @@ -2358,7 +2354,7 @@ FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonConfig.swift; sourceTree = ""; }; FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; FDF0B73F280402C4004C14C5 /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SMK_SetupStandardJobs.swift; sourceTree = ""; }; FDF0B7432804EF1B004C14C5 /* JobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunner.swift; sourceTree = ""; }; FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyModel.swift; sourceTree = ""; }; FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = ""; }; @@ -2373,7 +2369,7 @@ FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LegacyClosedGroups.swift"; sourceTree = ""; }; - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = ""; }; + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_RemoveLegacyYDB.swift; sourceTree = ""; }; FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionMessage.swift; sourceTree = ""; }; FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LibSession.swift"; sourceTree = ""; }; FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; @@ -2422,7 +2418,7 @@ FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = ""; }; FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2979,7 +2975,6 @@ B8A582AB258C64E800AFD84C /* Database */ = { isa = PBXGroup; children = ( - FD17D7C827F546CE00122BE0 /* Migrations */, FD17D7CB27F546F500122BE0 /* Models */, FD17D7B427F51E6700122BE0 /* Types */, FD17D7BB27F51F5C00122BE0 /* Utilities */, @@ -3676,7 +3671,6 @@ FD2272842C33E28D004D8A6C /* SnodeAPI */, FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, - C3C2A5B9255385ED00C340D1 /* Configuration.swift */, ); path = SessionNetworkingKit; sourceTree = ""; @@ -4012,35 +4006,50 @@ FD17D79427F3E03300122BE0 /* Migrations */ = { isa = PBXGroup; children = ( - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */, - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */, - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */, - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */, - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */, - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */, - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */, - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */, - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */, - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */, - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */, - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */, - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */, - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */, - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */, - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */, - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */, - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */, + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */, + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */, + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */, + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */, + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */, + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */, + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */, + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */, + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */, + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */, + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */, + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */, + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */, + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */, + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */, + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */, + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */, + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */, + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */, + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */, + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */, + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */, + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */, + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */, + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */, + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */, + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */, + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */, + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */, + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */, + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */, + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */, + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */, + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */, + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */, + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */, + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */, + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */, + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */, + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */, + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */, + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */, + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */, ); path = Migrations; sourceTree = ""; @@ -4048,27 +4057,11 @@ FD17D79D27F40CAA00122BE0 /* Database */ = { isa = PBXGroup; children = ( - FD17D79E27F40CC000122BE0 /* Migrations */, FD17D7A827F41BE300122BE0 /* Models */, ); path = Database; sourceTree = ""; }; - FD17D79E27F40CC000122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */, - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */, - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */, - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */, - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */, - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */, - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */, - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */, - ); - path = Migrations; - sourceTree = ""; - }; FD17D7A827F41BE300122BE0 /* Models */ = { isa = PBXGroup; children = ( @@ -4083,7 +4076,6 @@ FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */, - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD7162DA281B6C440060647B /* TypedTableAlias.swift */, FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */, FD1A553D2E14BE0E003761E4 /* PagedData.swift */, @@ -4094,7 +4086,6 @@ FD17D7BB27F51F5C00122BE0 /* Utilities */ = { isa = PBXGroup; children = ( - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */, FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, @@ -4105,19 +4096,6 @@ path = Utilities; sourceTree = ""; }; - FD17D7C827F546CE00122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */, - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */, - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */, - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */, - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */, - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */, - ); - path = Migrations; - sourceTree = ""; - }; FD17D7CB27F546F500122BE0 /* Models */ = { isa = PBXGroup; children = ( @@ -4259,7 +4237,6 @@ FD37E9F728A5F143003AE748 /* Migrations */ = { isa = PBXGroup; children = ( - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */, ); path = Migrations; sourceTree = ""; @@ -6028,7 +6005,6 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */, - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */, 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6200,14 +6176,12 @@ FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */, FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, @@ -6238,9 +6212,7 @@ FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, @@ -6251,18 +6223,13 @@ FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */, FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */, FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */, FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */, FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, @@ -6283,7 +6250,6 @@ files = ( FD2272C82C34EB0A004D8A6C /* Job.swift in Sources */, FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, @@ -6296,7 +6262,6 @@ FD42ECD62E3308B5002D03EA /* ObservableKey+SessionUtilitiesKit.swift in Sources */, FDE754D22C9BAF53002A2623 /* JobDependencies.swift in Sources */, FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */, - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */, 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */, FD00CDCB2D5317A7006B96D3 /* Scheduler+Utilities.swift in Sources */, FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */, @@ -6322,7 +6287,6 @@ FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */, FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */, @@ -6342,7 +6306,6 @@ FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */, FDB11A542DCD7A7F00BEF49F /* Task+Utilities.swift in Sources */, @@ -6352,11 +6315,9 @@ FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */, FD52CB652E13B6E900A4DA70 /* ObservationBuilder.swift in Sources */, FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */, - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */, FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */, FD78EA062DDEC8F600D55B50 /* AsyncSequence+Utilities.swift in Sources */, FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, @@ -6393,7 +6354,6 @@ FDE754D42C9BAF6B002A2623 /* UICollectionView+ReusableView.swift in Sources */, FDE754C02C9BAEF6002A2623 /* Array+Utilities.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, @@ -6414,7 +6374,6 @@ FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */, FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6424,10 +6383,11 @@ buildActionMask = 2147483647; files = ( FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */, - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */, + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, FD2273002C352D8E004D8A6C /* LibSession+GroupKeys.swift in Sources */, - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */, + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */, FD2272FB2C352D8E004D8A6C /* LibSession+UserGroups.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, FD22726F2C32911C004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift in Sources */, @@ -6437,7 +6397,7 @@ FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */, + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */, FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */, FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */, @@ -6451,7 +6411,7 @@ FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */, FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */, FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */, - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */, + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */, FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, FD2272FD2C352D8E004D8A6C /* LibSession+ConvoInfoVolatile.swift in Sources */, @@ -6466,28 +6426,28 @@ FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */, FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */, FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */, - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */, + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */, + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */, FD2272FC2C352D8E004D8A6C /* LibSession+Contacts.swift in Sources */, FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */, FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */, FD2272782C32911C004D8A6C /* AttachmentDownloadJob.swift in Sources */, FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */, - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */, + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */, FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */, FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */, - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */, - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */, + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */, 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */, - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */, FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */, 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, @@ -6497,7 +6457,7 @@ FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */, - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, @@ -6507,24 +6467,30 @@ FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */, FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */, - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */, FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */, FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */, FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */, FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */, FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */, + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */, FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */, + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */, FDB11A5D2DD300D300BEF49F /* SNProtoContent+Utilities.swift in Sources */, FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */, FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */, @@ -6540,11 +6506,11 @@ B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */, + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */, - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */, + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, FD2272FE2C352D8E004D8A6C /* LibSession+GroupMembers.swift in Sources */, FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */, @@ -6561,7 +6527,7 @@ FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */, FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */, - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, @@ -6574,8 +6540,9 @@ FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */, - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */, + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FDB11A502DCC6ADE00BEF49F /* ThreadUpdateInfo.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */, @@ -6584,11 +6551,14 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */, + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */, + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, @@ -6601,39 +6571,41 @@ FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */, - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */, + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */, FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */, FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */, FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */, FD981BD92DC9A69600564172 /* NotificationUserInfoKey.swift in Sources */, C352A2FF25574B6300338F3E /* (null) in Sources */, + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */, FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */, + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */, + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, FD2273012C352D8E004D8A6C /* LibSession+Shared.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, FD245C682850666300B966DD /* Message+Destination.swift in Sources */, FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */, + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, FD981BC62DC3310B00564172 /* ExtensionHelper.swift in Sources */, - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */, + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */, FD2272FF2C352D8E004D8A6C /* LibSession+UserProfile.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */, 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */, - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, @@ -6648,22 +6620,24 @@ FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */, FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */, + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */, FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */, FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */, - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */, + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */, FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FDB11A522DCC6B0000BEF49F /* OpenGroupUrlInfo.swift in Sources */, - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */, + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */, FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */, + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6678,7 +6652,6 @@ FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */, - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */, 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */, FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, @@ -6959,12 +6932,14 @@ FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD99D0922D10F5EE005D2E15 /* ThreadSafeSpec.swift in Sources */, + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDB11A592DD17D0600BEF49F /* MockLogger.swift in Sources */, FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */, + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */, @@ -6982,6 +6957,7 @@ FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */, FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */, FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index eea5572f6d..2d776443aa 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -70,7 +70,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) Log.info(.cat, "Setting up environment.") @@ -221,7 +220,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Dispatch async so things can continue to be progressed if a migration does need to run DispatchQueue.global(qos: .userInitiated).async { [weak self, dependencies] in AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -599,7 +597,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // The re-run the migration (should succeed since there is no data) AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2f861719a4..5260915ddc 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -2,58 +2,53 @@ import Foundation import GRDB import SessionUtilitiesKit -public enum SNMessagingKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .messagingKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_RemoveLegacyYDB.self - ], // Legacy DB removal - [ - _005_FixDeletedMessageReadState.self, - _006_FixHiddenModAdminSupport.self, - _007_HomeQueryOptimisationIndexes.self - ], // Add job priorities - [ - _008_EmojiReacts.self, - _009_OpenGroupPermission.self, - _010_AddThreadIdToFTS.self - ], // Fix thread FTS - [ - _011_AddPendingReadReceipts.self, - _012_AddFTSIfNeeded.self, - _013_SessionUtilChanges.self, - _014_GenerateInitialUserConfigDumps.self, - _015_BlockCommunityMessageRequests.self, - _016_MakeBrokenProfileTimestampsNullable.self, - _017_RebuildFTSIfNeeded_2_4_5.self, - _018_DisappearingMessagesConfiguration.self, - _019_ScheduleAppUpdateCheckJob.self, - _020_AddMissingWhisperFlag.self, - _021_ReworkRecipientState.self, - _022_GroupsRebuildChanges.self, - _023_GroupsExpiredFlag.self, - _024_FixBustedInteractionVariant.self, - _025_DropLegacyClosedGroupKeyPairTable.self, - _026_MessageDeduplicationTable.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [ - _027_MoveSettingsToLibSession.self, - _028_RenameAttachments.self, - _029_AddProMessageFlag.self - ] - ] - ) - } +public enum SNMessagingKit { // Just to make the external API nice + public static let migrations: [Migration.Type] = [ + _001_SUK_InitialSetupMigration.self, + _002_SUK_SetupStandardJobs.self, + _003_SUK_YDBToGRDBMigration.self, + _004_SNK_InitialSetupMigration.self, + _005_SNK_SetupStandardJobs.self, + _006_SMK_InitialSetupMigration.self, + _007_SMK_SetupStandardJobs.self, + _008_SNK_YDBToGRDBMigration.self, + _009_SMK_YDBToGRDBMigration.self, + _010_FlagMessageHashAsDeletedOrInvalid.self, + _011_RemoveLegacyYDB.self, + _012_AddJobPriority.self, + _013_FixDeletedMessageReadState.self, + _014_FixHiddenModAdminSupport.self, + _015_HomeQueryOptimisationIndexes.self, + _016_ThemePreferences.self, + _017_EmojiReacts.self, + _018_OpenGroupPermission.self, + _019_AddThreadIdToFTS.self, + _020_AddJobUniqueHash.self, + _021_AddSnodeReveivedMessageInfoPrimaryKey.self, + _022_DropSnodeCache.self, + _023_SplitSnodeReceivedMessageInfo.self, + _024_ResetUserConfigLastHashes.self, + _025_AddPendingReadReceipts.self, + _026_AddFTSIfNeeded.self, + _027_SessionUtilChanges.self, + _028_GenerateInitialUserConfigDumps.self, + _029_BlockCommunityMessageRequests.self, + _030_MakeBrokenProfileTimestampsNullable.self, + _031_RebuildFTSIfNeeded_2_4_5.self, + _032_DisappearingMessagesConfiguration.self, + _033_ScheduleAppUpdateCheckJob.self, + _034_AddMissingWhisperFlag.self, + _035_ReworkRecipientState.self, + _036_GroupsRebuildChanges.self, + _037_GroupsExpiredFlag.self, + _038_FixBustedInteractionVariant.self, + _039_DropLegacyClosedGroupKeyPairTable.self, + _040_MessageDeduplicationTable.self, + _041_RenameTableSettingToKeyValueStore.self, + _042_MoveSettingsToLibSession.self, + _043_RenameAttachments.self, + _044_AddProMessageFlag.self + ] public static func configure(using dependencies: Dependencies) { // Configure the job executors diff --git a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift similarity index 94% rename from SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift index 038c6de166..d91ffaca7d 100644 --- a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift @@ -4,10 +4,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "initialSetup" +enum _001_SUK_InitialSetupMigration: Migration { + static let identifier: String = "utilitiesKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Identity.self, Job.self, JobDependencies.self diff --git a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift similarity index 89% rename from SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift index ca787c2c2c..8f27b313d5 100644 --- a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "SetupStandardJobs" +enum _002_SUK_SetupStandardJobs: Migration { + static let identifier: String = "utilitiesKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift similarity index 70% rename from SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift index 382874faf3..5753532d9a 100644 --- a/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "YDBToGRDBMigration" +enum _003_SUK_YDBToGRDBMigration: Migration { + static let identifier: String = "utilitiesKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift similarity index 90% rename from SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift index 9473c323f8..9852a0a11f 100644 --- a/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "initialSetup" +enum _004_SNK_InitialSetupMigration: Migration { + static let identifier: String = "snodeKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift similarity index 91% rename from SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift index 845a42fe16..2241868cc8 100644 --- a/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "SetupStandardJobs" +enum _005_SNK_SetupStandardJobs: Migration { + static let identifier: String = "snodeKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift index 2823930730..6c9e861ca9 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "initialSetup" +enum _006_SMK_InitialSetupMigration: Migration { + static let identifier: String = "messagingKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Contact.self, Profile.self, SessionThread.self, DisappearingMessagesConfiguration.self, @@ -59,7 +58,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Profile table try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -106,7 +105,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the ClosedGroup table try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -157,7 +156,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the OpenGroup table try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -268,7 +267,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Interaction table try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") } diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift similarity index 93% rename from SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift index b9b3e31588..f7057035e0 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift @@ -7,9 +7,8 @@ import SessionNetworkingKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SetupStandardJobs" +enum _007_SMK_SetupStandardJobs: Migration { + static let identifier: String = "messagingKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift similarity index 69% rename from SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift index 565fd3e3f2..ac66dd7c0b 100644 --- a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "YDBToGRDBMigration" +enum _008_SNK_YDBToGRDBMigration: Migration { + static let identifier: String = "snodeKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift similarity index 79% rename from SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift index 13ecc5df76..7851f48d74 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "YDBToGRDBMigration" +enum _009_SMK_YDBToGRDBMigration: Migration { + static let identifier: String = "messagingKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift similarity index 81% rename from SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift rename to SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift index 989df981c8..ff71d5ffbe 100644 --- a/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift +++ b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// This migration adds a flag to the `SnodeReceivedMessageInfo` so that when deleting interactions we can /// ignore their hashes when subsequently trying to fetch new messages (which results in the storage server returning /// messages from the beginning of time) -enum _004_FlagMessageHashAsDeletedOrInvalid: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" +enum _010_FlagMessageHashAsDeletedOrInvalid: Migration { + static let identifier: String = "snodeKit.FlagMessageHashAsDeletedOrInvalid" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift similarity index 78% rename from SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift rename to SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift index c5c4c4a4d8..ef8588451f 100644 --- a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift +++ b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit import SessionNetworkingKit /// This migration used to remove the legacy YapDatabase files (the old logic has been removed and is no longer supported so it now does nothing) -enum _004_RemoveLegacyYDB: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RemoveLegacyYDB" +enum _011_RemoveLegacyYDB: Migration { + static let identifier: String = "messagingKit.RemoveLegacyYDB" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift similarity index 90% rename from SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift rename to SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift index 852033b582..93a2c68752 100644 --- a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift +++ b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _004_AddJobPriority: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobPriority" +enum _012_AddJobPriority: Migration { + static let identifier: String = "utilitiesKit.AddJobPriority" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift rename to SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift index efe33c321d..a49d63d0ca 100644 --- a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift +++ b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration fixes a bug where certain message variants could incorrectly be counted as unread messages -enum _005_FixDeletedMessageReadState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixDeletedMessageReadState" +enum _013_FixDeletedMessageReadState: Migration { + static let identifier: String = "messagingKit.FixDeletedMessageReadState" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift rename to SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift index 006b04c283..5247ae2d79 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates` /// for open groups so they will fully re-fetch their mod/admin lists -enum _006_FixHiddenModAdminSupport: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixHiddenModAdminSupport" +enum _014_FixHiddenModAdminSupport: Migration { + static let identifier: String = "messagingKit.FixHiddenModAdminSupport" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift rename to SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift index bf8ded493e..3a0a617928 100644 --- a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift +++ b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions -enum _007_HomeQueryOptimisationIndexes: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "HomeQueryOptimisationIndexes" +enum _015_HomeQueryOptimisationIndexes: Migration { + static let identifier: String = "messagingKit.HomeQueryOptimisationIndexes" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/Session/Database/Migrations/_001_ThemePreferences.swift b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift similarity index 78% rename from Session/Database/Migrations/_001_ThemePreferences.swift rename to SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift index 8bb9987f95..f19020e3ed 100644 --- a/Session/Database/Migrations/_001_ThemePreferences.swift +++ b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift @@ -13,9 +13,8 @@ import SessionUtilitiesKit /// **Note:** This migration used to live within `SessionUIKit` but we wanted to isolate it and remove dependencies from it so we /// needed to extract this migration into the `Session` and `SessionShareExtension` targets (since both need theming they both /// need to provide this migration as an option during setup) -enum _001_ThemePreferences: Migration { - static let target: TargetMigrations.Identifier = ._deprecatedUIKit - static let identifier: String = "ThemePreferences" +enum _016_ThemePreferences: Migration { + static let identifier: String = "uiKit.ThemePreferences" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -86,25 +85,3 @@ private extension Theme.PrimaryColor { } } } - -enum DeprecatedUIKitMigrationTarget: MigratableTarget { - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: ._deprecatedUIKit, - migrations: [ - // Want to ensure the initial DB stuff has been completed before doing any - // SNUIKit migrations - [], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _001_ThemePreferences.self - ], // Add job priorities - [], // Fix thread FTS - [], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift similarity index 91% rename from SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift rename to SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift index bcd9c2f84b..c102846bad 100644 --- a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the new types needed for Emoji Reacts -enum _008_EmojiReacts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "EmojiReacts" +enum _017_EmojiReacts: Migration { + static let identifier: String = "messagingKit.EmojiReacts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [Reaction.self] diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift rename to SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift index b8e7c47efb..bf51074058 100644 --- a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift +++ b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _009_OpenGroupPermission: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "OpenGroupPermission" +enum _018_OpenGroupPermission: Migration { + static let identifier: String = "messagingKit.OpenGroupPermission" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift similarity index 82% rename from SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift rename to SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift index 9c2aea1207..92dfecc4fd 100644 --- a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift +++ b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation /// searh (currently it's much slower than the global search) -enum _010_AddThreadIdToFTS: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddThreadIdToFTS" +enum _019_AddThreadIdToFTS: Migration { + static let identifier: String = "messagingKit.AddThreadIdToFTS" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -22,7 +21,7 @@ enum _010_AddThreadIdToFTS: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift similarity index 76% rename from SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift rename to SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift index e4a36701f5..b9bebf7e81 100644 --- a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift +++ b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _005_AddJobUniqueHash: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobUniqueHash" +enum _020_AddJobUniqueHash: Migration { + static let identifier: String = "utilitiesKit.AddJobUniqueHash" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift similarity index 91% rename from SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift rename to SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift index 6565fc40d1..55e215871e 100644 --- a/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift +++ b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration adds a primary key to `SnodeReceivedMessageInfo` based on the key and hash to speed up lookup -enum _005_AddSnodeReveivedMessageInfoPrimaryKey: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "AddSnodeReveivedMessageInfoPrimaryKey" +enum _021_AddSnodeReveivedMessageInfoPrimaryKey: Migration { + static let identifier: String = "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift similarity index 86% rename from SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift rename to SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift index 6cc16ea4ef..af5ceaaa5d 100644 --- a/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift +++ b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration drops the current `SnodePool` and `SnodeSet` and their associated jobs as they are handled by `libSession` now -enum _006_DropSnodeCache: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "DropSnodeCache" +enum _022_DropSnodeCache: Migration { + static let identifier: String = "snodeKit.DropSnodeCache" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift similarity index 96% rename from SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift rename to SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift index 779eecee5e..e77ead364d 100644 --- a/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift +++ b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration splits the old `key` structure used for `SnodeReceivedMessageInfo` into separate columns for more efficient querying -enum _007_SplitSnodeReceivedMessageInfo: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "SplitSnodeReceivedMessageInfo" +enum _023_SplitSnodeReceivedMessageInfo: Migration { + static let identifier: String = "snodeKit.SplitSnodeReceivedMessageInfo" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift similarity index 84% rename from SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift rename to SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift index 1eb3e6d265..2fad3edb4e 100644 --- a/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift +++ b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift @@ -2,13 +2,13 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration resets the `lastHash` value for all user config namespaces to force the app to fetch the latest config /// messages in case there are multi-part config message we had previously seen and failed to merge -enum _008_ResetUserConfigLastHashes: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "ResetUserConfigLastHashes" +enum _024_ResetUserConfigLastHashes: Migration { + static let identifier: String = "snodeKit.ResetUserConfigLastHashes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift rename to SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift index 5f51432095..0b0d5fec63 100644 --- a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift +++ b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration adds a table to track pending read receipts (it's possible to receive a read receipt message before getting the original /// message due to how one-to-one conversations work, by storing pending read receipts we should be able to prevent this case) -enum _011_AddPendingReadReceipts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddPendingReadReceipts" +enum _025_AddPendingReadReceipts: Migration { + static let identifier: String = "messagingKit.AddPendingReadReceipts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [PendingReadReceipt.self] diff --git a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift similarity index 80% rename from SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift rename to SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift index a030deed3f..b655432c2c 100644 --- a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift +++ b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back for internal test users whose FTS table was removed unintentionally -enum _012_AddFTSIfNeeded: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddFTSIfNeeded" +enum _026_AddFTSIfNeeded: Migration { + static let identifier: String = "messagingKit.AddFTSIfNeeded" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -17,7 +16,7 @@ enum _012_AddFTSIfNeeded: Migration { if try db.tableExists("interaction_fts") == false { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift rename to SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift index cb67ad5bf5..57e76b93a9 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift @@ -9,9 +9,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration makes the neccessary changes to support the updated user config syncing system -enum _013_SessionUtilChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SessionUtilChanges" +enum _027_SessionUtilChanges: Migration { + static let identifier: String = "messagingKit.SessionUtilChanges" static let minExpectedRunDuration: TimeInterval = 0.4 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ConfigDump.self] @@ -229,7 +228,7 @@ enum _013_SessionUtilChanges: Migration { } } -private extension _013_SessionUtilChanges { +private extension _027_SessionUtilChanges { static func generateLegacyClosedGroupKeyPairHash(threadId: String, publicKey: Data, secretKey: Data) -> String { return Data(Insecure.MD5 .hash(data: threadId.bytes + publicKey.bytes + secretKey.bytes) diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift rename to SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift index c81a813e12..a53031dd46 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift @@ -6,9 +6,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration goes through the current state of the database and generates config dumps for the user config types -enum _014_GenerateInitialUserConfigDumps: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GenerateInitialUserConfigDumps" +enum _028_GenerateInitialUserConfigDumps: Migration { + static let identifier: String = "messagingKit.GenerateInitialUserConfigDumps" static let minExpectedRunDuration: TimeInterval = 4.0 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift similarity index 94% rename from SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift rename to SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift index dd58e13355..22cb579ef3 100644 --- a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift +++ b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests -enum _015_BlockCommunityMessageRequests: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "BlockCommunityMessageRequests" +enum _029_BlockCommunityMessageRequests: Migration { + static let identifier: String = "messagingKit.BlockCommunityMessageRequests" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift rename to SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift index 82816602ca..dbbeb35044 100644 --- a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift +++ b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration updates the tiemstamps added to the `Profile` in earlier migrations to be nullable (having it not null /// results in migration issues when a user jumps between multiple versions) -enum _016_MakeBrokenProfileTimestampsNullable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MakeBrokenProfileTimestampsNullable" +enum _030_MakeBrokenProfileTimestampsNullable: Migration { + static let identifier: String = "messagingKit.MakeBrokenProfileTimestampsNullable" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift rename to SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift index c9c9240fde..9660270f21 100644 --- a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift +++ b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back if either the tables or any of the triggers no longer exist -enum _017_RebuildFTSIfNeeded_2_4_5: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RebuildFTSIfNeeded_2_4_5" +enum _031_RebuildFTSIfNeeded_2_4_5: Migration { + static let identifier: String = "messagingKit.RebuildFTSIfNeeded_2_4_5" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -30,7 +29,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") @@ -44,7 +43,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -58,7 +57,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -71,7 +70,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } diff --git a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift rename to SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift index 809c426e56..4bf07b018f 100644 --- a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift +++ b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _018_DisappearingMessagesConfiguration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DisappearingMessagesWithTypes" +enum _032_DisappearingMessagesConfiguration: Migration { + static let identifier: String = "messagingKit.DisappearingMessagesWithTypes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift similarity index 84% rename from SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift rename to SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift index 9f5bd4c724..f0df877278 100644 --- a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift +++ b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _019_ScheduleAppUpdateCheckJob: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ScheduleAppUpdateCheckJob" +enum _033_ScheduleAppUpdateCheckJob: Migration { + static let identifier: String = "messagingKit.ScheduleAppUpdateCheckJob" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift rename to SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift index 90dbfc4fbd..ecd158b8b1 100644 --- a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _020_AddMissingWhisperFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddMissingWhisperFlag" +enum _034_AddMissingWhisperFlag: Migration { + static let identifier: String = "messagingKit.AddMissingWhisperFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift rename to SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift index a47d202666..f0ebe35f20 100644 --- a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift +++ b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _021_ReworkRecipientState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ReworkRecipientState" +enum _035_ReworkRecipientState: Migration { + static let identifier: String = "messagingKit.ReworkRecipientState" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -180,7 +179,7 @@ enum _021_ReworkRecipientState: Migration { } } -private extension _021_ReworkRecipientState { +private extension _035_ReworkRecipientState { enum LegacyState: Int { case sending case failed diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift rename to SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index d3ad1f25be..a7444ce0f2 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -8,9 +8,8 @@ import GRDB import SessionNetworkingKit import SessionUtilitiesKit -enum _022_GroupsRebuildChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsRebuildChanges" +enum _036_GroupsRebuildChanges: Migration { + static let identifier: String = "messagingKit.GroupsRebuildChanges" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] @@ -209,7 +208,7 @@ enum _022_GroupsRebuildChanges: Migration { } } -private extension _022_GroupsRebuildChanges { +private extension _036_GroupsRebuildChanges { static func generateFilename(format: ImageFormat = .jpeg, using dependencies: Dependencies) -> String { return dependencies[singleton: .crypto] .generate(.uuid()) diff --git a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift rename to SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift index 2bffc37639..294efd4846 100644 --- a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _023_GroupsExpiredFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsExpiredFlag" +enum _037_GroupsExpiredFlag: Migration { + static let identifier: String = "messagingKit.GroupsExpiredFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift rename to SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift index 9b65965362..5929ad6c87 100644 --- a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift +++ b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// There was a bug with internal releases of the Groups Rebuild feature where we incorrectly assigned an `Interaction.Variant` /// value of `3` to deleted message artifacts when it should have been `2`, this migration updates any interactions with a value of `2` /// to be `3` -enum _024_FixBustedInteractionVariant: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixBustedInteractionVariant" +enum _038_FixBustedInteractionVariant: Migration { + static let identifier: String = "messagingKit.FixBustedInteractionVariant" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift similarity index 74% rename from SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift rename to SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift index afc0dd376d..20111dade4 100644 --- a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift +++ b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// Legacy closed groups are no longer supported so we can drop the `closedGroupKeyPair` table from /// the database -enum _025_DropLegacyClosedGroupKeyPairTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DropLegacyClosedGroupKeyPairTable" +enum _039_DropLegacyClosedGroupKeyPairTable: Migration { + static let identifier: String = "messagingKit.DropLegacyClosedGroupKeyPairTable" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift rename to SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift index 5addd66869..5431858f19 100644 --- a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift +++ b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift @@ -8,9 +8,8 @@ import SessionNetworkingKit /// The different platforms use different approaches for message deduplication but in the future we want to shift the database logic into /// `libSession` so it makes sense to try to define a longer-term deduplication approach we we can use in `libSession`, additonally /// the PN extension will need to replicate this deduplication data so having a single source-of-truth for the data will make things easier -enum _026_MessageDeduplicationTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MessageDeduplicationTable" +enum _040_MessageDeduplicationTable: Migration { + static let identifier: String = "messagingKit.MessageDeduplicationTable" static let minExpectedRunDuration: TimeInterval = 5 static var createdTables: [(FetchableRecord & TableRecord).Type] = [ MessageDeduplication.self @@ -343,7 +342,7 @@ enum _026_MessageDeduplicationTable: Migration { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { static func legacyDedupeIdentifier( variant: Interaction.Variant, timestampMs: Int64 @@ -372,7 +371,7 @@ internal extension _026_MessageDeduplicationTable { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { enum ControlMessageProcessRecordVariant: Int { case readReceipt = 1 case typingIndicator = 2 diff --git a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift similarity index 68% rename from SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift rename to SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift index ffad28e128..48a366aecf 100644 --- a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift +++ b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _006_RenameTableSettingToKeyValueStore: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "RenameTableSettingToKeyValueStore" // stringlint:disable +enum _041_RenameTableSettingToKeyValueStore: Migration { + static let identifier: String = "utilitiesKit.RenameTableSettingToKeyValueStore" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ KeyValueStore.self ] diff --git a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift rename to SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift index 3f6353e72c..5e63bd1bff 100644 --- a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift +++ b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift @@ -6,9 +6,8 @@ import SessionUIKit import SessionUtilitiesKit /// This migration extracts an old settings from the database and saves them into libSession -enum _027_MoveSettingsToLibSession: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MoveSettingsToLibSession" +enum _042_MoveSettingsToLibSession: Migration { + static let identifier: String = "messagingKit.MoveSettingsToLibSession" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift similarity index 99% rename from SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift rename to SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift index a4e8969695..ff94b962c2 100644 --- a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift +++ b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift @@ -8,9 +8,8 @@ import SessionUtilitiesKit /// This migration renames all attachments to use a hash of the download url for the filename instead of a random UUID (means we can /// generate the filename just from the URL and don't need to store the filename) -enum _028_RenameAttachments: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RenameAttachments" +enum _043_RenameAttachments: Migration { + static let identifier: String = "messagingKit.RenameAttachments" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift rename to SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift index 0d2751199e..7f51d1ffc2 100644 --- a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _029_AddProMessageFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddProMessageFlag" +enum _044_AddProMessageFlag: Migration { + static let identifier: String = "messagingKit.AddProMessageFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Models/MessageDeduplication.swift b/SessionMessagingKit/Database/Models/MessageDeduplication.swift index 6a73d52936..e8b5a8f531 100644 --- a/SessionMessagingKit/Database/Models/MessageDeduplication.swift +++ b/SessionMessagingKit/Database/Models/MessageDeduplication.swift @@ -209,7 +209,7 @@ public extension MessageDeduplication { _ processedMessage: ProcessedMessage, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant try ensureMessageIsNotADuplicate( threadId: processedMessage.threadId, uniqueIdentifier: processedMessage.uniqueIdentifier, @@ -402,12 +402,12 @@ private extension MessageDeduplication { _ db: ObservingDatabase, threadId: String, legacyIdentifier: String?, - legacyVariant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, + legacyVariant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, timestampMs: Int64?, serverExpirationTimestamp: TimeInterval?, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant guard let legacyIdentifier: String = legacyIdentifier, let legacyVariant: Variant = legacyVariant, @@ -463,7 +463,7 @@ private extension MessageDeduplication { } @available(*, deprecated, message: "⚠️ Remove this code once once enough time has passed since it's release (at least 1 month)") - static func getLegacyVariant(for variant: Message.Variant?) -> _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { + static func getLegacyVariant(for variant: Message.Variant?) -> _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { guard let variant: Message.Variant = variant else { return nil } switch variant { @@ -494,7 +494,7 @@ private extension MessageDeduplication { case .standard(_, _, _, let messageInfo, _): guard let timestampMs: UInt64 = messageInfo.message.sentTimestampMs, - let variant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) + let variant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) else { return nil } return "LegacyRecord-\(variant.rawValue)-\(timestampMs)" // stringlint:ignore diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index bd36080603..22fb802b43 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -18,9 +18,7 @@ class MessageDeduplicationSpec: AsyncSpec { @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 85d0809b20..130facfd6c 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -27,10 +27,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index 15012b5318..e549e2c221 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -35,10 +35,7 @@ class MessageSendJobSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread.upsert( diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index a226aab1df..79692802c7 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -20,10 +20,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 07bda1e05f..309aa452d8 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -28,11 +28,7 @@ class LibSessionGroupInfoSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self, - SNNetworkingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 8001ede737..43f6c045de 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -27,10 +27,7 @@ class LibSessionGroupMembersSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index f23341efdf..cb8ff17dd8 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -27,10 +27,7 @@ class LibSessionSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 24635b39af..da31a7c154 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -110,10 +110,7 @@ class OpenGroupManagerSpec: QuickSpec { }() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index be86b9ed76..f731e18ecf 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -29,11 +29,7 @@ class MessageReceiverGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 2f55e63806..69d1a6d505 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -29,10 +29,7 @@ class MessageSenderGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index 6ddb79b9bf..ee73e9fbc0 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -17,10 +17,7 @@ class MessageSenderSpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index 6f966ebf9c..79f98b853e 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -21,10 +21,7 @@ class CommunityPollerSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 5cd9216cab..13dd801e31 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -24,10 +24,7 @@ class ExtensionHelperSpec: AsyncSpec { @TestState(singleton: .extensionHelper, in: dependencies) var extensionHelper: ExtensionHelper! = ExtensionHelper(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( diff --git a/SessionNetworkingKit/Configuration.swift b/SessionNetworkingKit/Configuration.swift deleted file mode 100644 index 18a73d2615..0000000000 --- a/SessionNetworkingKit/Configuration.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUtilitiesKit - -public enum SNNetworkingKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .networkingKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_FlagMessageHashAsDeletedOrInvalid.self - ], // Legacy DB removal - [], // Add job priorities - [], // Fix thread FTS - [ - _005_AddSnodeReveivedMessageInfoPrimaryKey.self, - _006_DropSnodeCache.self, - _007_SplitSnodeReceivedMessageInfo.self, - _008_ResetUserConfigLastHashes.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index e977151317..39797ee76b 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -45,7 +45,6 @@ final class ShareNavController: UINavigationController { dependencies.warmCache(cache: .appVersion) AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in // stringlint:ignore_start if !Log.loggerExists(withPrefix: "SessionShareExtension") { diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index 7028b12544..beaaa7ed26 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -21,12 +21,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index 227c06bf86..8d2532b0b0 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -21,12 +21,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index ec9d79c0a2..77514dedf8 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -29,12 +29,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity( diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 5c63cd7fae..c9b824b196 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -38,14 +38,7 @@ class DatabaseSpec: QuickSpec { @TestState var initialResult: Result! = nil @TestState var finalResult: Result! = nil - let allMigrations: [Storage.KeyedMigration] = SynchronousStorage.sortedMigrationInfo( - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ] - ) + let allMigrations: [Migration.Type] = SNMessagingKit.migrations let dynamicTests: [MigrationTest] = MigrationTest.extractTests(allMigrations) let allTableTypes: [(TableRecord & FetchableRecord).Type] = MigrationTest.extractDatabaseTypes(allMigrations) MigrationTest.explicitValues = [ @@ -75,12 +68,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can be created from an empty state it("can be created from an empty state") { mockStorage.perform( - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -92,7 +80,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database table types it("can still parse the database table types") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -115,7 +103,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database types setting null where possible it("can still parse the database types setting null where possible") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -137,9 +125,9 @@ class DatabaseSpec: QuickSpec { // MARK: -- can migrate from X to Y dynamicTests.forEach { test in - it("can migrate from \(test.initialMigrationKey) to \(test.finalMigrationKey)") { + it("can migrate from \(test.initialMigrationIdentifier) to \(test.finalMigrationIdentifier)") { let initialStateResult: Result = { - if let cachedResult: Result = snapshotCache[test.initialMigrationKey] { + if let cachedResult: Result = snapshotCache[test.initialMigrationIdentifier] { return cachedResult } @@ -153,7 +141,7 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) var initialResult: Result! storage.perform( - sortedMigrations: test.initialMigrations, + migrations: test.initialMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -163,10 +151,10 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) try MigrationTest.generateDummyData(storage, nullsWherePossible: false) - snapshotCache[test.initialMigrationKey] = .success(dbQueue) + snapshotCache[test.initialMigrationIdentifier] = .success(dbQueue) return .success(dbQueue) } catch { - snapshotCache[test.initialMigrationKey] = .failure(error) + snapshotCache[test.initialMigrationIdentifier] = .failure(error) return .failure(error) } }() @@ -175,7 +163,7 @@ class DatabaseSpec: QuickSpec { switch initialStateResult { case .success(let db): sourceDb = db case .failure(let error): - fail("Failed to prepare the initial state for '\(test.initialMigrationKey)'. Error: \(error)") + fail("Failed to prepare the initial state for '\(test.initialMigrationIdentifier)'. Error: \(error)") return } @@ -186,7 +174,7 @@ class DatabaseSpec: QuickSpec { // Peform the target migrations to ensure the migrations themselves worked correctly mockStorage.perform( - sortedMigrations: test.migrationsToTest, + migrations: test.migrationsToTest, async: false, onProgressUpdate: nil, onComplete: { result in finalResult = result } @@ -195,12 +183,68 @@ class DatabaseSpec: QuickSpec { switch finalResult { case .success: break case .failure(let error): - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: \(error)") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: \(error)") case .none: - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: No result") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: No result") } } } + + // MARK: -- migration order hasn't changed + it("migration order hasn't changed") { + expect(SNMessagingKit.migrations.map { $0.identifier }).to(equal([ + "utilitiesKit.initialSetup", + "utilitiesKit.SetupStandardJobs", + "utilitiesKit.YDBToGRDBMigration", + "snodeKit.initialSetup", + "snodeKit.SetupStandardJobs", + "messagingKit.initialSetup", + "messagingKit.SetupStandardJobs", + "snodeKit.YDBToGRDBMigration", + "messagingKit.YDBToGRDBMigration", + "snodeKit.FlagMessageHashAsDeletedOrInvalid", + "messagingKit.RemoveLegacyYDB", + "utilitiesKit.AddJobPriority", + "messagingKit.FixDeletedMessageReadState", + "messagingKit.FixHiddenModAdminSupport", + "messagingKit.HomeQueryOptimisationIndexes", + "uiKit.ThemePreferences", + "messagingKit.EmojiReacts", + "messagingKit.OpenGroupPermission", + "messagingKit.AddThreadIdToFTS", + "utilitiesKit.AddJobUniqueHash", + "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey", + "snodeKit.DropSnodeCache", + "snodeKit.SplitSnodeReceivedMessageInfo", + "snodeKit.ResetUserConfigLastHashes", + "messagingKit.AddPendingReadReceipts", + "messagingKit.AddFTSIfNeeded", + "messagingKit.SessionUtilChanges", + "messagingKit.GenerateInitialUserConfigDumps", + "messagingKit.BlockCommunityMessageRequests", + "messagingKit.MakeBrokenProfileTimestampsNullable", + "messagingKit.RebuildFTSIfNeeded_2_4_5", + "messagingKit.DisappearingMessagesWithTypes", + "messagingKit.ScheduleAppUpdateCheckJob", + "messagingKit.AddMissingWhisperFlag", + "messagingKit.ReworkRecipientState", + "messagingKit.GroupsRebuildChanges", + "messagingKit.GroupsExpiredFlag", + "messagingKit.FixBustedInteractionVariant", + "messagingKit.DropLegacyClosedGroupKeyPairTable", + "messagingKit.MessageDeduplicationTable", + "utilitiesKit.RenameTableSettingToKeyValueStore", + "messagingKit.MoveSettingsToLibSession", + "messagingKit.RenameAttachments", + "messagingKit.AddProMessageFlag" + ])) + } + + // MARK: -- there are no duplicate migration names + it("there are no duplicate migration names") { + expect(Set(SNMessagingKit.migrations.map { $0.identifier }).sorted()) + .to(equal(SNMessagingKit.migrations.map { $0.identifier }.sorted())) + } } } } @@ -236,15 +280,15 @@ private struct TableColumn: Hashable { private class MigrationTest { static var explicitValues: [TableColumn: (any DatabaseValueConvertible)] = [:] - let initialMigrations: [Storage.KeyedMigration] - let migrationsToTest: [Storage.KeyedMigration] + let initialMigrations: [Migration.Type] + let migrationsToTest: [Migration.Type] - var initialMigrationKey: String { return (initialMigrations.last?.key ?? "an empty database") } - var finalMigrationKey: String { return (migrationsToTest.last?.key ?? "invalid") } + var initialMigrationIdentifier: String { return (initialMigrations.last?.identifier ?? "an empty database") } + var finalMigrationIdentifier: String { return (migrationsToTest.last?.identifier ?? "invalid") } private init( - initialMigrations: [Storage.KeyedMigration], - migrationsToTest: [Storage.KeyedMigration] + initialMigrations: [Migration.Type], + migrationsToTest: [Migration.Type] ) { self.initialMigrations = initialMigrations self.migrationsToTest = migrationsToTest @@ -252,7 +296,7 @@ private class MigrationTest { // MARK: - Test Data - static func extractTests(_ allMigrations: [Storage.KeyedMigration]) -> [MigrationTest] { + static func extractTests(_ allMigrations: [Migration.Type]) -> [MigrationTest] { return (0..<(allMigrations.count - 1)) .flatMap { index -> [MigrationTest] in ((index + 1).. MigrationTest in @@ -264,10 +308,10 @@ private class MigrationTest { } } - static func extractDatabaseTypes(_ allMigrations: [Storage.KeyedMigration]) -> [(TableRecord & FetchableRecord).Type] { + static func extractDatabaseTypes(_ allMigrations: [Migration.Type]) -> [(TableRecord & FetchableRecord).Type] { return Array(allMigrations .reduce(into: [:]) { result, next in - next.migration.createdTables.forEach { table in + next.createdTables.forEach { table in result[ObjectIdentifier(table).hashValue] = table } } @@ -392,69 +436,3 @@ private class MigrationTest { } } } - -enum TestAllMigrationRequirementsReversedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresAllMigrationRequirementsReversedMigration.self - ] - ] - ) - } -} - -enum TestRequiresLibSessionStateMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresLibSessionStateMigration.self - ] - ] - ) - } -} - -enum TestRequiresSessionIdCachedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresSessionIdCachedMigration.self - ] - ] - ) - } -} - -enum TestRequiresAllMigrationRequirementsReversedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresLibSessionStateMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresSessionIdCachedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e351b5a8b5..f39b6b65a3 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -24,12 +24,7 @@ class OnboardingSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index 65c099d860..2a55c6ffae 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -21,12 +21,7 @@ class NotificationContentViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState var secretKey: [UInt8]! = Array(Data(hex: TestConstants.edSecretKey)) diff --git a/SessionUtilitiesKit/Configuration.swift b/SessionUtilitiesKit/Configuration.swift index ce7ef571ee..c9a0e7503f 100644 --- a/SessionUtilitiesKit/Configuration.swift +++ b/SessionUtilitiesKit/Configuration.swift @@ -4,41 +4,12 @@ import Foundation import UIKit.UIFont import GRDB -public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API nice +public enum SNUtilitiesKit { public static var maxFileSize: UInt = 0 public static var isRunningTests: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil // stringlint:ignore } - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .utilitiesKit, - migrations: [ - [ - // Intentionally including the '_003_YDBToGRDBMigration' in the first migration - // set to ensure the 'Identity' data is migrated before any other migrations are - // run (some need access to the users publicKey) - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self, - _003_YDBToGRDBMigration.self - ], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _004_AddJobPriority.self - ], // Add job priorities - [], // Fix thread FTS - [ - _005_AddJobUniqueHash.self - ], - [ - _006_RenameTableSettingToKeyValueStore.self - ], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } - public static func configure( networkMaxFileSize: UInt, using dependencies: Dependencies diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index b162e27c99..e9b4cfbc4f 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -246,8 +246,6 @@ open class Storage { // MARK: - Migrations - public typealias KeyedMigration = (key: String, identifier: TargetMigrations.Identifier, migration: Migration.Type) - public static func appliedMigrationIdentifiers(_ db: ObservingDatabase) -> Set { let migrator: DatabaseMigrator = DatabaseMigrator() @@ -255,47 +253,11 @@ open class Storage { .defaulting(to: []) } - public static func sortedMigrationInfo(migrationTargets: [MigratableTarget.Type]) -> [KeyedMigration] { - typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet) - - return migrationTargets - .map { target -> TargetMigrations in target.migrations() } - .sorted() - .reduce(into: [[MigrationInfo]]()) { result, next in - next.migrations.enumerated().forEach { index, migrationSet in - if result.count <= index { - result.append([]) - } - - result[index] = (result[index] + [(next.identifier, migrationSet)]) - } - } - .reduce(into: []) { result, next in - next.forEach { identifier, migrations in - result.append(contentsOf: migrations.map { (identifier.key(with: $0), identifier, $0) }) - } - } - } - public func perform( - migrationTargets: [MigratableTarget.Type], + migrations: [Migration.Type], async: Bool = true, onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, onComplete: @escaping (Result) -> () - ) { - perform( - sortedMigrations: Storage.sortedMigrationInfo(migrationTargets: migrationTargets), - async: async, - onProgressUpdate: onProgressUpdate, - onComplete: onComplete - ) - } - - internal func perform( - sortedMigrations: [KeyedMigration], - async: Bool, - onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, - onComplete: @escaping (Result) -> () ) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { let error: Error = (startupError ?? StorageError.startupFailed) @@ -306,36 +268,33 @@ open class Storage { // Setup and run any required migrations var migrator: DatabaseMigrator = DatabaseMigrator() - sortedMigrations.forEach { _, identifier, migration in - migrator.registerMigration( - self, - targetIdentifier: identifier, - migration: migration, - using: dependencies - ) + migrations.forEach { migration in + migrator.registerMigration(migration.identifier) { [dependencies] db in + let migration = migration.loggedMigrate(using: dependencies) + try migration(ObservingDatabase.create(db, using: dependencies)) + } } // Determine which migrations need to be performed and gather the relevant settings needed to // inform the app of progress/states let completedMigrations: [String] = (try? dbWriter.read { db in try migrator.completedMigrations(db) }) .defaulting(to: []) - let unperformedMigrations: [KeyedMigration] = sortedMigrations + let unperformedMigrations: [Migration.Type] = migrations .reduce(into: []) { result, next in - guard !completedMigrations.contains(next.key) else { return } + guard !completedMigrations.contains(next.identifier) else { return } result.append(next) } let migrationToDurationMap: [String: TimeInterval] = unperformedMigrations .reduce(into: [:]) { result, next in - result[next.key] = next.migration.minExpectedRunDuration + result[next.identifier] = next.minExpectedRunDuration } - let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations - .map { _, _, migration in migration.minExpectedRunDuration } + let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations.map { $0.minExpectedRunDuration } let totalMinExpectedDuration: TimeInterval = migrationToDurationMap.values.reduce(0, +) // Store the logic to handle migration progress and completion let progressUpdater: (String, CGFloat) -> Void = { (targetKey: String, progress: CGFloat) in - guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { key, _, _ in key == targetKey }) else { + guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { $0.identifier == targetKey }) else { return } @@ -352,8 +311,8 @@ open class Storage { let migrationCompleted: (Result) -> () = { [weak self, migrator, dbWriter, dependencies] result in // Make sure to transition the progress updater to 100% for the final migration (just // in case the migration itself didn't update to 100% itself) - if let lastMigrationKey: String = unperformedMigrations.last?.key { - MigrationExecution.current?.progressUpdater(lastMigrationKey, 1) + if let lastMigrationIdentifier: String = unperformedMigrations.last?.identifier { + MigrationExecution.current?.progressUpdater(lastMigrationIdentifier, 1) } self?.hasCompletedMigrations = true @@ -401,8 +360,8 @@ open class Storage { let migrationContext: MigrationExecution.Context = MigrationExecution.Context(progressUpdater: progressUpdater) // If we have an unperformed migration then trigger the progress updater immediately - if let firstMigrationKey: String = unperformedMigrations.first?.key { - migrationContext.progressUpdater(firstMigrationKey, 0) + if let firstMigrationIdentifier: String = unperformedMigrations.first?.identifier { + migrationContext.progressUpdater(firstMigrationIdentifier, 0) } MigrationExecution.$current.withValue(migrationContext) { diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index 36a9026a8f..4609edfbdd 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -12,7 +12,6 @@ public extension Log.Category { // MARK: - Migration public protocol Migration { - static var target: TargetMigrations.Identifier { get } static var identifier: String { get } static var minExpectedRunDuration: TimeInterval { get } static var createdTables: [(TableRecord & FetchableRecord).Type] { get } @@ -21,17 +20,12 @@ public protocol Migration { } public extension Migration { - static func loggedMigrate( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - using dependencies: Dependencies - ) -> ((_ db: ObservingDatabase) throws -> ()) { + static func loggedMigrate(using dependencies: Dependencies) -> ((_ db: ObservingDatabase) throws -> ()) { return { (db: ObservingDatabase) in - Log.info(.migration, "Starting \(targetIdentifier.key(with: self))") + Log.info(.migration, "Starting \(identifier)") /// Store the `currentlyRunningMigration` in case it's useful MigrationExecution.current?.currentlyRunningMigration = MigrationExecution.CurrentlyRunningMigration( - identifier: targetIdentifier, migration: self ) defer { MigrationExecution.current?.currentlyRunningMigration = nil } @@ -44,7 +38,7 @@ public extension Migration { MigrationExecution.current?.observedEvents.append(contentsOf: db.events) MigrationExecution.current?.postCommitActions.merge(db.postCommitActions) { old, _ in old } - Log.info(.migration, "Completed \(targetIdentifier.key(with: self))") + Log.info(.migration, "Completed \(identifier)") } } } @@ -53,10 +47,9 @@ public extension Migration { public enum MigrationExecution { public struct CurrentlyRunningMigration: ThreadSafeType { - public let identifier: TargetMigrations.Identifier public let migration: Migration.Type - public var key: String { identifier.key(with: migration) } + public var key: String { migration.identifier } } public final class Context { @@ -83,6 +76,7 @@ public enum MigrationExecution { @TaskLocal public static var current: Context? + // stringlint:ignore_contents public static func updateProgress(_ progress: CGFloat) { // In test builds ignore any migration progress updates (we run in a custom database writer anyway) guard !SNUtilitiesKit.isRunningTests else { return } diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift deleted file mode 100644 index 860647c618..0000000000 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public protocol MigratableTarget { - static func migrations() -> TargetMigrations -} - -public struct TargetMigrations: Comparable { - /// This identifier is used to determine the order each set of migrations should run in. - /// - /// All migrations within a specific set will run first, followed by all migrations for the same set index in - /// the next `Identifier` before moving on to the next `MigrationSet`. So given the migrations: - /// - /// `{a: [1], [2, 3]}, {b: [4, 5], [6]}` - /// - /// the migrations will run in the following order: - /// - /// `a1, b4, b5, a2, a3, b6` - public enum Identifier: String, CaseIterable, Comparable { - // WARNING: The string version of these cases are used as migration identifiers so - // changing them will result in the migrations running again - case session - case utilitiesKit - case networkingKit = "snodeKit" - case messagingKit - case _deprecatedUIKit = "uiKit" - case test - - public static func < (lhs: Self, rhs: Self) -> Bool { - let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count) - let rhsIndex: Int = (Identifier.allCases.firstIndex(of: rhs) ?? Identifier.allCases.count) - - return (lhsIndex < rhsIndex) - } - - public func key(with migration: Migration.Type) -> String { - return "\(self.rawValue).\(migration.identifier)" - } - } - - public typealias MigrationSet = [Migration.Type] - - let identifier: Identifier - let migrations: [MigrationSet] - - // MARK: - Initialization - - public init( - identifier: Identifier, - migrations: [MigrationSet] - ) { - guard !migrations.contains(where: { migration in migration.contains(where: { $0.target != identifier }) }) else { - preconditionFailure("Attempted to register a migration with the wrong target") - } - - self.identifier = identifier - self.migrations = migrations - } - - // MARK: - Equatable - - public static func == (lhs: TargetMigrations, rhs: TargetMigrations) -> Bool { - return ( - lhs.identifier == rhs.identifier && - lhs.migrations.count == rhs.migrations.count - ) - } - - // MARK: - Comparable - - public static func < (lhs: Self, rhs: Self) -> Bool { - return (lhs.identifier < rhs.identifier) - } -} diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift deleted file mode 100644 index 748175ca8d..0000000000 --- a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public extension DatabaseMigrator { - mutating func registerMigration( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - migration: Migration.Type, - foreignKeyChecks: ForeignKeyChecks = .deferred, - using dependencies: Dependencies - ) { - self.registerMigration( - targetIdentifier.key(with: migration), - migrate: { db in - let migration = migration.loggedMigrate(storage, targetIdentifier: targetIdentifier, using: dependencies) - try migration(ObservingDatabase.create(db, using: dependencies)) - } - ) - } -} diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index a6c60c58b8..5101300fc7 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -15,9 +15,7 @@ class IdentitySpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self - ], + migrations: [_001_SUK_InitialSetupMigration.self], using: dependencies ) diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index 82b6fe25e2..bfd9d50009 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -47,14 +47,12 @@ class JobRunnerSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self + migrations: [ + _001_SUK_InitialSetupMigration.self, + _012_AddJobPriority.self, + _020_AddJobUniqueHash.self ], - using: dependencies, - initialData: { db in - // Migrations add jobs which we don't want so delete them - try Job.deleteAll(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var jobRunner: JobRunnerType! = JobRunner( isTestingJobRunner: true, diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index f6e76580bf..1a3f2f7558 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -10,7 +10,6 @@ import SessionUtilitiesKit public enum AppSetup { public static func setupEnvironment( requestId: String? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], appSpecificBlock: (() -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), @@ -43,7 +42,6 @@ public enum AppSetup { runPostSetupMigrations( requestId: requestId, backgroundTask: backgroundTask, - additionalMigrationTargets: additionalMigrationTargets, migrationProgressChanged: migrationProgressChanged, migrationsCompletion: migrationsCompletion, using: dependencies @@ -57,7 +55,6 @@ public enum AppSetup { public static func runPostSetupMigrations( requestId: String? = nil, backgroundTask: SessionBackgroundTask? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), using dependencies: Dependencies @@ -65,12 +62,7 @@ public enum AppSetup { var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function, using: dependencies)) dependencies[singleton: .storage].perform( - migrationTargets: additionalMigrationTargets - .appending(contentsOf: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self - ]), + migrations: SNMessagingKit.migrations, onProgressUpdate: migrationProgressChanged, onComplete: { originalResult in // Now that the migrations are complete there are a few more states which need diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index fef7fd8aae..59510cf436 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -11,8 +11,7 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { public init( customWriter: DatabaseWriter? = nil, - migrationTargets: [MigratableTarget.Type]? = nil, - migrations: [Storage.KeyedMigration]? = nil, + migrations: [Migration.Type]? = nil, using dependencies: Dependencies, initialData: ((ObservingDatabase) throws -> ())? = nil ) { @@ -21,20 +20,9 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { super.init(customWriter: customWriter, using: dependencies) - // Process any migration targets first - if let migrationTargets: [MigratableTarget.Type] = migrationTargets { + if let migrations: [Migration.Type] = migrations { perform( - migrationTargets: migrationTargets, - async: false, - onProgressUpdate: nil, - onComplete: { _ in } - ) - } - - // Then process any provided migration info - if let migrations: [Storage.KeyedMigration] = migrations { - perform( - sortedMigrations: migrations, + migrations: migrations, async: false, onProgressUpdate: nil, onComplete: { _ in } From 53aacadc723c334b604395c2989bb0126f55720a Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 8 Aug 2025 16:17:55 +1000 Subject: [PATCH 032/244] clean --- Session.xcodeproj/project.pbxproj | 4 ---- Session/Settings/SettingsViewModel.swift | 24 +++++++++++-------- SessionMessagingKit/Configuration.swift | 3 +-- .../Migrations/_030_AddProfileProProof.swift | 20 ---------------- .../Database/Models/Profile.swift | 2 +- .../Jobs/UpdateProfilePictureJob.swift | 3 +-- 6 files changed, 17 insertions(+), 39 deletions(-) delete mode 100644 SessionMessagingKit/Database/Migrations/_030_AddProfileProProof.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 37ee53ca9f..74d23532aa 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -199,7 +199,6 @@ 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; - 94AAB1622E28742300A6FA18 /* _030_AddProfileProProof.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1612E28742200A6FA18 /* _030_AddProfileProProof.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B6BAF52E30A88800E718BB /* SessionProState.swift */; }; 94B6BB062E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */; }; @@ -1573,7 +1572,6 @@ 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShineButton.swift; sourceTree = ""; }; 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = PinnedConversationsCTA.webp; sourceTree = ""; }; 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; - 94AAB1612E28742200A6FA18 /* _030_AddProfileProProof.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_AddProfileProProof.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; 94B6BAF52E30A88800E718BB /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimationCropped.webp; sourceTree = ""; }; @@ -4053,7 +4051,6 @@ FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */, FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */, 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */, - 94AAB1612E28742200A6FA18 /* _030_AddProfileProProof.swift */, ); path = Migrations; sourceTree = ""; @@ -6608,7 +6605,6 @@ FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, - 94AAB1622E28742300A6FA18 /* _030_AddProfileProProof.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 1cd5acc5b7..0b52821b5e 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -713,19 +713,23 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl guard let imageData: Data = source.imageData else { return } let isAnimatedImage: Bool = ImageDataManager.isAnimatedImage(imageData) - guard isAnimatedImage && !dependencies[cache: .libSession].isSessionPro else { - self?.updateProfile( - displayPictureUpdate: .currentUserUploadImageData( - data: imageData, - sessionProProof: !isAnimatedImage ? nil : - dependencies.mutate(cache: .libSession, { $0.getProProof() }) - ), - onComplete: { [weak modal] in modal?.close() } - ) + guard ( + !isAnimatedImage || + dependencies[cache: .libSession].isSessionPro || + !dependencies[feature: .sessionProEnabled] + ) else { + self?.showSessionProCTAIfNeeded() return } - self?.showSessionProCTAIfNeeded() + self?.updateProfile( + displayPictureUpdate: .currentUserUploadImageData( + data: imageData, + sessionProProof: !isAnimatedImage ? nil : + dependencies.mutate(cache: .libSession, { $0.getProProof() }) + ), + onComplete: { [weak modal] in modal?.close() } + ) default: modal.close() } diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 6c68dcc84b..128fe2579f 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -48,8 +48,7 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API [ _027_MoveSettingsToLibSession.self, _028_RenameAttachments.self, - _029_AddProMessageFlag.self, - _030_AddProfileProProof.self + _029_AddProMessageFlag.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_030_AddProfileProProof.swift b/SessionMessagingKit/Database/Migrations/_030_AddProfileProProof.swift deleted file mode 100644 index add7903f5a..0000000000 --- a/SessionMessagingKit/Database/Migrations/_030_AddProfileProProof.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUtilitiesKit - -enum _030_AddProfileProProof: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddProfileProProof" - static let minExpectedRunDuration: TimeInterval = 0.1 - static var createdTables: [(FetchableRecord & TableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws { - try db.alter(table: "profile") { t in - t.add(column: "sessionProProof", .text) - } - - MigrationExecution.updateProgress(1) - } -} diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 5268979f59..b625c2a177 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -321,7 +321,7 @@ public extension Profile { public extension Profile { func shoudAnimateProfilePicture(using dependencies: Dependencies) -> Bool { - guard dependencies.hasSet(feature: .sessionProEnabled) && dependencies[feature: .sessionProEnabled] else { return true } + guard dependencies[feature: .sessionProEnabled] else { return true } guard self.id == dependencies[cache: .general].sessionId.hexString else { return dependencies.mutate(cache: .libSession, { $0.validateProProof(for: self) }) diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index 13c5cc8578..038b913d53 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -34,8 +34,7 @@ public enum UpdateProfilePictureJob: JobExecutor { let runJob: () -> () = { /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` - let profile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } - let sessionProProof = dependencies.mutate(cache: .libSession) { $0.getProProof() } + let (profile, sessionProProof) = dependencies.mutate(cache: .libSession) { ($0.profile, $0.getProProof()) } let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } .map { dependencies[singleton: .fileManager].contents(atPath: $0) } From a6bb7a2135fb4896408c4963c82a0b6ac9ed5eb6 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 8 Aug 2025 16:27:45 +1000 Subject: [PATCH 033/244] clean up --- .../Jobs/UpdateProfilePictureJob.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index 038b913d53..b71048a2c9 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -32,7 +32,13 @@ public enum UpdateProfilePictureJob: JobExecutor { return deferred(job) // Don't need to do anything if it's not the main app } - let runJob: () -> () = { + let expirationDate: Date? = dependencies[defaults: .standard, key: .profilePictureExpiresDate] + let lastUploadDate: Date? = dependencies[defaults: .standard, key: .lastProfilePictureUpload] + + if + expirationDate.map({ dependencies.dateNow.timeIntervalSince($0) > 0 }) == true, + lastUploadDate.map({ dependencies.dateNow.timeIntervalSince($0) > (14 * 24 * 60 * 60) }) == true + { /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` let (profile, sessionProProof) = dependencies.mutate(cache: .libSession) { ($0.profile, $0.getProProof()) } let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl @@ -59,19 +65,6 @@ public enum UpdateProfilePictureJob: JobExecutor { } ) } - - if - let profilePicutreExpiresData: Date = dependencies[defaults: .standard, key: .profilePictureExpiresDate], - dependencies.dateNow.timeIntervalSince(profilePicutreExpiresData) > 0 - { - runJob() - } - else if - let lastProfilePictureUpload: Date = dependencies[defaults: .standard, key: .lastProfilePictureUpload], - dependencies.dateNow.timeIntervalSince(lastProfilePictureUpload) > (14 * 24 * 60 * 60) - { - runJob() - } else { // Reset the `nextRunTimestamp` value just in case the last run failed so we don't get stuck // in a loop endlessly deferring the job @@ -82,8 +75,14 @@ public enum UpdateProfilePictureJob: JobExecutor { .updateAll(db, Job.Columns.nextRunTimestamp.set(to: 0)) } } + + Log.info( + .cat, + expirationDate != nil ? + "Deferred as current picture hasn't expired" : + "Deferred as not enough time has passed since the last update" + ) - Log.info(.cat, "Deferred as not enough time has passed since the last update") return deferred(job) } } From 0c1ea791abe8fe0edc5d8424757ed2beb2c5c5c8 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 8 Aug 2025 16:56:55 +1000 Subject: [PATCH 034/244] wip: clean up --- .../Config Handling/LibSession+GroupMembers.swift | 2 +- .../Utilities/Profile+CurrentUser.swift | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 6e1c451aa1..5523efe6ea 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -523,7 +523,7 @@ internal extension LibSession { Profile( id: member.get(\.session_id), name: member.get(\.name), - lastNameUpdate: TimeInterval(member.profile_updated), + lastNameUpdate: TimeInterval(Double(serverTimestampMs) / 1000), nickname: nil, displayPictureUrl: member.get(\.profile_pic.url, nullIfEmpty: true), displayPictureEncryptionKey: (member.get(\.profile_pic.url, nullIfEmpty: true) == nil ? nil : diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index d38c4841d3..a838533459 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -78,12 +78,13 @@ public extension Profile { } } + let profileUpdateTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, - profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, + profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -142,7 +143,7 @@ public extension Profile { /// There were some bugs (somewhere) where some of these timestamps valid could be in seconds or milliseconds so we need to try to /// detect this and convert it to proper seconds (if we don't then we will never update the profile) - func convertToSections(_ maybeValue: Double?) -> TimeInterval { + func convertToSeconds(_ maybeValue: Double?) -> TimeInterval { guard let value: Double = maybeValue else { return 0 } if value > 9_000_000_000_000 { // Microseconds @@ -155,7 +156,7 @@ public extension Profile { } // Name - switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSections(profile.lastNameUpdate))) { + switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSeconds(profile.lastNameUpdate))) { case (.none, _, _): break case (.currentUserUpdate(let name), true, true), (.contactUpdate(let name), false, true): guard let name: String = name, !name.isEmpty, name != profile.name else { break } @@ -172,13 +173,13 @@ public extension Profile { } // Blocks community message requests flag - if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > convertToSections(profile.lastBlocksCommunityMessageRequests) { + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > convertToSeconds(profile.lastBlocksCommunityMessageRequests) { profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) } // Profile picture & profile key - switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSections(profile.displayPictureLastUpdated))) { + switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSeconds(profile.displayPictureLastUpdated))) { case (.none, _, _): break case (.currentUserUploadImageData, _, _), (.groupRemove, _, _), (.groupUpdateTo, _, _): preconditionFailure("Invalid options for this function") From 02013aae89aed19df8003d4efad0b3a88ef340ff Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 11 Aug 2025 09:39:09 +1000 Subject: [PATCH 035/244] Fixed a couple of cases where we were incorrectly resolving colors --- SessionUIKit/Style Guide/ThemeManager.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 3b018a8008..1ceae3a4dc 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -234,14 +234,19 @@ public enum ThemeManager { with primaryColor: Theme.PrimaryColor ) -> T? { switch value { - case .value(let value, let alpha): return T.resolve(value, for: theme)?.alpha(alpha) + case .value(let value, let alpha): + let color: T? = color(for: value, in: theme, with: primaryColor) + return color?.alpha(alpha) + case .primary: return T.resolve(primaryColor) case .explicitPrimary(let explicitPrimary): return T.resolve(explicitPrimary) case .highlighted(let value, let alwaysDarken): + let color: T? = color(for: value, in: theme, with: primaryColor)! + switch (currentTheme.interfaceStyle, alwaysDarken) { - case (.light, _), (_, true): return T.resolve(value, for: theme)?.brighten(-0.06) - default: return T.resolve(value, for: theme)?.brighten(0.08) + case (.light, _), (_, true): return color?.brighten(-0.06) + default: return color?.brighten(0.08) } case .dynamicForInterfaceStyle(let light, let dark): From aa64d363077e4a0562ffbae756df3b5d87838c3c Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 11 Aug 2025 09:59:57 +1000 Subject: [PATCH 036/244] clean up --- .../Utilities/Profile+CurrentUser.swift | 20 +++---------------- .../ProfilePictureView+Convenience.swift | 19 +++++++++++------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index a838533459..64094e9bca 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -141,22 +141,8 @@ public extension Profile { let profile: Profile = Profile.fetchOrCreate(db, id: publicKey) var profileChanges: [ConfigColumnAssignment] = [] - /// There were some bugs (somewhere) where some of these timestamps valid could be in seconds or milliseconds so we need to try to - /// detect this and convert it to proper seconds (if we don't then we will never update the profile) - func convertToSeconds(_ maybeValue: Double?) -> TimeInterval { - guard let value: Double = maybeValue else { return 0 } - - if value > 9_000_000_000_000 { // Microseconds - return (value / 1_000_000) - } else if value > 9_000_000_000 { // Milliseconds - return (value / 1000) - } - - return TimeInterval(value) // Seconds - } - // Name - switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSeconds(profile.lastNameUpdate))) { + switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > profile.lastNameUpdate.defaulting(to: 0))) { case (.none, _, _): break case (.currentUserUpdate(let name), true, true), (.contactUpdate(let name), false, true): guard let name: String = name, !name.isEmpty, name != profile.name else { break } @@ -173,13 +159,13 @@ public extension Profile { } // Blocks community message requests flag - if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > convertToSeconds(profile.lastBlocksCommunityMessageRequests) { + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests.defaulting(to: 0) { profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) } // Profile picture & profile key - switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > convertToSeconds(profile.displayPictureLastUpdated))) { + switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > profile.displayPictureLastUpdated.defaulting(to: 0))) { case (.none, _, _): break case (.currentUserUploadImageData, _, _), (.groupRemove, _, _), (.groupUpdateTo, _, _): preconditionFailure("Invalid options for this function") diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 6a533ec77b..d4f3e3e0a8 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -48,18 +48,23 @@ public extension ProfilePictureView { ) switch (explicitPath, publicKey.isEmpty, threadVariant) { + // TODO: Deal with this case later when implement group related Pro features + case (.some(let path), _, .legacyGroup), (.some(let path), _, .group): fallthrough + case (.some(let path), _, .community): + /// If we are given an explicit `displayPictureUrl` then only use that + return (Info( + source: .url(URL(fileURLWithPath: path)), + shouldAnimated: true, + isCurrentUser: false, + icon: profileIcon + ), nil) + case (.some(let path), _, _): - let shouldAnimated: Bool = { - guard let profile: Profile = profile else { - return threadVariant == .community - } - return profile.shoudAnimateProfilePicture(using: dependencies) - }() /// If we are given an explicit `displayPictureUrl` then only use that return ( Info( source: .url(URL(fileURLWithPath: path)), - shouldAnimated: shouldAnimated, + shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) == true), isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), icon: profileIcon ), From c41a7800dca503edb407d032a7edbe54957c1d95 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 11 Aug 2025 11:17:14 +1000 Subject: [PATCH 037/244] clean up --- .../Database/Models/Profile.swift | 2 +- .../ProfilePictureView+Convenience.swift | 17 ++++++-------- SessionSnodeKit/Types/Network.swift | 2 +- .../Modals & Toast/ConfirmationModal.swift | 2 +- .../Components/ProfilePictureView.swift | 22 +++++++++++-------- .../Components/SwiftUI/ProCTAModal.swift | 3 ++- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index b625c2a177..45229f86c4 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -320,7 +320,7 @@ public extension Profile { // MARK: - Convenience public extension Profile { - func shoudAnimateProfilePicture(using dependencies: Dependencies) -> Bool { + func animationBehaviour(using dependencies: Dependencies) -> ProfilePictureView.Info.AnimationBehaviour { guard dependencies[feature: .sessionProEnabled] else { return true } guard self.id == dependencies[cache: .general].sessionId.hexString else { diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index d4f3e3e0a8..3f670253e6 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -54,8 +54,7 @@ public extension ProfilePictureView { /// If we are given an explicit `displayPictureUrl` then only use that return (Info( source: .url(URL(fileURLWithPath: path)), - shouldAnimated: true, - isCurrentUser: false, + animationBehaviour: .contactEnableAnimation, icon: profileIcon ), nil) @@ -64,8 +63,7 @@ public extension ProfilePictureView { return ( Info( source: .url(URL(fileURLWithPath: path)), - shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) == true), - isCurrentUser: (publicKey == dependencies[cache: .general].sessionId.hexString), + animationBehaviour: profile?.animationBehaviour(using: dependencies), icon: profileIcon ), nil @@ -81,8 +79,7 @@ public extension ProfilePictureView { case .hero, .modal: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), - shouldAnimated: true, - isCurrentUser: false, + animationBehaviour: .contactEnableAnimation, inset: UIEdgeInsets( top: 12, left: 12, @@ -121,7 +118,7 @@ public extension ProfilePictureView { return ( Info( source: source, - shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + shouldAnimate: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), icon: profileIcon ), @@ -145,7 +142,7 @@ public extension ProfilePictureView { return Info( source: source, - shouldAnimated: other.shoudAnimateProfilePicture(using: dependencies), + shouldAnimate: other.shoudAnimateProfilePicture(using: dependencies), isCurrentUser: (other.id == dependencies[cache: .general].sessionId.hexString), icon: additionalProfileIcon ) @@ -153,7 +150,7 @@ public extension ProfilePictureView { .defaulting( to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), - shouldAnimated: false, + shouldAnimate: false, isCurrentUser: false, renderingMode: .alwaysTemplate, themeTintColor: .white, @@ -189,7 +186,7 @@ public extension ProfilePictureView { return ( Info( source: source, - shouldAnimated: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), + shouldAnimate: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), icon: profileIcon), nil diff --git a/SessionSnodeKit/Types/Network.swift b/SessionSnodeKit/Types/Network.swift index 3c2803f534..51959df78e 100644 --- a/SessionSnodeKit/Types/Network.swift +++ b/SessionSnodeKit/Types/Network.swift @@ -132,7 +132,7 @@ public extension Network { using dependencies: Dependencies ) throws -> PreparedRequest { var headers: [HTTPHeader: String] = [:] - if dependencies.hasSet(feature: .shortenFileTTL) { + if dependencies[feature: .shortenFileTTL] { headers = [.fileCustomTTL : "60"] } return try PreparedRequest( diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 822ca029c4..b9680f1af7 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -550,7 +550,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.update( ProfilePictureView.Info( source: (source ?? placeholder), - shouldAnimated: true, + shouldAnimate: true, isCurrentUser: true, icon: icon ) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index 8c8568da50..d4e2ac7b09 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -6,9 +6,15 @@ import Lucide public final class ProfilePictureView: UIView { public struct Info { + public enum AnimationBehaviour { + case alwaysEnableAnimation + case contactDisableAnimation + case contactEnableAnimation + case currentUser(SessionProManagerType?) + } + let source: ImageDataManager.DataSource? - let shouldAnimated: Bool - let isCurrentUser: Bool + let animationBehaviour: AnimationBehaviour let renderingMode: UIImage.RenderingMode? let themeTintColor: ThemeValue? let inset: UIEdgeInsets @@ -18,8 +24,7 @@ public final class ProfilePictureView: UIView { public init( source: ImageDataManager.DataSource?, - shouldAnimated: Bool, - isCurrentUser: Bool, + animationBehaviour: AnimationBehaviour, renderingMode: UIImage.RenderingMode? = nil, themeTintColor: ThemeValue? = nil, inset: UIEdgeInsets = .zero, @@ -28,8 +33,7 @@ public final class ProfilePictureView: UIView { forcedBackgroundColor: ForcedThemeValue? = nil ) { self.source = source - self.shouldAnimated = shouldAnimated - self.isCurrentUser = isCurrentUser + self.animationBehaviour = animationBehaviour self.renderingMode = renderingMode self.themeTintColor = themeTintColor self.inset = inset @@ -110,7 +114,7 @@ public final class ProfilePictureView: UIView { } private var dataManager: ImageDataManagerType? - private var sessionProState: SessionProManagerType? + private var currentUserSessionProState: SessionProManagerType? public var disposables: Set = Set() public var size: Size { didSet { @@ -548,7 +552,7 @@ public final class ProfilePictureView: UIView { imageView.image = source.directImage?.withRenderingMode(renderingMode) case (.some(let source), _): - imageView.shouldAnimateImage = info.shouldAnimated + imageView.shouldAnimateImage = info.shouldAnimate imageView.loadImage(source) default: imageView.image = nil @@ -599,7 +603,7 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = false case (.some(let source), _): - additionalImageView.shouldAnimateImage = additionalInfo.shouldAnimated + additionalImageView.shouldAnimateImage = additionalInfo.shouldAnimate additionalImageView.loadImage(source) additionalImageContainerView.isHidden = false diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 9545ee17e1..b547d200da 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -36,7 +36,8 @@ public struct ProCTAModal: View { } } /// Note: This is a hack to manually position the animated avatar in the CTA background image to prevent heavy loading for the - /// animated webp. + /// animated webp. These coordinates are based on the full size image and get scaled during rendering based on the actual size + /// of the modal. public var animatedAvatarImagePadding: (leading: CGFloat, top: CGFloat) { switch self { case .generic: From 5b0aae3e318e24934ab7f9a2a5ba1a94ca3f2741 Mon Sep 17 00:00:00 2001 From: Teamified Date: Mon, 11 Aug 2025 11:14:37 +0800 Subject: [PATCH 038/244] Fix join community tabbar spacing --- Session/Open Groups/JoinOpenGroupVC.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index ace6225588..5b3f4e4177 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -76,7 +76,17 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC setNavBarTitle("communityJoin".localized()) view.themeBackgroundColor = .backgroundSecondary - let navBarHeight: CGFloat = (navigationController?.navigationBar.frame.size.height ?? 0) + + // Only account for navigation header when view controller + // presentation type is `fullScreen` + var navBarHeight: CGFloat { + switch modalPresentationStyle { + case .fullScreen: + return navigationController?.navigationBar.frame.size.height ?? 0 + default: + return 0 + } + } let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) closeButton.themeTintColor = .textPrimary From 55b97a874bdb38ff7d36a14ad7c16806a3923787 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 11 Aug 2025 14:25:32 +1000 Subject: [PATCH 039/244] rename and refactor on animation behaviour for profile picture views --- .../Views & Modals/IncomingCallBanner.swift | 2 +- Session/Conversations/ConversationVC.swift | 2 +- .../Input View/MentionSelectionView.swift | 4 +- .../Message Cells/VisibleMessageCell.swift | 4 +- Session/Home/HomeVC.swift | 2 +- Session/Shared/FullConversationCell.swift | 8 +-- .../Views/SessionCell+AccessoryView.swift | 4 +- .../Database/Models/Profile.swift | 10 ---- .../ProfilePictureView+Convenience.swift | 31 +++++++---- .../SimplifiedConversationCell.swift | 8 ++- .../Modals & Toast/ConfirmationModal.swift | 5 +- .../Components/ProfilePictureView.swift | 51 +++++++++---------- 12 files changed, 65 insertions(+), 66 deletions(-) diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 96de6f87b6..29374fd112 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -26,7 +26,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, dataManager: dependencies[singleton: .imageDataManager], - sessionProState: dependencies[singleton: .sessionProState] + currentUserSessionProState: dependencies[singleton: .sessionProState] ) private lazy var displayNameLabel: UILabel = { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 7552a680a6..ee7ae68525 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1420,7 +1420,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa let profilePictureView = ProfilePictureView( size: .navigation, dataManager: viewModel.dependencies[singleton: .imageDataManager], - sessionProState: viewModel.dependencies[singleton: .sessionProState] + currentUserSessionProState: viewModel.dependencies[singleton: .sessionProState] ) profilePictureView.update( publicKey: threadData.threadId, // Contact thread uses the contactId diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 6169b88865..37425e562b 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -125,7 +125,7 @@ private extension MentionSelectionView { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, dataManager: nil, - sessionProState: nil + currentUserSessionProState: nil ) private lazy var displayNameLabel: UILabel = { @@ -203,7 +203,7 @@ private extension MentionSelectionView { currentUserSessionIds: currentUserSessionIds ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: profile.id, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 833d94aae1..a89c7a9343 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -68,7 +68,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, dataManager: nil, - sessionProState: nil + currentUserSessionProState: nil ) lazy var bubbleBackgroundView: UIView = { @@ -324,7 +324,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView.isHidden = !cellViewModel.canHaveProfile profilePictureView.alpha = (profileShouldBeVisible ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.authorId, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index ec82f654fd..e208388918 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -461,7 +461,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let profilePictureView = ProfilePictureView( size: .navigation, dataManager: viewModel.dependencies[singleton: .imageDataManager], - sessionProState: viewModel.dependencies[singleton: .sessionProState] + currentUserSessionProState: viewModel.dependencies[singleton: .sessionProState] ) profilePictureView.accessibilityIdentifier = "User settings" profilePictureView.accessibilityLabel = "User settings" diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index ceb4ceaded..915657ac26 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -24,7 +24,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, dataManager: nil, - sessionProState: nil + currentUserSessionProState: nil ) private lazy var displayNameLabel: UILabel = { @@ -281,7 +281,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // MARK: --Search Results public func updateForDefaultContacts(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -358,7 +358,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC using dependencies: Dependencies ) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -434,7 +434,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC ) ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 36f5067a25..27e9dbf776 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -636,7 +636,7 @@ extension SessionCell { // MARK: -- DisplayPicture private func createDisplayPictureView() -> ProfilePictureView { - return ProfilePictureView(size: .list, dataManager: nil, sessionProState: nil) + return ProfilePictureView(size: .list, dataManager: nil, currentUserSessionProState: nil) } private func layoutDisplayPictureView(_ view: UIView?, size: ProfilePictureView.Size) { @@ -661,7 +661,7 @@ extension SessionCell { profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) profilePictureView.size = accessory.size profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: accessory.id, threadVariant: accessory.threadVariant, diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 45229f86c4..d8ba4276e8 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -320,16 +320,6 @@ public extension Profile { // MARK: - Convenience public extension Profile { - func animationBehaviour(using dependencies: Dependencies) -> ProfilePictureView.Info.AnimationBehaviour { - guard dependencies[feature: .sessionProEnabled] else { return true } - - guard self.id == dependencies[cache: .general].sessionId.hexString else { - return dependencies.mutate(cache: .libSession, { $0.validateProProof(for: self) }) - } - - return dependencies[cache: .libSession].isSessionPro - } - func displayNameForMention( for threadVariant: SessionThread.Variant = .contact, ignoringNickname: Bool = false, diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index 3f670253e6..e73f328c74 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -54,7 +54,7 @@ public extension ProfilePictureView { /// If we are given an explicit `displayPictureUrl` then only use that return (Info( source: .url(URL(fileURLWithPath: path)), - animationBehaviour: .contactEnableAnimation, + animationBehaviour: .generic(true), icon: profileIcon ), nil) @@ -63,7 +63,7 @@ public extension ProfilePictureView { return ( Info( source: .url(URL(fileURLWithPath: path)), - animationBehaviour: profile?.animationBehaviour(using: dependencies), + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), icon: profileIcon ), nil @@ -79,7 +79,7 @@ public extension ProfilePictureView { case .hero, .modal: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), - animationBehaviour: .contactEnableAnimation, + animationBehaviour: .generic(true), inset: UIEdgeInsets( top: 12, left: 12, @@ -118,8 +118,7 @@ public extension ProfilePictureView { return ( Info( source: source, - shouldAnimate: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), - isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), icon: profileIcon ), additionalProfile @@ -142,16 +141,14 @@ public extension ProfilePictureView { return Info( source: source, - shouldAnimate: other.shoudAnimateProfilePicture(using: dependencies), - isCurrentUser: (other.id == dependencies[cache: .general].sessionId.hexString), + animationBehaviour: ProfilePictureView.animationBehaviour(from: other, using: dependencies), icon: additionalProfileIcon ) } .defaulting( to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), - shouldAnimate: false, - isCurrentUser: false, + animationBehaviour: .generic(false), renderingMode: .alwaysTemplate, themeTintColor: .white, inset: UIEdgeInsets( @@ -186,8 +183,7 @@ public extension ProfilePictureView { return ( Info( source: source, - shouldAnimate: (profile?.shoudAnimateProfilePicture(using: dependencies) ?? false), - isCurrentUser: (profile?.id == dependencies[cache: .general].sessionId.hexString), + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), icon: profileIcon), nil ) @@ -195,6 +191,19 @@ public extension ProfilePictureView { } } +public extension ProfilePictureView { + static func animationBehaviour(from profile: Profile?, using dependencies: Dependencies) -> Info.AnimationBehaviour { + guard dependencies[feature: .sessionProEnabled] else { return .generic(true) } + guard let profile: Profile = profile else { return .generic(false) } + + guard profile.id == dependencies[cache: .general].sessionId.hexString else { + return .contact(dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) })) + } + + return .currentUser(dependencies[cache: .libSession].isSessionPro) + } +} + public extension ProfilePictureSwiftUI { init?( size: ProfilePictureView.Size, diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index c22018ecca..8e2c657fc1 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -40,7 +40,11 @@ final class SimplifiedConversationCell: UITableViewCell { }() private lazy var profilePictureView: ProfilePictureView = { - let view: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil, sessionProState: nil) + let view: ProfilePictureView = ProfilePictureView( + size: .list, + dataManager: nil, + currentUserSessionProState: nil + ) view.translatesAutoresizingMaskIntoConstraints = false return view @@ -88,7 +92,7 @@ final class SimplifiedConversationCell: UITableViewCell { public func update(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { accentLineView.alpha = (cellViewModel.threadIsBlocked == true ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setSessionProState(dependencies[singleton: .sessionProState]) + profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index b9680f1af7..05664600cf 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -216,7 +216,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { private lazy var profileView: ProfilePictureView = ProfilePictureView( size: .modal, dataManager: nil, - sessionProState: nil + currentUserSessionProState: nil ) private lazy var textToConfirmContainer: UIView = { @@ -550,8 +550,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.update( ProfilePictureView.Info( source: (source ?? placeholder), - shouldAnimate: true, - isCurrentUser: true, + animationBehaviour: .currentUser(true), icon: icon ) ) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index d4e2ac7b09..370da7d6ac 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -7,10 +7,16 @@ import Lucide public final class ProfilePictureView: UIView { public struct Info { public enum AnimationBehaviour { - case alwaysEnableAnimation - case contactDisableAnimation - case contactEnableAnimation - case currentUser(SessionProManagerType?) + case generic(Bool) // For communities and when Pro is not enabled + case contact(Bool) + case currentUser(Bool) + + public var enableAnimation: Bool { + switch self { + case .generic(let enableAnimation), .contact(let enableAnimation), .currentUser(let enableAnimation): + return enableAnimation + } + } } let source: ImageDataManager.DataSource? @@ -300,8 +306,7 @@ public final class ProfilePictureView: UIView { // MARK: - Lifecycle - public init(size: Size, dataManager: ImageDataManagerType?, sessionProState: SessionProManagerType?) { - self.sessionProState = sessionProState + public init(size: Size, dataManager: ImageDataManagerType?, currentUserSessionProState: SessionProManagerType?) { self.dataManager = dataManager self.size = size @@ -310,19 +315,9 @@ public final class ProfilePictureView: UIView { clipsToBounds = true setUpViewHierarchy() - sessionProState?.isSessionProPublisher - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink( - receiveValue: { [weak self] isPro in - if isPro { - self?.startAnimatingIfNeeded() - } else { - self?.stopAnimatingIfNeeded() - } - } - ) - .store(in: &disposables) + if let currentUserSessionProState: SessionProManagerType = currentUserSessionProState { + setCurrentUserSessionProState(currentUserSessionProState) + } } public required init?(coder: NSCoder) { @@ -418,9 +413,11 @@ public final class ProfilePictureView: UIView { self.additionalImageView.setDataManager(dataManager) } - public func setSessionProState(_ sessionProState: SessionProManagerType) { - self.sessionProState = sessionProState - sessionProState.isSessionProPublisher + public func setCurrentUserSessionProState(_ currentUserSessionProState: SessionProManagerType) { + self.currentUserSessionProState = currentUserSessionProState + + // TODO: Refactor this to use async/await instead of Combine + currentUserSessionProState.isSessionProPublisher .subscribe(on: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink( @@ -552,13 +549,13 @@ public final class ProfilePictureView: UIView { imageView.image = source.directImage?.withRenderingMode(renderingMode) case (.some(let source), _): - imageView.shouldAnimateImage = info.shouldAnimate + imageView.shouldAnimateImage = info.animationBehaviour.enableAnimation imageView.loadImage(source) default: imageView.image = nil } - if info.isCurrentUser { + if case .currentUser(_) = info.animationBehaviour { self.shouldAnimateForCurrentUserProUpgrade = .main } @@ -603,7 +600,7 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = false case (.some(let source), _): - additionalImageView.shouldAnimateImage = additionalInfo.shouldAnimate + additionalImageView.shouldAnimateImage = additionalInfo.animationBehaviour.enableAnimation additionalImageView.loadImage(source) additionalImageContainerView.isHidden = false @@ -612,7 +609,7 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = true } - if additionalInfo.isCurrentUser { + if case .currentUser(_) = additionalInfo.animationBehaviour { self.shouldAnimateForCurrentUserProUpgrade = .additional } @@ -703,7 +700,7 @@ public struct ProfilePictureSwiftUI: UIViewRepresentable { ProfilePictureView( size: size, dataManager: dataManager, - sessionProState: sessionProState + currentUserSessionProState: sessionProState ) } From 61f1cfae2bc9c5e4c283d6d2d5f5b325fc4c47e2 Mon Sep 17 00:00:00 2001 From: Bilb <1544279+Bilb@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:10:23 +0000 Subject: [PATCH 040/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 22105 ++++++++++------ 1 file changed, 14625 insertions(+), 7480 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 1ed97ebd47..5e4040a9a3 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -980,6 +980,12 @@ "value" : "معرف الحساب" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesab ID-si" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -992,6 +998,12 @@ "value" : "ID účtu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account-ID" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -1004,12 +1016,30 @@ "value" : "Konta ID" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID de cuenta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID de cuenta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ID du client" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "खाता ID" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -1022,6 +1052,18 @@ "value" : "ID Akun" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID account" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アカウント ID" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1040,11 +1082,53 @@ "value" : "Identyfikator konta" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID da Conta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cont" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID аккаунта" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account ID" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesap kimliği" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ідентифікатор облікового запису" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "账户 ID" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "帳號 ID" + } } } }, @@ -6859,6 +6943,12 @@ "value" : "Tilføj administratorer" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administratoren hinzufügen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -6871,12 +6961,30 @@ "value" : "Aldoni administrantojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir Administradores" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir Administradores" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter des administrateurs" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एडमिन जोड़ें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -6889,6 +6997,18 @@ "value" : "Tambah Admin" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi amministratori" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理者を追加する" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -6907,6 +7027,36 @@ "value" : "Dodaj administratorów" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar administradores" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adaugă administratori" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить администраторов" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lägg till administratörer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici Ekle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -6918,12 +7068,24 @@ "state" : "translated", "value" : "添加管理员" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增管理員" + } } } }, "addAdminsDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admin edəcəyiniz istifadəçinin Hesab ID-sini daxil edin.

    Birdən çox istifadəçi əlavə etmək üçün vergüllə ayrılmış hər Hesab ID-sini daxil edin. Bir dəfəyə 20-yə qədər Hesab ID-si daxil edilə bilər." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -6936,24 +7098,60 @@ "value" : "Zadejte ID účtu uživatele, kterého povyšujete na správce.

    Chcete-li přidat více uživatelů, zadejte každé ID účtu oddělené čárkou. Najednou lze zadat až 20 ID účtů." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du zum Administrator ernennst.

    Um mehrere Nutzer hinzuzufügen, gib jede Account-ID durch ein Komma getrennt ein. Es können bis zu 20 Account-IDs gleichzeitig angegeben werden." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enter the Account ID of the user you are promoting to admin.

    To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario que desea promover a administrador.

    Para agregar varios usuarios, ingrese cada Account ID separado por una coma. Puede especificar hasta 20 Account ID a la vez." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario que desea promover a administrador.

    Para agregar varios usuarios, ingrese cada Account ID separado por una coma. Puede especificar hasta 20 Account ID a la vez." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l'utilisateur que vous souhaitez promouvoir en administrateur.

    Pour ajouter plusieurs utilisateurs, saisissez chaque identifiant de compte séparé par une virgule. Vous pouvez spécifier jusqu'à 20 identifiants à la fois." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप एडमिन बना रहे हैं।

    एक से अधिक उपयोगकर्ताओं को जोड़ने के लिए, प्रत्येक Account ID को कॉमा से अलग करके दर्ज करें। एक बार में अधिकतम 20 Account ID दर्ज किए जा सकते हैं।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg a felhasználó fiókazonosítóját, akit adminisztrátorrá kíván kinevezni.

    Egyszerre több felhasználó hozzáadásához adja meg az egyes fiókazonosítókat vesszővel elválasztva. Egyszerre legfeljebb 20 fiókazonosító adható meg." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente che vuoi promuovere ad amministratore.

    Per aggiungere più utenti, inserisci ogni Account ID separato da una virgola. È possibile specificare fino a 20 Account ID alla volta." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理者に昇格させるユーザーのAccount IDを入力してください。

    複数のユーザーを追加するには、各Account IDをカンマで区切って入力してください。一度に最大20件まで指定できます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -6972,6 +7170,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, którego chcesz awansować na administratora.

    Aby dodać wielu użytkowników, wpisz każdy identyfikator konta oddzielone przecinkiem. Można jednocześnie podać maksymalnie 20 identyfikatorów kont." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que está a promover a administrador.

    Para adicionar vários utilizadores, introduza cada ID de Conta separado por vírgulas. Podem ser especificados até 20 IDs de Conta de cada vez." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu ID-ul contului utilizatorului pe care îl promovezi ca administrator.

    Pentru a adăuga mai mulți utilizatori, introduceți fiecare ID al contului separat prin virgulă. Pot fi specificate până la 20 de ID-uri de cont o dată." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы повышаете до администратора.

    Чтобы добавить нескольких пользователей, введите ID каждого аккаунта через запятую. Одновременно можно указать до 20 идентификаторов учётных записей." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du gör till administratör.

    För att lägga till flera användare, ange varje Account ID separerat med ett kommatecken. Upp till 20 Account ID:er kan anges åt gången." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici olarak atadığınız kullanıcının Hesap Kimliğini girin.

    Birden fazla kullanıcı eklemek için her Hesap Kimliğini virgülle ayırarak girin. Tek seferde en fazla 20 Hesap Kimliği belirtilebilir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -6983,6 +7211,12 @@ "state" : "translated", "value" : "请输入您正在授权为管理员的用户的帐户 ID。

    要添加多个用户,请输入用逗号分隔的每个帐户 ID。一次最多可以指定20个帐户 ID。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要晉升為管理員的使用者的 Account ID。

    若要新增多位使用者,請輸入以逗號分隔的每個 Account ID。一次最多可指定 20 個 Account ID。" + } } } }, @@ -12887,6 +13121,12 @@ "value" : "Promozione non inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進が送信されていません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -12905,6 +13145,18 @@ "value" : "Promocja niewysłana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoção não enviada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovarea nu a fost trimisă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -12940,6 +13192,12 @@ "state" : "translated", "value" : "授权未发送" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升未傳送" + } } } }, @@ -13521,6 +13779,12 @@ "value" : "Stato promozione sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進のステータスが不明です" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -13539,6 +13803,18 @@ "value" : "Status promocji nieznany" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado da promoção desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statusul promovării necunoscut" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -13574,6 +13850,12 @@ "state" : "translated", "value" : "授权状态未知" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升狀態未知" + } } } }, @@ -18299,6 +18581,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドミンへの昇進を送信中" + } + } + } + } + } + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -18417,6 +18721,68 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar promoção de administrador" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar promoções de administrador" + } + } + } + } + } + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit promovările la nivel de administrator" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimite promovarea la nivel de administrator" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit promovările la nivel de administrator" + } + } + } + } + } + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -18596,6 +18962,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升中" + } + } + } + } + } + } } } }, @@ -20536,484 +20924,10 @@ "appearanceAutoDarkMode" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto donker-modus" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الوضع المظلم التلقائي" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avto qaranlıq rejim" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "آٹو ڈارک موڈ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Аўтаматычны цёмная тэма" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматичен тъмен режим" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mode fosc automàtic" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatický tmavý režim" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto ddull tywyll" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto mørk tilstand" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatischer Dunkler Modus" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αυτόματη σκοτεινή λειτουργία" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aŭtomata malhela reĝimo" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo oscuro automático" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo oscuro automático" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autom. tumerežiim" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "حالت تیرهٔ خودکار" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automaattinen tumma tila" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thème sombre automatique" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yanayin duhu-atomatik" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מצב כהה אוטומטי" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "स्वचालित डार्क-मोड" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski tamni način" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatikus sötét mód" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ավտոմատ մութ ռեժիմ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mode gelap otomatis" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modalità scura automatica" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "オートダークモード" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ავტომატიკური დაბნელების რეჟიმი" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ម៉ូដងងឹតដោយស្វ័យប្រវត្តិ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸ್ವಯಂ ಡಾರ್ಕ್ ಮೋಡ್" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "자동 다크 모드" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ئۆتۆ مود - تەختە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Moda tarî bi otomatîk" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ໂໂມດມືດອັດຕະໂນມັດ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatinis tamsus režimas" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automātiska tumša tēma" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматски темен режим" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автомат бараан горим" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto mode-gelap" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "အလိုအလျောက် အမှောင်-mode" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørk-modus" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørkmodus" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "स्वतः अँध्यारो मोड" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatische nachtmodus" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørkmodus" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Self mode yowala yowala" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਟੋ ਡਾਰਕ-ਮੋਡ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatyczny tryb ciemny" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mod întunecat automat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматический темный режим" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski tamni mod" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ස්වයං අඳුරු ප්‍රකාරය" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatický tmavý režim" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Samodejni temni način" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modaliteti automatik i errësirës" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Аутоматски тамни режим" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski režim tamne teme" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatiskt mörkläge" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Njia ya giza ya kiotomatiki" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "தானியங்கு இருண்ட பயன்முறை" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ఆటో డార్క్-మోడ్" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "โหมดมืดอัตโนมัติ" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Otomatik karanlık-mod" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматичний темний режим" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آٹو ڈارک موڈ" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avtomatik qorong'u rejim" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chế độ tối tự động" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Imowudi emnyama ngokuzenzekelayo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "自动开启深色模式" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動深色模式" + "value" : "Auto Dark Mode" } } } @@ -28272,6 +28186,18 @@ "value" : "Piktogramo de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono de la app" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28296,6 +28222,12 @@ "value" : "Ikon Aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona dell'app" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28326,6 +28258,18 @@ "value" : "Ikona aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28338,6 +28282,12 @@ "value" : "App-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama ikonu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28355,6 +28305,12 @@ "state" : "translated", "value" : "应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示" + } } } }, @@ -28367,6 +28323,12 @@ "value" : "تغيير اسم و أيقونة التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu və adını dəyişdir" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28379,6 +28341,12 @@ "value" : "Změnit ikonu a název aplikace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Symbol und Name ändern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -28391,18 +28359,48 @@ "value" : "Ŝanĝi piktogramon de aplikaĵo kaj nomon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar icono y nombre de la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar icono y nombre de la aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Changer l'icône et le nom de l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन और नाम बदलें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az alkalmazás ikonjának és nevének módosítása" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia nome e icona dell'app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリのアイコンと名前を変更" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28421,11 +28419,53 @@ "value" : "Zmień ikonę i nazwę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar Ícone e Nome da Aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimbă pictograma și numele aplicației" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить имя и иконку приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byt appikon och namn" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Simgesini ve Adını Değiştir" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Змінити значок і назву застосунку" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改应用图标和名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "變更應用程式圖示與名稱" + } } } }, @@ -28438,6 +28478,12 @@ "value" : "يتطلب تغيير أيقونة التطبيق واسمه إغلاق {app_name}. سوف تستمر الإشعارات في استخدام أيقونة واسم {app_name} الافتراضي." } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu və adını dəyişdirərkən {app_name} bağlanılacaq. Bildirişlər, ilkin {app_name} ikonunu və adını istifadə etməyə davam edəcək." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28450,24 +28496,60 @@ "value" : "Změna ikony a názvu aplikace vyžaduje, aby byla aplikace {app_name} zavřena. Oznámení budou nadále používat výchozí ikonu a název {app_name}." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Ändern des App-Symbols und -Namens erfordert, dass {app_name} beendet wird. Benachrichtigungen verwenden weiterhin das Standardsymbol und den Standardnamen von {app_name}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Changing the app icon and name requires {app_name} to be closed. Notifications will continue to use the default {app_name} icon and name." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar el icono y el nombre de la aplicación requiere cerrar {app_name}. Las notificaciones seguirán utilizando el icono y nombre predeterminados de {app_name}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar el icono y el nombre de la aplicación requiere cerrar {app_name}. Las notificaciones seguirán utilizando el icono y nombre predeterminados de {app_name}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Changer l'icône et le nom de l'application nécessite la fermeture de {app_name}. Les notifications continueront d'utiliser l'icône et le nom par défaut de {app_name}." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन और नाम बदलने के लिए {app_name} को बंद करना आवश्यक है। सूचनाएं डिफ़ॉल्ट {app_name} आइकन और नाम का उपयोग करना जारी रखेंगी।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az alkalmazás ikonjának és nevének módosításához be kell zárni a Session alkalmazást. Az értesítések továbbra is az alapértelmezett Session ikont és nevet fogják használni." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La modifica dell'icona e del nome dell'app richiede la chiusura di {app_name}. Le notifiche continueranno a mostrare l'icona e il nome predefiniti di {app_name}." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリのアイコンと名前を変更するには {app_name} を終了する必要があります。通知では引き続きデフォルトの {app_name} のアイコンと名前が使用されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28486,11 +28568,53 @@ "value" : "Zmiana ikony i nazwy aplikacji wymaga zamknięcia aplikacji {app_name}. Powiadomienia nadal będą używać domyślnej ikony i nazwy {app_name}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar o ícone e nome da aplicação requer o encerramento do {app_name}. As notificações continuarão a usar o ícone e nome predefinidos de {app_name}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimbarea pictogramei și a numelui aplicației necesită închiderea {app_name}. Notificările vor continua să folosească pictograma și numele implicit {app_name}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для изменения значка и названия приложения необходимо закрыть приложение {app_name}. Уведомления продолжат использовать стандартный значок и название приложения {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "För att byta appikon och namn måste {app_name} stängas. Aviseringar kommer fortsätta använda standardikonen och namnet för {app_name}." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama simgesini ve adını değiştirmek, {app_name} uygulamasının kapatılmasını gerektirir. Bildirimler, varsayılan {app_name} simgesini ve adını kullanmaya devam edecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зміна значка та назви застосунку потребує закриття {app_name}. Сповіщення надходитимуть зі стандартною назвою та значком {app_name}." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改应用图标和名称需要关闭 {app_name}。通知仍将使用默认的 {app_name} 图标和名称。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "變更應用程式圖示與名稱需要關閉 {app_name}。通知將繼續使用預設的 {app_name} 圖示與名稱。" + } } } }, @@ -28533,6 +28657,18 @@ "value" : "Alternate app icon and name is displayed on home screen and app drawer." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono y nombre alternativos de la app se muestran en la pantalla principal y el cajón de aplicaciones." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono y nombre alternativos de la app se muestran en la pantalla principal y el cajón de aplicaciones." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28551,6 +28687,18 @@ "value" : "Az alternatív alkalmazásikon és név megjelenik a kezdőképernyőn és az alkalmazásfiókban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona e il nome alternativi dell'app sono visualizzati nella schermata principale e nel cassetto delle app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンと名前は、ホーム画面およびアプリドロワーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28569,6 +28717,18 @@ "value" : "Alternatywna ikona i nazwa aplikacji są wyświetlane na ekranie głównym i w szufladzie aplikacji." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone e nome alternativos da aplicação são exibidos no ecrã principal e na gaveta de aplicações." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma și numele alternative ale aplicației sunt afișate pe ecranul principal și în sertarul aplicației." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28581,6 +28741,12 @@ "value" : "Alternativ app-ikon och namn visas på hem skärmen och app." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu ve ismi, ana ekran ve uygulama çekmecesinde gözükür." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28598,6 +28764,12 @@ "state" : "translated", "value" : "替代的应用图标与应用名会显示在主页和应用抽屉中。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "替代的應用程式圖示與名稱會顯示於主畫面與應用程式抽屜中。" + } } } }, @@ -28610,6 +28782,12 @@ "value" : "يتم عرض أيقونة التطبيق المحدد والاسم على الشاشة الرئيسة و درج التطبيقات." } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilmiş tətbiq ikonu və adı, əsas ekranda və tətbiq siyirməsində nümayiş olunur." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28622,24 +28800,60 @@ "value" : "Na domovské obrazovce a v seznamu aplikací se zobrazí vybraná ikona a název aplikace." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das ausgewählte App-Symbol und der Name werden auf dem Startbildschirm und in der App-Übersicht angezeigt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "The selected app icon and name is displayed on the home screen and app drawer." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El icono y el nombre seleccionados de la aplicación se muestran en la pantalla de inicio y en el cajón de aplicaciones." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El icono y el nombre seleccionados de la aplicación se muestran en la pantalla de inicio y en el cajón de aplicaciones." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'icône et le nom de l'application sélectionnés sont affichés sur l'écran d'accueil et dans le tiroir d'applications" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "चयनित ऐप आइकन और नाम होम स्क्रीन और ऐप ड्रॉअर में प्रदर्शित होता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "A kiválasztott alkalmazás ikonja és neve megjelenik a kezdőképernyőn és az alkalmazásfiókban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona e il nome dell'app selezionati vengono mostrati nella schermata principale e nel drawer delle app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択したアプリアイコンと名前は、ホーム画面とアプリドロワーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28658,11 +28872,53 @@ "value" : "Wybrana ikona i nazwa aplikacji będą wyświetlane na ekranie głównym oraz w szufladzie aplikacji." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone e o nome da aplicação selecionados são apresentados no ecrã principal e na gaveta de aplicações." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma și numele aplicației selectate sunt afișate pe ecranul principal și în sertarul de aplicații." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выбранный значок и название приложения отображаются на главном экране и в панели приложений" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vald appikon och namn visas på hemskärmen och i applådan." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilen uygulama simgesi ve adı, ana ekranda ve uygulama çekmecesinde görüntülenir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Альтернативний значок та назва використовуються на домашньому екрані та у переліку застосунків." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "所选应用图标与名称将显示在主屏幕和应用抽屉中。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "所選擇的應用程式圖示與名稱將顯示於主畫面與應用程式清單中。" + } } } }, @@ -28717,6 +28973,18 @@ "value" : "Piktogramo kaj nomo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono y nombre" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono y nombre" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28741,6 +29009,18 @@ "value" : "Ikon dan nama" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona e nome" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコンと名前" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28759,6 +29039,18 @@ "value" : "Ikona i nazwa" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone e nome" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă și nume" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28771,6 +29063,12 @@ "value" : "Ikon och namn" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İkon ve isim" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28788,6 +29086,12 @@ "state" : "translated", "value" : "图标与应用名" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示與名稱" + } } } }, @@ -28830,6 +29134,18 @@ "value" : "Alternate app icon is displayed on home screen and app library. App name will still appear as '{app_name}'." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono alternativo de la app se muestra en la pantalla principal y en la biblioteca de apps. El nombre de la app seguirá apareciendo como \"{app_name}\"." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono alternativo de la app se muestra en la pantalla principal y en la biblioteca de apps. El nombre de la app seguirá apareciendo como \"{app_name}\"." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28848,6 +29164,18 @@ "value" : "Az alternatív alkalmazásikon megjelenik a kezdőképernyőn és az alkalmazáskönyvtárban. Az alkalmazás neve továbbra is „{app_name}” néven jelenik meg." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona alternativa dell'app è visualizzata nella schermata principale e nella libreria delle app. Il nome dell'app sarà comunque visualizzato come \"{app_name}\"." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンはホーム画面およびアプリライブラリに表示されます。アプリ名は「{app_name}」として表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28866,6 +29194,18 @@ "value" : "Alternatywna ikona aplikacji jest wyświetlana na ekranie głównym i w bibliotece aplikacji. Nazwa aplikacji będzie nadal wyświetlana jako „{app_name}”." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone alternativo da aplicação é exibido no ecrã principal e na biblioteca de aplicações. O nome da aplicação continuará a aparecer como \"{app_name}\"." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma alternativă a aplicației este afișată pe ecranul principal și în biblioteca de aplicații. Numele aplicației va apărea în continuare ca „{app_name}”." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28878,6 +29218,12 @@ "value" : "Alternativ app-ikon visas på hem skärmen och app biblioteket. App namn kommer fortsätta visas som '{app_name}'." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama simgesi, ana ekranda ve uygulama arşivinde görüntülenir. Uygulama adı yine {app_name} olarak görünmeye devam edecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28889,6 +29235,12 @@ "state" : "translated", "value" : "替代的应用图标会显示在主页和应用列表中。应用名仍会显示为“{app_name}”。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "替代的應用程式圖示會顯示於主畫面與應用程式資料庫中。應用程式名稱仍會顯示為「{app_name}」。" + } } } }, @@ -28943,6 +29295,18 @@ "value" : "Uzi alternativan piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono alternativo de la app" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono alternativo de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28967,6 +29331,18 @@ "value" : "Gunakan ikon aplikasi alternatif" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usa un'icona alternativa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替アプリアイコンを使用" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28985,6 +29361,18 @@ "value" : "Użyj alternatywnej ikony aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícone alternativo da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosește pictograma alternativă pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28997,6 +29385,12 @@ "value" : "Använda alternativ app-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu kullan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29014,6 +29408,12 @@ "state" : "translated", "value" : "使用替代的应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用替代的應用程式圖示" + } } } }, @@ -29068,6 +29468,18 @@ "value" : "Uzi alternativan piktogramon de aplikaĵo kaj nomon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono y nombre alternativos de la app" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono y nombre alternativos de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29092,6 +29504,18 @@ "value" : "Gunakan ikon aplikasi alternatif dan nama" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usa icona e nome alternativi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンと名前を使用" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29110,6 +29534,18 @@ "value" : "Użyj alternatywnej ikony i nazwy aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícone e nome alternativos da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosește pictograma și numele alternative pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29122,6 +29558,12 @@ "value" : "Använd alternativ app-ikon och namn" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu ve ismi kullan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29139,6 +29581,12 @@ "state" : "translated", "value" : "使用替代的应用图标与应用名" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用替代的應用程式圖示與名稱" + } } } }, @@ -29193,6 +29641,18 @@ "value" : "Elektu alternativan piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar ícono alternativo de la app" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar ícono alternativo de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29217,6 +29677,18 @@ "value" : "Pilih ikon aplikasi alternatif" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona un'icona alternativa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替アプリアイコンを選択" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29235,6 +29707,18 @@ "value" : "Wybierz alternatywną ikonę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar ícone alternativo da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selectează o pictogramă alternativă pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29247,6 +29731,12 @@ "value" : "Välj en alternativ app-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu seç" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29264,6 +29754,12 @@ "state" : "translated", "value" : "选择替代的应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇替代的應用程式圖示" + } } } }, @@ -29318,6 +29814,18 @@ "value" : "Piktogramo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29342,6 +29850,18 @@ "value" : "Ikon" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコン" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29360,6 +29880,18 @@ "value" : "Ikona" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29372,6 +29904,12 @@ "value" : "Ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İkon" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29389,6 +29927,12 @@ "state" : "translated", "value" : "图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示" + } } } }, @@ -29443,6 +29987,18 @@ "value" : "Kalkulilo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29467,6 +30023,18 @@ "value" : "Kalkulator" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calcolatrice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電卓" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29485,6 +30053,18 @@ "value" : "Kalkulator" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculator" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29497,6 +30077,12 @@ "value" : "Miniräknare" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesap makinesi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29514,6 +30100,12 @@ "state" : "translated", "value" : "计算器" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "計算機" + } } } }, @@ -29562,6 +30154,18 @@ "value" : "KunvenoSE" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29580,6 +30184,18 @@ "value" : "Találkozók" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29598,6 +30214,18 @@ "value" : "Spotkania" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ReuniãoSE" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29610,6 +30238,12 @@ "value" : "MötenSE" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29621,6 +30255,12 @@ "state" : "translated", "value" : "SE云会议" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } } } }, @@ -29675,6 +30315,18 @@ "value" : "Novaĵoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noticias" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noticias" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29699,6 +30351,18 @@ "value" : "Berita" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notizie" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニュース" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29717,6 +30381,18 @@ "value" : "Aktualności" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notícias" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Știri" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29729,6 +30405,12 @@ "value" : "Nyheter" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haberler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29746,6 +30428,12 @@ "state" : "translated", "value" : "新闻" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "新聞" + } } } }, @@ -29800,6 +30488,18 @@ "value" : "Notoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29824,6 +30524,18 @@ "value" : "Catatan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Note" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メモ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29842,6 +30554,18 @@ "value" : "Notatki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Note" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29854,6 +30578,12 @@ "value" : "Anteckningar" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notlar" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29871,6 +30601,12 @@ "state" : "translated", "value" : "笔记" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆記" + } } } }, @@ -29925,6 +30661,18 @@ "value" : "Akcioj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acciones" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acciones" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29949,6 +30697,18 @@ "value" : "Bursa Saham" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borsa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "株式" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29967,6 +30727,18 @@ "value" : "Akcje" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ações" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acțiuni" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29979,6 +30751,12 @@ "value" : "Aktier" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borsa" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29996,6 +30774,12 @@ "state" : "translated", "value" : "股票" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "股票" + } } } }, @@ -30050,6 +30834,18 @@ "value" : "Vetero" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clima" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clima" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30074,6 +30870,18 @@ "value" : "Cuaca" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meteo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "天気" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -30092,6 +30900,18 @@ "value" : "Pogoda" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meteorologia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vremea" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -30104,6 +30924,12 @@ "value" : "Väder" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hava Durumu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -30121,6 +30947,12 @@ "state" : "translated", "value" : "天气" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "天氣" + } } } }, @@ -30636,6 +31468,12 @@ "value" : "Vedhæftninger" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anhänge" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -30648,12 +31486,30 @@ "value" : "Alfiksitaĵoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos adjuntos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos adjuntos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pièces jointes" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अटैचमेंट्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -30666,6 +31522,18 @@ "value" : "Lampiran" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allegati" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "添付ファイル" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -30684,6 +31552,36 @@ "value" : "Załączniki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anexos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atașamente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilagor" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -30695,6 +31593,12 @@ "state" : "translated", "value" : "附件" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件" + } } } }, @@ -32201,7 +33105,7 @@ "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Automaticky stahovat média a soubory z tohoto chatu." + "value" : "Automaticky stahovat média a soubory této konverzace." } }, "cy" : { @@ -44212,7 +45116,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Metadata, fayldan silinə bilmir." + "value" : "Meta veri, fayldan silinə bilmir." } }, "bal" : { @@ -52840,7 +53744,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Saxladığınız qoşmalara cihazınızdakı digər tətbiqlər müraciət edə bilər." + "value" : "Saxladığınız qoşmalara cihazınızdakı digər tətbiqlər erişə bilər." } }, "bal" : { @@ -56690,7 +57594,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kimlik doğrulamaya müraciət edilə bilmədi." + "value" : "Kimlik doğrulamaya erişilə bilmədi." } }, "bal" : { @@ -60082,6 +60986,12 @@ "value" : "Indtast konto-ID'et for den bruger, du vil fjerne blokering for" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du entsperrst" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -60094,18 +61004,48 @@ "value" : "Enigu ID de la konto de la uzanto, kiun vi malblokas" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a quitar la prohibición." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a quitar la prohibición." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l’utilisateur que vous souhaitez débannir" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप अनबैन कर रहे हैं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg annak a felhasználónak a fiókazonosítóját, amelyiknek a kitiltását fel akarja oldani" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente a cui desideri revocare il blocco" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロック解除するユーザーのAccount IDを入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -60124,6 +61064,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, który odbanowałeś" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que pretende desbloquear" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce ID-ul contului utilizatorului căruia îi ridici interdicția" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы разблокируете" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du tar bort blockeringen för" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yasağını kaldırdığınız kullanıcının Hesap Kimliğini girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -60135,6 +61105,12 @@ "state" : "translated", "value" : "输入您想取消封禁的用户的帐户 ID" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要解除封鎖的使用者的 Account ID" + } } } }, @@ -61608,6 +62584,12 @@ "value" : "Indtast konto-ID'et for den bruger, du vil blokere" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du sperrst" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -61620,18 +62602,48 @@ "value" : "Enigu ID de la konto de la uzanto, kiun vi blokas" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a prohibir." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a prohibir." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l'utilisateur que vous souhaitez débannir" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप बैन कर रहे हैं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg annak a felhasználónak a fiókazonosítóját, amelyiket ki akarja tiltani" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente che desideri bloccare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロックするユーザーのAccount IDを入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -61650,6 +62662,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, którego chcesz zablokować" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que pretende bloquear" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce ID-ul contului utilizatorului pe care îl blochezi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы блокируете" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du blockerar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yasakladığınız kullanıcının Hesap Kimliğini girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -61661,17 +62703,143 @@ "state" : "translated", "value" : "输入您想封禁的用户的帐户 ID" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要封鎖的使用者的 Account ID" + } } } }, "blindedId" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kor ID" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cega" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskované ID" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschleierte ID" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Blinded ID" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cegado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cegado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID aveuglé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ब्लाइंडेड ID" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID offuscato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラインドID" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afgeschermde ID" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukryty identyfikator" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID Oculto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cenzurat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрытый ID" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskerat ID" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Körleştirilmiş Kimlik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знеособлений ID" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "盲化 ID" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏 ID" + } } } }, @@ -62163,6 +63331,24 @@ "blockBlockedDescription" : { "extractionState" : "manual", "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokkeer hierdie kontak om 'n boodskap te stuur." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إلغاء حظر جهة الإتصال لإرسال رسالة" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj göndərmək üçün bu kontaktı əngəldən çıxardın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -62175,23 +63361,209 @@ "value" : "Pro odeslání zprávy tento kontakt odblokujte." } }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dadrwystro'r cyswllt hwn i anfon neges" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Blockierung dieses Kontakts frei, um eine Nachricht zu senden." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Καταργήστε τη φραγή αυτής τη επαφής για να στείλετε ένα μήνυμα" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unblock this contact to send a message" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviarle mensajes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviar mensajes." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lähettääksesi viestin tälle yhteystiedolle sinun tulee ensin poistaa asettamasi esto." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débloquez ce contact pour envoyer un message" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई संदेश भेजने के लिए इस संपर्क को अनवरोधित करें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üzenet küldéséhez oldd fel a kontakt letiltását." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lepaskan blokir kontak ini untuk mengirim pesan." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per inviare un messaggio sblocca questo contatto." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この連絡先にメッセージを送るためにブロックを解除する" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "შეტყობინების გაგზავნისთვის ბლოკი მოხსენით" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 사용자에게 메시지를 보내려면 먼저 차단을 해제하세요" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیوەندە لابردن بۆ بریتیە لە ناردنی پەیامێک." + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیوەندە لابردن بۆ بریتیە لە ناردنی پەیامێک." + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Түгжээг арилгаж, мессеж илгээх боломжтой болно" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyahsekat kontak ini untuk menghantar mesej" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avblokker denne kontakten for å sende en beskjed." + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokkeer dit contact om een bericht te verzenden." + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odblokuj ten kontakt, aby wysłać wiadomość" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquear este contato para enviar uma mensagem" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloqueie este contacto para enviar uma mensagem." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblochează acest contact pentru a putea trimite mesaje" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблокируйте этот контакт, чтобы отправить сообщение" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avblockera denna kontakt för att skicka meddelanden." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "ஒரு செய்தியை அனுப்ப இந்த தொடர்பை விடுவிக்கவும்." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleti göndermek için bu kişinin engellenmesini kaldırın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Розблокувати контакт для надсилання повідомлення." + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", "value" : "取消屏蔽此联系人以发送消息" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "解除封鎖聯絡人以傳送訊息。" + } } } }, @@ -63644,6 +65016,17 @@ } } }, + "blockedContactsmanageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View and manage blocked contacts." + } + } + } + }, "blockUnblock" : { "extractionState" : "manual", "localizations" : { @@ -71344,7 +72727,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofon müraciətinə icazə vermədiyiniz üçün {name} edən zəngi buraxdınız." + "value" : "Mikrofon erişiminə icazə vermədiyiniz üçün {name} edən zəngi buraxdınız." } }, "ca" : { @@ -74119,6 +75502,18 @@ "value" : "Uprawnienie „Połączenia głosowe i wideo” można włączyć w Ustawieniach uprawnień." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pode ativar a permissão de \"Chamadas de voz e vídeo\" nas Definições de Permissões." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puteți activa permisiunea „Apeluri Vocale și Video” în Setările de Confidențialitate." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -74154,6 +75549,12 @@ "state" : "translated", "value" : "您可以在权限设置中启用 “语音和视频通话 ”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以在隱私權設定中啟用「語音和視訊通話」權限。" + } } } }, @@ -79923,7 +81324,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kamera müraciətinə icazə ver" + "value" : "Kamera erişiminə icazə ver" } }, "bal" : { @@ -80402,7 +81803,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} foto və video çəkmək üçün kameraya müraciət etməlidir, ancaq bu icazəyə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedib \"İcazələr\"i seçin və \"Kamera\"nı fəallaşdırın." + "value" : "{app_name} foto və video çəkmək üçün kameraya erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedib \"İcazələr\"i seçin və \"Kamera\"nı fəallaşdırın." } }, "bal" : { @@ -82324,6 +83725,28 @@ } } }, + "change" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change" + } + } + } + }, + "changePasswordDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." + } + } + } + }, "changePasswordFail" : { "extractionState" : "manual", "localizations" : { @@ -83791,7 +85214,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bütün datanı təmizlə" + "value" : "Bütün veriləri təmizlə" } }, "bal" : { @@ -84276,7 +85699,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu, mesajlarınızı və kontaktlarınızı həmişəlik siləcək. Yalnız bu cihazı təmizləmək istəyirsiniz, yoxsa datanızı bütün şəbəkədən də silmək istəyirsiniz?" + "value" : "Bu, mesajlarınızı və kontaktlarınızı həmişəlik siləcək. Yalnız bu cihazı təmizləmək istəyirsiniz, yoxsa verilərinizi bütün şəbəkədən də silmək istəyirsiniz?" } }, "bal" : { @@ -84755,7 +86178,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Data silinmədi" + "value" : "Verilər silinmədi" } }, "bal" : { @@ -85291,13 +86714,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Data, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." + "value" : "Veri, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Data, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." + "value" : "Veri, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." } } } @@ -86805,7 +88228,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bilinməyən bir xəta baş verdi və datanız silinmədi. Bunun əvəzinə datanızı yalnız bu cihazdan silmək istəyirsiniz?" + "value" : "Bilinməyən bir xəta baş verdi və veriləriniz silinmədi. Bunun əvəzinə verilərinizi yalnız bu cihazdan silmək istəyirsiniz?" } }, "bal" : { @@ -88260,7 +89683,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Datanızı şəbəkədən silmək istədiyinizə əminsiniz? Davam etsəniz, mesajlarınızı və kontaktlarınızı bərpa edə bilməyəcəksiniz." + "value" : "Verilərinizi şəbəkədən silmək istədiyinizə əminsiniz? Davam etsəniz, mesajlarınızı və kontaktlarınızı bərpa edə bilməyəcəksiniz." } }, "bal" : { @@ -89724,18 +91147,42 @@ "value" : "Slet enhed og genstart" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät löschen und neu starten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Clear Device and Restart" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y reiniciar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y reiniciar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer l’appareil et redémarrer" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिवाइस साफ़ करें और पुनः आरंभ करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -89748,6 +91195,18 @@ "value" : "Hapus Perangkat dan Hidupkan Ulang" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella dispositivo e riavvia" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "端末の消去と再起動" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -89766,6 +91225,36 @@ "value" : "Wyczyść urządzenie i uruchom ponownie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar Dispositivo e Reiniciar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță dispozitivul și repornește" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить устройство и перезапустить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa enhet och starta om" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazı temizle ve Yeniden başlat" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -89777,6 +91266,18 @@ "state" : "translated", "value" : "Xóa thiết bị và khởi động lại" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除设备并重新启动应用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除裝置並重新啟動" + } } } }, @@ -89807,18 +91308,42 @@ "value" : "Slet enhed og gendan" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät löschen und wiederherstellen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Clear Device and Restore" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y restaurar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y restaurar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer l’appareil et restaurer" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिवाइस साफ़ करें और पुनः प्राप्त करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -89831,6 +91356,18 @@ "value" : "Hapus Perangkat dan Pulihkan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella dispositivo e ripristina" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "端末の消去と復元" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -89849,6 +91386,36 @@ "value" : "Wyczyść urządzenie i przywróć" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar Dispositivo e Restaurar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță dispozitivul și restaurează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить устройство и восстановить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa enhet och återställ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazı temizle ve geri yükle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -89860,6 +91427,18 @@ "state" : "translated", "value" : "Xóa thiết bị và khôi phục" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除设备并恢复" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除裝置並還原" + } } } }, @@ -90860,24 +92439,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra din samtale med {name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von deinem Gespräch mit {name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from your conversation with {name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de tu conversación con {name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de tu conversación con {name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de votre conversation avec {name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {name} के साथ बातचीत से सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes üzenetet a(z) {name} nevű partnerével való beszélgetésből ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi della tua chat con {name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{name}との会話のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -90896,11 +92511,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z konwersacji z {name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens da sua conversa com {name} neste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele din conversația ta cu {name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из вашего чата с {name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från din konversation med {name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} ile olan sohbetinizdeki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з вашої розмови з {name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上与 {name} 的所有对话消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除此裝置上與 {name} 的所有對話訊息嗎?" + } } } }, @@ -91416,24 +93073,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {community_name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {community_name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {community_name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {community_name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {community_name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {community_name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {community_name} के सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {community_name} összes üzenetét ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {community_name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{community_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -91452,11 +93145,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {community_name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {community_name} deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele de la {community_name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {community_name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {community_name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{community_name} topluluğundaki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення від {community_name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {community_name}的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除本裝置上來自 {community_name} 的所有訊息嗎?" + } } } }, @@ -92942,24 +94677,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {group_name}?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {group_name} löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {group_name}?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name}?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {group_name} ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई {group_name} से सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {group_name} összes üzenetét?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {group_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -92978,11 +94749,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {group_name}?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {group_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să ștergi toate mesajele de la {group_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {group_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {group_name}?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} grubundaki tüm mesajları temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з {group_name}?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {group_name} 的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除 {group_name} 中的所有訊息嗎?" + } } } }, @@ -93498,24 +95311,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {group_name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {group_name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {group_name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {group_name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {group_name} के सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {group_name} összes üzenetét ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {group_name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{group_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -93534,11 +95383,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {group_name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {group_name} deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele de la {group_name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {group_name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {group_name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} grubundaki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з {group_name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {group_name} 的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除本裝置上來自 {group_name} 的所有訊息嗎?" + } } } }, @@ -94054,24 +95945,60 @@ "value" : "Er du sikker på, at du vil slette alle Egen note-beskeder fra din enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten in »Notiz an mich« auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all Note to Self messages on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar todos los mensajes de Nota Personal en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar todos los mensajes de Nota Personal en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages Note pour soi-même sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से सभी अपने लिए नोट संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes Jegyzet magamnak üzenetet ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di Note to Self da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の自分用メモのすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94090,11 +96017,53 @@ "value" : "Czy na pewno chcesz usunąć z urządzenia wszystkie Moje notatki?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens da Nota Pessoal deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur că vrei să ștergi toate mesajele Notă personală de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из Заметки для себя на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden i Notera till mig själv på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazdaki tüm Kendime Not mesajlarını temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення в Нотатці для себе на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上所有 Note to Self 消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除此裝置上的所有 小筆記訊息嗎?" + } } } }, @@ -94125,6 +96094,12 @@ "value" : "Slet på denne enhed" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auf diesem Gerät löschen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -94137,12 +96112,30 @@ "value" : "Forigi sur ĉi tiu aparato" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar en este dispositivo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar en este dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer sur cet appareil" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस डिवाइस पर साफ़ करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -94155,6 +96148,18 @@ "value" : "Hapus dalam perangkat ini" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella da questo dispositivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上で削除" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94173,17 +96178,53 @@ "value" : "Wyczyść na tym urządzeniu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar neste dispositivo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță pe acest dispozitiv" + } + }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить на этом устройстве" } }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa på denna enhet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazı temizle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити на цьому пристрої" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在此设备上清除" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在此裝置上清除" + } } } }, @@ -94681,6 +96722,12 @@ "value" : "إغلاق التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiqi bağla" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -94693,6 +96740,12 @@ "value" : "Zavřít aplikaci" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App schließen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -94705,12 +96758,30 @@ "value" : "Fermi aplikaĵon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप बंद करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -94723,6 +96794,18 @@ "value" : "Tutup Aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリを終了" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94741,11 +96824,53 @@ "value" : "Zamknij aplikację" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar Aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Închide aplicația" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть приложение" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stäng appen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulamayı kapat" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити застосунок" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭应用程序" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉應用程式" + } } } }, @@ -96635,6 +98760,119 @@ } } }, + "communityDescriptionEnter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma aaçıqlamasını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu una descripció de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte popis komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Beschreibung eingeben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter a community description" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce una descripción de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce una descripción de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez une description de la communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एक सामुदायिक विवरण दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティの説明を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitybeschrijving in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź opis społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digite uma descrição da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu o descriere a comunității" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange en communitybeskrivning" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入社群描述" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入社群描述" + } + } + } + }, "communityEnterUrl" : { "extractionState" : "manual", "localizations" : { @@ -103365,6 +105603,232 @@ } } }, + "communityNameEnter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma adını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Namen eingeben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter a community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce un nombre de comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce un nombre de comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez un nom de communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एक सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digite o nome da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu numele comunității" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange ett communitynamn" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入社群名稱" + } + } + } + }, + "communityNameEnterPlease" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, icma adını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosím zadejte název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib einen Community-Namen ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer un nom de communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proszę wprowadzić nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira um nome de Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci un nume al comunității" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange ett communitynamn" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入社群名稱" + } + } + } + }, "communityUnknown" : { "extractionState" : "manual", "localizations" : { @@ -111077,6 +113541,17 @@ } } }, + "contentNotificationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose the content displayed in local notifications when an incoming message is received." + } + } + } + }, "conversationsAddedToHome" : { "extractionState" : "manual", "localizations" : { @@ -117373,478 +119848,10 @@ "conversationsEnterNewLine" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER stuur 'n boodskap, ENTER begin 'n nuwe lyn" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER يرسل الرسالة، ENTER يبدأ سطرًا جديدًا" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER mesajı göndərir, ENTER yeni sətrə keçir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER پیام روانگی، ENTER نوکی خط شروع کـــــــن" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER адпраўляе паведамленне, ENTER пачынае новы радок" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER изпраща съобщение, ENTER започва нов ред" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER একটি বার্তা পাঠাবে, ENTER একটি নতুন লাইন শুরু করবে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "MAJÚSCULA + ENTRA envia un missatge, ENTRA comença una línia nova" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER odešle zprávu, ENTER začne nový řádek" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER yn anfon neges, ENTER yn dechrau llinell newydd" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sender en besked, ENTER starter en ny linje" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + Eingabe sendet eine Nachricht, Eingabe startet eine neue Zeile" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER στέλνει ένα μήνυμα, ENTER ξεκινά μια νέα γραμμή" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sendas mesaĝon, ENTER komencas novan linion" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER envía un mensaje, ENTER inicia una nueva línea" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER envía un mensaje, ENTER inicia una nueva línea" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER saadab sõnumi, ENTER alustab uut rida" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER mezu bat bidaltzen du, ENTER lerro berri bat hasten" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ارسال پیام، ENTER خط جدید آغاز می‌کند" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER lähettää viestin, ENTER aloittaa uuden rivin" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ang nagpapadala ng mensahe, ENTER ang nagsisimula ng bagong linya" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "MAJUSCULE + ENTRÉE envoie un message, ENTRÉE commence une nouvelle ligne" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER envía unha mensaxe, ENTER inicia unha nova liña" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER yana aikawa da saƙo, ENTER yana fara sabon layi" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER שולח הודעה, ENTER מתחיל שורה חדשה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER एक संदेश भेजता है, ENTER एक नई पंक्ति शुरू करता है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi redak" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER elküldi az üzenetet, ENTER új sort kezd" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER-ը ուղարկում է հաղորդագրություն, ENTER-ը սկսում է նոր տող" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER mengirim pesan, ENTER mulai baris baru" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + INVIO invia un messaggio, INVIO inizia una nuova riga" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "シフト + エンター 送信、エンター 改行" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER აგზავნის შეტყობინებას, ENTER იწყებს ახალ სტრიქონს" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ផ្ញើសារ ENTER ចាប់ផ្តើមបន្ទាត់ថ្មី" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ಒಂದು ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುತ್ತದೆ, ENTER ಹೊಸ ಎರಡು ಸಾಲು ಪ್ರಾರಂಭಿಸುತ್ತದೆ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER 메시지 전송, ENTER 새 줄 시작" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER (وێنەی هاوبەش بە ناوی کانفرم ، ENTER (نوێ دەستەکی." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "BIKAR BIKI + ENTER peyamekê dişîne, ENTER xeta nû dest pê dibej." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER esoose obubaka, ENTER entandiika olumuli" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER siunčia žinutę, ENTER pradeda naują eilutę" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER nosūta ziņu, ENTER sāk jaunu rindu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER испраќа порака, ENTER започнува нов ред" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER мессеж илгээнэ, ENTER шинэ мөр эхлүүлнэ" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER untuk menghantar mesej, ENTER untuk memulakan baris baru" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER သည် စာ မက်ဆေ့ချ်ပေးပါ။ ENTER သည် စာကြောင်းအသစ်စ လား။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sender en melding, ENTER starter en ny linje" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sender melding, ENTER starter en ny linje" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER सन्देश पठाउँछ, ENTER नयाँ लाइन सुरु गर्छ" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER verzendt een bericht, ENTER begint een nieuwe regel" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sender ei melding, ENTER startar ei ny linje" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ਇੱਕ ਸੁਨੇਹਾ ਭੇਜਦਾ ਹੈ, ENTER ਇੱਕ ਨਵੀ ਲਾਈਨ ਸ਼ੁਰੂ ਕਰਦਾ ਹੈ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER wysyła wiadomość, ENTER zaczyna nową linijkę" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER پیغام لیږي، ENTER نوی کرښه پیلوي" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER envia uma mensagem, ENTER inicia uma nova linha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER envia uma mensagem, ENTER começa uma nova linha" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER trimite un mesaj, ENTER începe o linie nouă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER отправляет сообщение, ENTER начинает новую строку" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi red" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER පණිවිඩය යවයි, ENTER නව මූලය පටන් ගනී" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER odošle správu, ENTER začne nový riadok" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER pošlje sporočilo, ENTER začne novo vrstico" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER dërgon një mesazh, ENTER fillon një rresht të ri" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER шаље поруку, ENTER почиње нови ред" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi red" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER skickar ett meddelande, ENTER startar en ny rad" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER kutuma ujumbe, ENTER kuanza mstari mpya" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER செய்தி அனுப்பும், ENTER புதிய வரியைத் தொடங்குகிறது" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER సందేశం పంపుతుంది, ENTER కొత్త పంక్తిని ప్రారంభిస్తుంది" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ส่งข้อความ, ENTER เริ่มบรรทัดใหม่" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER ileti gönderir, ENTER yeni bir satır başlatır" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER надсилає повідомлення, ENTER починає новий рядок" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "شفٹ + انٹر پیغام بھیجتا ہے، انٹر نئی لائن شروع کرتا ہے" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER xabarni yuboradi, ENTER yangi qatordan boshlaydi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER gửi tin nhắn, ENTER bắt đầu dòng mới" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT+回车键发送消息,回车键换行" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "SHIFT + ENTER 傳送訊息, ENTER 起新行" + "value" : "SHIFT + ENTER sends a message, ENTER starts a new line." } } } @@ -117852,484 +119859,10 @@ "conversationsEnterSends" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER stuur 'n boodskap, SHIFT + ENTER begin 'n nuwe lyn" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إدخال يرسل رسالة، SHIFT + ENTER يبدأ سطر جديد" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER mesajı göndərir, SHIFT + ENTER yeni sətrə keçir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ایک پیغام بھیجتا ہے، SHIFT + ENTER ایک نیا لائن شروع کرتا ہے" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER адпраўляе паведамленне, SHIFT + ENTER пачынае новы радок" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER изпраща съобщение, SHIFT + ENTER започва нов ред" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER একটি বার্তা পাঠাবে, SHIFT + ENTER একটি নতুন লাইন শুরু করে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envia un missatge, SHIFT + ENTER comença una nova línia" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zmáčknutím ENTER se zpráva odešle, SHIFT + ENTER vytvoří nový řádek" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER yn anfon neges, SHIFT + ENTER yn dechrau llinell newydd" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sender en besked, SHIFT + ENTER starter ny linje" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Eingabetaste sendet eine Nachricht, Shift-Taste + Eingabetaste startet eine neue Zeile" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER στέλνει ένα μήνυμα, SHIFT + ENTER ξεκινάει μια νέα γραμμή" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sends a message, SHIFT + ENTER starts a new line" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sendas mesaĝon, SHIFT + ENTER startas novan linion" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envía un mensaje, SHIFT + ENTER empieza una nueva línea" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER saadab sõnumi, SHIFT + ENTER alustab uut rida" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "SARRERA mezu bat bidaltzen du, SHIFT + SARRERA lerro berri bat hasten du" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "کلید Enter پیام را ارسال می‌کند، SHIFT + ENTER یک خط جدید شروع می‌کند" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER lähettää viestin, SHIFT + ENTER aloittaa uuden rivin" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ay nagpapadala ng mensahe, SHIFT + ENTER ay nag-uumpisa ng bagong linya" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTRÉE envoie un message, MAJ + ENTRÉE commence une nouvelle ligne" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envía unha mensaxe, SHIFT + ENTER comeza unha nova liña" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "MENU tana aika saƙo, SHIFT + MENU yana farawa da layi na sabo" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER שולח הודעה, SHIFT + ENTER מתחיל שורה חדשה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER संदेश भेजता है, SHIFT + ENTER नई लाइन शुरू करता है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER elküldi az üzenetet, SHIFT + ENTER új sort kezd" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter ստեղնը հաղորդագրություն է ուղարկում, SHIFT + ENTER-ը նոր տող է սկսում" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER mengirim pesan, SHIFT + ENTER memulai baris baru" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "INVIO invia un messaggio, MAIUSC + INVIO inizia una nuova riga" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "エンターでメッセージを送信、シフト + エンターで改行を開始" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER იღებს შეტყობინებას, SHIFT + ENTER იწყება ახალ ხაზზე" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ផ្ញើសារ, SHIFT + ENTER ចាប់ផ្តើមបន្ទាត់ថ្មី" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುತ್ತದೆ, SHIFT + ENTER ಹೊಸ ಸಾಲನ್ನು ಪ್ರಾರಂಭಿಸುತ್ತದೆ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "엔터(ENTER) 키로 메시지를 보내고, 시프트(Shift) + 엔터(ENTER) 키로 새 줄을 시작합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER نامەیەک نێردە، SHIFT + ENTER هێڵەکی تازە دەستپێبکات" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER peyamê dişîne, SHIFT + ENTER xeteke nû dide destpêkirin" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENU yerekawo, SHIFT + ENU evvumbula olunyiriri olupya" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ສົ່ງຂໍ້ຄວາມ, SHIFT + ENTER ເລີ່ມແຖວໃຫມ່" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER išsiųs žinutę, SHIFT + ENTER pradės naują eilutę" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER nosūta ziņojumu, SHIFT + ENTER sāk jaunu rindu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER испраќа порака, SHIFT + ENTER започнува нов ред" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER мессеж явуулна, SHIFT + ENTER шинэ мөр эхэлнэ" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER menghantar mesej, SHIFT + ENTER memulakan baris baru" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ဆို Message ပို့ပါ၊ SHIFT + ENTER ဆို စာကြောင်းအသစ်စတင်ပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sender en melding, SHIFT + ENTER starter en ny linje" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sender en melding, SHIFT + ENTER starter en ny linje" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER सन्देश पठाउँछ, SHIFT + ENTER नयाँ लाइन सुरु गर्छ" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER verzendt een bericht, SHIFT + ENTER begint een nieuwe regel" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sender ei melding, SHIFT + ENTER startar ei ny linje" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER imatumiza uthenga, SHIFT + ENTER imayamba mzere watsopano" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ਸੰਦੇਸ਼ ਭੇਜਦਾ ਹੈ, SHIFT + ENTER ਇੱਕ ਨਵੀਂ ਲਾਈਨ ਤੋਂ ਸ਼ੁਰੂ ਹੁੰਦਾ ਹੈ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER wysyła wiadomość, SHIFT + ENTER rozpoczyna nową linijkę" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER پیغام لیږي، SHIFT + ENTER نوی کرښه پیلوي" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envia uma mensagem, SHIFT + ENTER inicia uma nova linha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER envia a mensagem, SHIFT + ENTER inicia uma nova linha" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER trimite un mesaj, SHIFT + ENTER începe o linie nouă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER для отправки, SHIFT + ENTER для новой строки" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER පණිවිඩයක් යොමු කරයි, SHIFT + ENTER නව නිමාවක් ආරම්භ කරයි" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER odosiela správu, SHIFT + ENTER začína nový riadok" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER pošlje sporočilo, SHIFT + ENTER začne novo vrstico" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER dërgon një mesazh, SHIFT + ENTER nis një rresht të ri" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER шаље поруку, SHIFT + ENTER започиње нови ред" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER sänder ett meddelande, SHIFT + ENTER påbörjar en ny rad" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER hutuma ujumbe, SHIFT + ENTER inaanza mstari mpya" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER செய்தியை அனுப்பும், SHIFT + ENTER புதிய வரியை ஆரம்பிக்கும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER సందేశం పంపుతుంది, SHIFT + ENTER కొత్త పంక్తిని ప్రారంభిస్తుంది" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ส่งข้อความ, SHIFT + ENTER เริ่มบรรทัดใหม่" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER iletiyi gönderir, SHIFT + ENTER yeni satır başlatır" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER надсилає повідомлення, SHIFT + ENTER починає новий рядок" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER پیغام بھیجتا ہے، SHIFT + ENTER نئی لائن شروع کرتا ہے" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER xabarni yuboradi, SHIFT + ENTER yangi qatordan boshlaydi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter gửi tin nhắn, SHIFT + ENTER bắt đầu một dòng mới" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ithumela umyalezo, SHIFT + ENTER uqala ulayini omtsha" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "回车键发送消息,SHIFT+回车键换行" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER 發送訊息,SHIFT + ENTER 創建新行" + "value" : "ENTER sends a message, SHIFT + ENTER starts a new line." } } } @@ -119774,484 +121307,10 @@ "conversationsMessageTrimmingTrimCommunitiesDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skrap boodskappe van Gemeenskap geselsies ouer as 6 maande, en waar daar meer as 2,000 boodskappe is." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسائل القديمة من محادثات المجتمع التي تكون أقدم من 6 أشهر، وحيث يوجد أكثر من 2000 رسالة." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "6 aydan köhnə və 2,000-dən çox mesajın olduğu İcma danışıqlarındakı mesajları sil." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "کمیونٹی کی بات چیت میں 6 ماہ سے زیادہ پرانے اور 2000 سے زیادہ پیغامات حذف کریں۔" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выдаліць паведамленні з гутаркоў супольнасці, старыя за 6 месяцаў, і калі там больш за 2,000 паведамленняў." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изтрийте съобщения от Community разговори по-стари от 6 месеца, и където има над 2000 съобщения." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "কমিউনিটি কথোপকথনের ৬ মাসের বেশি পুরনো এবং যেখানে ২,০০০ এর বেশি মেসেজ আছে তা মুছুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suprimeix missatges de converses de la Community més antics de 6 mesos i quan hi ha més de 2,000 missatges." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Z komunit vymazat zprávy starší než 6 měsíců a ponechat maximálně 2000 nejnovějších zpráv v každé komunitě." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dileu negeseuon o sgyrsiau Cymuned hŷn na 6 mis, a lle mae dros 2,000 o negeseuon." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slet beskeder fra Community-samtaler der er ældre end 6 måneder, og hvor der er over 2.000 beskeder." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lösche Nachrichten von Community-Konversationen, die älter als 6 Monate sind, und wo es über 2.000 Nachrichten gibt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Διαγραφή μηνυμάτων από Community συνομιλίες άνω των 6 μηνών και όπου υπάρχουν πάνω από 2,000 μηνύματα." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forigi mesaĝojn de Komunumo konversacioj pli malnovajn ol 6 monatoj, kaj kie estas pli ol 2,000 mesaĝoj." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensajes de conversaciones de Comunidad mayores a 6 meses, y donde hay más de 2,000 mensajes." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensajes de conversaciones de Comunidad con más de 6 meses y donde haya más de 2,000 mensajes." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kustutage sõnumid Community vestlustest, mis on vanemad kui 6 kuud ja kus on üle 2 000 sõnumi." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "6 hilabete baino zaharragoak diren eta 2,000 mezu baino gehiago dituzten Community elkarrizketetako mezuak ezabatu." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف پیام از انجمن‌های قدیمی تر از ۶ ماه و انجمن‌هایی با بیش از 2000 پیام." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poista viestit Community-keskusteluista, jotka ovat yli 6 kuukautta vanhoja ja joissa on yli 2,000 viestiä." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Burahin ang mga mensahe mula sa mga usapan sa Community na mas matagal na sa 6 buwan, at kung saan higit sa 2,000 ang mga mensahe." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Supprimez les messages des Communautés de plus de 6 mois et où il y a plus de 2 000 messages." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensaxes das conversas en Comunidade de máis de 6 meses, e onde hai máis de 2,000 mensaxes." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Goge saƙonni daga tattaunawar Community wanda ya fi watanni 6 da wucewa, kuma inda akwai saƙonni sama da 2,000." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מחק הודעות משיחות קהילה שגילן מעל 6 חודשים, ובהן יש יותר מ-2,000 הודעות." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "6 महीने से अधिक पुराने सामुदायिक वार्तालापों से, और जहां 2,000 से अधिक संदेश हों, संदेशों को हटा दें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izbrišite poruke iz Community razgovora starije od 6 mjeseci i gdje ima više od 2.000 poruka." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Üzenetek törlése a közösségi beszélgetésekből, amelyek régebbiek, mint 6 hónap, és ahol több, mint 2.000 üzenet van." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ջնջել Community զրույցների հաղորդագրությունները, որոնք հին են 6 ամիսից ավել և որտեղ առկա է 2,000-ից ավել հաղորդագրություն" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hapus pesan dari percakapan Community yang lebih lama dari 6 bulan, dan di mana ada lebih dari 2.000 pesan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminare i messaggi dalle chat delle Comunità più vecchie di 6 mesi e con oltre 2.000 messaggi." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "6ヶ月以上のコミュニティと2,000以上のメッセージがあるコミュニティからメッセージを削除します。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "წაშალეთ შეტყობინებები Community-ის საუბრებიდან, რომლებიც 6 თვეზე ძველია და 2,000-ზე მეტი შეტყობინებაა." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "លុបសារចេញពីសន្ទនាសហគមន៍ ដែលមាន វ័យចាស់ជាង ៦ ខែ និងមានចំណុចប្រទាក់សារលើសពី 2000" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "6 ತಿಂಗಳಿಗಿಂತ ಹಳೆಮಾದಿ ಮತ್ತು ೨,೦೦೦ ಸಂದೇಶಗಳು ಇರುವ ಸಮುದಾಯ ಸಂಭಾಷಣೆಗಳಿಂದ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "6개월 이상된 커뮤니티 대화의 메시지와 2,000개 이상의 메시지를 삭제합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "سڕینەوەی پەیامەکان لەلایەن گفتوگۆیی کۆمەڵگا ئێستا لە 6 مانگان زووتر، و هەڵەتوان زۆربەی 2,000 پەیام هەبێت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Peyaman ji Civatan piştî 6 mehan an gava ku li wir zêdetirî 2000 peyaman çêbe jê bibe." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Okuggya ebibukiribwa mu Community conversations mapya nga z’emiyezi 6 ez’ensi, y29,000." - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ລຶບຂໍ້ຄວາມຈາກການສົນທະນາຂອງ Community ເກີນຫົກເດືອນ, ແລະມີເກີນສອງພັນຂໍ້ຄວາມ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ištrinti žinutes iš Bendruomenės pokalbių, senesnius nei 6 mėnesiai ir kur yra virš 2 000 žinučių." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dzēst ziņojumus no Kopienas sarunām, kas vecāki par 6 mēnešiem un kur ir vairāk nekā 2 000 ziņojumu." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Избриши пораки од разговори со заедницата постари од 6 месеци и каде што има повеќе од 2,000 пораки." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Community харилцан ярианаас 6 сараас дээш хугацааны, 2,000-аас дээш мессежтэй яриа устгана." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Padamkan mesej dari perbualan Community yang lebih lama dari 6 bulan, dan di mana terdapat lebih dari 2000 mesej." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "Community စကားပြောဆိုမှုများမှ ၆ လကျော်အရွယ်ရှိပြီး မက်ဆေ့ချ် ၂,၀၀၀ ကျော်ရှိသော မက်ဆေ့ချ်များကို ဖျက်ပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slett meldinger fra Community-samtaler eldre enn 6 måneder, og der det er over 2,000 meldinger." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slett meldinger fra Community-samtaler eldre enn 6 måneder og der det er over 2 000 meldinger." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "६ महिना भन्दा पुराना र २,००० भन्दा बढी सन्देशहरू भएका समुदाय कुराकानीहरू मेटाउनुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Community-berichten ouder dan 6 maanden verwijderen en afromen op 2000 berichten." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slett meldingar frå Community-samtalar eldre enn 6 månader, og der det er over 2 000 meldingar." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chotsani mauthenga kuchokera pa zokambirana za Community zosapitilira miyezi 6, ndi pamene pali mauthenga opitilira 2,000." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਕਮਿਊਨਿਟੀ ਗੱਲਬਾਤਾਂ ਤੋਂ 6 ਮਹੀਨੇ ਤੋਂ ਵੱਧ ਪੁਰੇ ਹੋਏ ਅਤੇ 2,000 ਸੰਦੇਸ਼ ਸੰਦੇਸ਼ ਹਟਾਓ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuń wiadomości z konwersacji społecznościowych starszych niż 6 miesięcy i zawierających ponad 2000 wiadomości." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د 6 میاشتو څخه زاړه او 2000 څخه زیات پیغامونه له Community مکالمو پاک کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Excluir mensagens de conversas da Comunidade mais antigas que 6 meses, e onde há mais de 2.000 mensagens." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar mensagens de conversas da Comunidade com mais de 6 meses, e onde há mais de 2,000 mensagens." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Șterge mesajele din conversațiile din comunități mai vechi de 6 luni și din conversațiile unde sunt peste 2.000 de mesaje." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить сообщения из переписок сообщества старше 6 месяцев и в которых более 2,000 сообщений." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obriši poruke iz Community razgovora starijih od 6 mjeseci, gdje ima više od 2,000 poruka." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ප්‍රජාවේ සම්භාෂණ වලින් මාස 6කට වඩා පැරණි පණිවිඩ මකා දමන්න, සහ පණිවිඩ 2,000 ඉක්මවී ඇත." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vymazať správy z Community konverzácií starších ako 6 mesiacov, a kde je viac ako 2,000 správ." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izbrišite sporočila iz pogovorov v skupnostih, ki so starejša od 6 mesecev in kjer je več kot 2000 sporočil." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fshiji mesazhet nga bisedat e Community më të vjetra se 6 muaj, dhe ku ka mbi 2,000 mesazhe." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обриши поруке из Community преписки старије од 6 месеци, и када има преко 2000 порука." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obriši poruke iz konverzacija u Community starije od 6 meseci, i gde ih ima više od 2,000." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Radera meddelanden från Community-konversationer äldre än 6 månader, och där det finns över 2 000 meddelanden." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Futa ujumbe kutoka Mazungumzo ya Community wenye umri zaidi ya miezi 6, na ambapo kuna zaidi ya jumbe 2,000." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "6 மாதங்களுக்கு மேற்பட்ட மற்றும் 2,000 தகவல்களுக்கு மேற்பட்ட Community உரையாடலிலிருந்து தகவலைகளை நீக்கு." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సంఘాల సంభాషణల నుండి 6 నెలల కంటే పాతవి మరియు 2,000 సందేశాల కంటే ఎక్కువ ఉన్న సందేశాలను తొలగించండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ลบข้อความใน Community การสนทนาที่อายุมากกว่า 6 เดือน และมีข้อความมากกว่า 2,000 ข้อความ" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Topluluk sohbetlerinden 6 aydan eski ve 2.000'den fazla ileti içeren iletileri silin." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Видаляти повідомлення у спільнотах старше 6 місяців і де більше 2 000 повідомлень." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Community گفتگو سے پیغامات حذف کریں جو 6 مہینے سے زیادہ پرانے ہوں اور 2,000 سے زیادہ پیغامات ہوں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jamiyat suhbatlaridan 6 oydan oshgan va 2000 dan ortiq xabar mavjud bo'lgan xabarlarni o'chirish." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xóa tin nhắn từ cuộc hội thoại Community cũ hơn 6 tháng, và nơi có hơn 2.000 tin nhắn." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sangula imilayezo yeNguquko ezingaphaya kweinyanga ezi-6, nee malunga ne-2,000 yemilayezo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "删除来自拥有超过2,000条消息的社群内6个月以上的旧消息。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "刪除擁有2000條以上訊息的社群中,六個月之前的舊訊息" + "value" : "Auto-delete messages older than 6 months in communities with 2000+ messages." } } } @@ -121217,478 +122276,10 @@ "conversationsSendWithEnterKey" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stuur met Enter Sleutel" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "ارسل مع مفتاح الدخول" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter düyməsi ilə göndər" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "بھیج کے ساتھ انٹر کلید" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Адпраўце, націснуўшы клавішу Enter" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изпращане с Enter клавиш" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "এন্টার কি দিয়ে পাঠান" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envieu amb la tecla Enter" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odeslat klávesou Enter" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anfon gyda'r Allwedd Enter" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send med Enter-tasten" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mit Eingabetaste senden" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αποστολή με το πλήκτρο Enter" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Send with Enter Key" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sendi per la Eniga klavo" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar con la tecla Into" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar con la tecla de enter" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saada Enter-klahviga" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bidali Enter Teklarekin" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "ارسال با کلید Enter" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lähetä Enter-näppäimellä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send with Enter Key" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Envoyer avec bouton Entrée" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar coa tecla Enter" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aika tare da Maɓallin Shigarwa" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שלח עם מקש Enter" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "एन्टर कुंजी के साथ भेजें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pošalji pomoću tipke Enter" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Küldés az enter billentyűvel" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ուղարկել Enter ստեղնով" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kirim dengan Tombol Enter" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invia con il tasto Invio" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "エンターキーで送信" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ღილაკით გაგზავნა" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ផ្ញើដោយប្រើខ្យល់ Key" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಎಂಟರ್ ಕೀ ಬಳಸಿ ಕಳುಹಿಸಿ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter 키를 사용하여 보내기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ناردن بە کلیلی ئێنتر" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bi Enter Bicîh Bibe" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sindikira nga okozesa Enter Key" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siųsti naudojant Enter" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nosūtīt ar Enter taustiņu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Испрати со копчето Enter" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter товчлуурыг ашиглан илгээх" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hantar dengan Kekunci Enter" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter Key ဖြင့် ပို့ပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send med Enter Key" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send med Enter Key" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter कुञ्जीबाट पठाउनुहोस्" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verzenden met Enter toets" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send med Enter Key" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send with Enter Key" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਐਨਟੀਰ ਕੁੰਜੀ ਨਾਲ ਭੇਜੋ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wyślij za pomocą klawisza Enter" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د Enter کیلي سره ولیږئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar com a tecla Enter" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enviar com Enter" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trimite cu tasta Enter" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отправить с помощью клавиши Enter" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pošalji sa Enter tipkom" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter යතුර සමග යවන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odoslať správu tlačidlom Enter" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pošlji s tipko Enter" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dërgoni me Enter Key" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљи са ентер тастером" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pošalji pomoću tipke Enter" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sänd via tryckning på returtangent" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tuma na Kibonyezo cha Enter" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter விசையால் அனுப்பு" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ఎంటర్ కీతో పంపుము" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ส่งด้วยปุ่ม Enter" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter tuşu ile gönder" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Надіслати клавішею Enter" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter Key کے ساتھ بھیجنا" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter tugmasi bilan jo'natish" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gửi bằng Phím Enter" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thumela ngazo Ukhiye we-Enter" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用回车键发送" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "回車鍵發送" + "value" : "Send with Enter" } } } @@ -122172,6 +122763,17 @@ } } }, + "conversationsSendWithShiftEnter" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send with Shift+Enter" + } + } + } + }, "conversationsSettingsAllMedia" : { "extractionState" : "manual", "localizations" : { @@ -125618,12 +126220,30 @@ "value" : "कॉल बनाया जा रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hívás készítése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Membuat Panggilan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Creazione chiamata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話を作成中" + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -125648,6 +126268,18 @@ "value" : "Tworzenie połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A criar chamada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se creează apelul" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -125683,6 +126315,12 @@ "state" : "translated", "value" : "正在创建通话" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在建立通話" + } } } }, @@ -126171,6 +126809,17 @@ } } }, + "darkMode" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dark Mode" + } + } + } + }, "databaseErrorClearDataWarning" : { "extractionState" : "manual", "localizations" : { @@ -126198,24 +126847,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og oprette en ny konto?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du wirklich alle Nachrichten, Anhänge und Kontodaten von diesem Gerät löschen und ein neues Konto erstellen?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y crear una cuenta nueva?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y crear una cuenta nueva?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et créer un nouveau compte ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से सभी संदेश, अटैचमेंट और खाता डेटा हटाना चाहते हैं और एक नया खाता बनाना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és új fiókot hoz létre?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare tutti i messaggi, gli allegati e i dati dell'account da questo dispositivo e creare un nuovo account?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上のすべてのメッセージ、添付ファイル、アカウントデータを削除し、新しいアカウントを作成してもよろしいですか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -126234,11 +126919,53 @@ "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i utworzyć nowe konto?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende apagar todas as mensagens, anexos e dados da conta deste dispositivo e criar uma nova conta?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să ștergi toate mesajele, atașamentele și datele contului de pe acest dispozitiv și să creezi un cont nou?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения, вложения и данные учетной записи с этого устройства и создать новую учетную запись?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera alla meddelanden, bilagor och kontodata från denna enhet och skapa ett nytt konto?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm mesajları, ekleri ve hesap verilerini bu cihazdan silip yeni bir hesap oluşturmak istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення, вкладення та дані облікового запису з цього пристрою та створити новий обліковий запис?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要删除此设备上的所有消息、附件和帐户数据,并创建新帐户吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要從此裝置中刪除所有訊息、附件及帳號資料,並建立一個新帳號嗎?" + } } } }, @@ -126305,6 +127032,18 @@ "value" : "डेटाबेस त्रुटि हुई है।

    समस्या निवारण के लिए अपने एप्लिकेशन लॉग्स को शेयर करने के लिए निर्यात करें। यदि यह असफल रहता है, तो {app_name} को फिर से इंस्टॉल करें और अपना खाता पुनः प्राप्त करें।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adatbázishiba történt.

    Exportálja az alkalmazás naplóit, hogy megoszhassa azokat a hibaelhárításhoz. Ha ez nem sikerül, telepítse újra a(z) {app_name} alkalmazást és állítsa vissza a fiókját." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore nel database.

    Esporta i log dell'applicazione per condividerli e facilitare la risoluzione del problema. Se non funziona, reinstalla {app_name} e ripristina il tuo account." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -126329,12 +127068,30 @@ "value" : "Wystąpił błąd bazy danych.

    Wyeksportuj dzienniki aplikacji do udostępnienia w celu rozwiązania problemu. Jeśli to się nie powiedzie, zainstaluj ponownie {app_name} i przywróć swoje konto." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro na base de dados.

    Exporte os registos da sua aplicação para partilhar para apoio à resolução de problemas. Se isto não resultar, reinstale o {app_name} e recupere a sua conta." + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "A apărut o eroare în baza de date.

    Exportați jurnalele aplicației pentru a le partaja în vederea depanării. Dacă nu reușiți, reinstalați {app_name} și restaurați-vă contul." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Произошла ошибка базы данных.

    Экспортируйте журналы приложения для использования в целях устранения неполадок. Если это не поможет, переустановите {app_name} и восстановите учётную запись." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ett databasfel har inträffat.

    Exportera dina applikationsloggar för att dela dem för felsökning. Om detta misslyckas, installera om {app_name} och återställ ditt konto." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -126352,6 +127109,12 @@ "state" : "translated", "value" : "发生数据库错误。

    请导出您的应用日志以进行故障排除。如果不成功,请重新安装{app_name}并恢复您的帐户。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "發生資料庫錯誤。

    請匯出您的應用程式日誌以便分享並協助故障排除。如果仍無法解決,請重新安裝 {app_name} 並還原您的帳號。" + } } } }, @@ -126382,24 +127145,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og gendanne din konto fra netværket?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du wirklich alle Nachrichten, Anhänge und Kontodaten von diesem Gerät löschen und dein Konto aus dem Netzwerk wiederherstellen?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y restaurar tu cuenta desde la red?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y restaurar tu cuenta desde la red?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et restaurer votre compte depuis le réseau ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से सभी संदेश, अटैचमेंट और खाता डेटा हटाना चाहते हैं और नेटवर्क से अपना खाता पुनः प्राप्त करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és vissza állítja a fiókját a hálózatról?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare tutti i messaggi, gli allegati e i dati dell'account da questo dispositivo e ripristinare il tuo account dalla rete?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上のすべてのメッセージ、添付ファイル、アカウントデータを削除し、ネットワークからアカウントを復元してもよろしいですか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -126418,6 +127217,36 @@ "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i przywrócić konto z sieci?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende apagar todas as mensagens, anexos e dados da conta deste dispositivo e restaurar a sua conta a partir da rede?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur că vrei să ștergi toate mesajele, atașamentele și datele contului de pe acest dispozitiv și să restaurezi contul tău din rețea?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения, вложения и данные учетной записи с этого устройства и восстановить свою учетную запись из сети?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill ta bort alla meddelanden, bilagor och kontodata från den här enheten och återställa ditt konto från nätverket?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm mesajları, ekleri ve hesap verilerini bu cihazdan silip hesabınızı ağdan geri yüklemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -126429,6 +127258,18 @@ "state" : "translated", "value" : "Bạn có chắc chắn muốn xóa tất cả tin nhắn, tệp đính kèm, và dữ liệu tài khoản khỏi thiết bị này và khôi phục lại tài khoản của bạn từ mạng lưới?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要删除此设备上的所有消息、附件和帐户数据,并从网络中恢复你的帐户吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要從此裝置中刪除所有訊息、附件及帳號資料,並從網路中還原您的帳號嗎?" + } } } }, @@ -126929,7 +127770,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Tətbiqinizin databazası {app_name} tətbiqinin versiyası ilə uyumlu deyil. Yeni bir databaza yaratmaq və {app_name} istifadə etməyə davam etmək üçün tətbiqi yenidən quraşdırın və hesabınızı bərpa edin.

    Xəbərdarlıq: Bu, iki həftədən köhnə olan bütün mesajların və qoşmaların itkisi ilə nəticələnəcək." + "value" : "Tətbiqinizin veri bazası, {app_name} tətbiqinin versiyası ilə uyumlu deyil. Yeni bir veri bazası yaratmaq və {app_name} istifadə etməyə davam etmək üçün tətbiqi yenidən quraşdırın və hesabınızı bərpa edin.

    Xəbərdarlıq: Bu, iki həftədən köhnə olan bütün mesajların və qoşmaların itkisi ilə nəticələnəcək." } }, "bal" : { @@ -127408,7 +128249,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databaza optimallaşdırılır" + "value" : "Veri bazası optimallaşdırılır" } }, "bal" : { @@ -127462,7 +128303,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Optimiere Datenbank" + "value" : "Datenbank wird optimiert" } }, "el" : { @@ -134683,7 +135524,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "{name} hat den Timer für verschwindenen Nachrichten auf {time} eingestellt." + "value" : "{name} hat den Timer für verschwindende Nachrichten auf {time} eingestellt" } }, "el" : { @@ -136524,24 +137365,60 @@ "value" : "Er du sikker på, at du vil slette {name} fra dine kontakter?

    Dette vil slette din samtale, herunder alle beskeder og vedhæftede filer. Fremtidige beskeder fra {name} vises som en besked anmodning." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du {name} wirklich aus deinen Kontakten löschen?

    Dies wird deine Unterhaltung einschließlich aller Nachrichten und Anhänge löschen. Zukünftige Nachrichten von {name} erscheinen als Nachrichtenanfrage." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to delete {name} from your contacts?

    This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar a {name} de tus contactos?

    Esto eliminará tu conversación, incluidos todos los mensajes y archivos adjuntos. Los mensajes futuros de {name} aparecerán como una solicitud de mensaje." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar a {name} de tus contactos?

    Esto eliminará tu conversación, incluidos todos los mensajes y archivos adjuntos. Los mensajes futuros de {name} aparecerán como una solicitud de mensaje." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir supprimer {name} de vos contacts ?

    Cela supprimera votre conversation, y compris tous les messages et pièces jointes. Les futurs messages de {name} apparaîtront comme une demande de message." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई अपने संपर्कों से {name} को हटाना चाहते हैं?

    यह आपके वार्तालाप को हटा देगा, जिसमें सभी संदेश और अटैचमेंट्स शामिल हैं। {name} से भविष्य के संदेश Message request के रूप में दिखाई देंगे।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a névjegyek közül a következőt: {name}?

    Ezzel törli a beszélgetést, beleértve az összes üzenetet és mellékletet. A jövőben a(z) {name} nevű partnerétől érkező üzenetek üzenetkérésként fognak megjelenni." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare {name} dai tuoi contatti?

    Questo eliminerà la conversazione, inclusi tutti i messaggi e gli allegati. I messaggi futuri da parte di {name} verranno visualizzati come richiesta di messaggio." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name}を連絡先から削除してもよろしいですか?

    この操作により、すべての会話(メッセージや添付ファイルを含む)が削除されます。{name}からの今後のメッセージはメッセージリクエストとして表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -136560,6 +137437,36 @@ "value" : "Czy na pewno chcesz usunąć {name} ze swoich kontaktów?

    Spowoduje to usunięcie konwersacji, w tym wszystkich wiadomości i załączników. Przyszłe wiadomości od {name} będą wyświetlane jako prośba o wiadomość." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende remover {name} dos seus contactos?

    Isto eliminará a sua conversa, incluindo todas as mensagens e anexos. Mensagens futuras de {name} aparecerão como um pedido de mensagem." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să ștergi {name} din contactele tale?

    Aceasta va șterge conversația ta, inclusiv toate mesajele și atașamentele. Mesajele viitoare de la {name} vor apărea ca o solicitare de mesaj." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить {name} из контактов?

    Ваша переписка будет удалена, включая все сообщения и вложения. Последующие сообщения от {name} будут появляться в виде запроса на общение." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill ta bort {name} från dina kontakter?

    Detta kommer att radera din konversation, inklusive alla meddelanden och bilagor. Framtida meddelanden från {name} kommer att visas som en meddelandeförfrågan." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} kişisini kişilerinizden silmek istediğinizden emin misiniz?

    Bu işlem, tüm mesajlar ve ekler dahil olmak üzere sohbetinizi silecektir. {name} kişisinden gelen gelecekteki mesajlar, mesaj isteği olarak görünecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -136571,6 +137478,12 @@ "state" : "translated", "value" : "您确定要删除联系人{name}吗?

    该操作将删除你们的会话,包括所有消息和附件。来自{name}的新消息将被视为消息请求。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要從聯絡人中刪除 {name} 嗎?

    這將刪除您的對話,包括所有訊息與附件。來自 {name} 的未來訊息將會顯示為 Message request。" + } } } }, @@ -136601,24 +137514,60 @@ "value" : "Er du sikker på, at du vil slette din samtale med {name}?
    Dette vil permanent slette alle beskeder og vedhæftede filer." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du deine Unterhaltung mit {name} wirklich löschen?
    Dies wird alle Nachrichten und Anhänge dauerhaft löschen." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to delete your conversation with {name}?
    This will permanently delete all messages and attachments." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar tu conversación con {name}?
    Esto eliminará permanentemente todos los mensajes y archivos adjuntos." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres eliminar tu conversación con {name}?
    Esto eliminará permanentemente todos los mensajes y archivos adjuntos." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir supprimer votre conversation avec {name} ?
    Cela supprimera définitivement tous les messages et pièces jointes." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई {name} के साथ अपना वार्तालाप हटाना चाहते हैं?
    यह सभी संदेशों और अटैचमेंट्स को स्थायी रूप से हटा देगा।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a következő partnerével folytatott beszélgetést: {name}?
    Ez véglegesen törli az összes üzenetet és mellékletet." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare la tua conversazione con {name}?
    Questa azione eliminerà in modo permanente tutti i messaggi e gli allegati." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name}との会話を削除してよろしいですか?
    この操作により、すべてのメッセージと添付ファイルが完全に削除されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -136637,6 +137586,36 @@ "value" : "Czy na pewno chcesz usunąć swoją rozmowę z {name}?
    Spowoduje to trwałe usunięcie wszystkich wiadomości i załączników." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem certeza de que deseja apagar sua conversa com {name}?
    Isto irá eliminar permanentemente todas as mensagens e anexos." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să ștergi conversația cu {name}?
    Aceasta va șterge definitiv toate mesajele și fișierele atașate." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить свою беседу с {name}?
    Это приведет к безвозвратному удалению всех сообщений и вложений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera din konversation med {name}?
    Detta kommer permanent radera alla meddelanden och bilagor." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} ile olan sohbetinizi silmek istediğinizden emin misiniz?
    Bu işlem, tüm mesajları ve ekleri kalıcı olarak silecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -136648,6 +137627,12 @@ "state" : "translated", "value" : "您确定要删除您与{name}的会话吗?
    该操作将永久删除所有消息和附件。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除與 {name} 的對話嗎?
    這將永久刪除所有訊息和附件。" + } } } }, @@ -139565,6 +140550,68 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem certeza de que deseja apagar esta mensagem?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem certeza de que deseja apagar essas mensagens?" + } + } + } + } + } + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunteți sigur că doriți să ștergeți aceste mesaje?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunteți sigur că doriți să ștergeți acest mesaj?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunteți sigur că doriți să ștergeți aceste mesaje?" + } + } + } + } + } + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -139744,6 +140791,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除這些訊息嗎?" + } + } + } + } + } + } } } }, @@ -143523,6 +144592,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på du vil slette denne beskjeden fra denne enheten bare?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker du vil slette dem her beskjedene fra denne enheten bare?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -143591,6 +144688,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem certeza de que deseja apagar esta mensagem apenas deste dispositivo?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza que pretende eliminar estas mensagens apenas deste dispositivo?" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -143804,6 +144929,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定只從此設備上刪除這些訊息嗎?" + } + } + } + } + } + } } } }, @@ -143825,7 +144972,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu mesajı hamı üçün silmək istədiyinizə əminsiniz?" + "value" : "Bu mesajı hər kəs üçün silmək istədiyinizə əminsiniz?" } }, "bal" : { @@ -152333,7 +153480,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "开始听写..." + "value" : "开始语音输入..." } }, "zh-TW" : { @@ -168660,6 +169807,17 @@ } } }, + "display" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display" + } + } + } + }, "displayNameDescription" : { "extractionState" : "manual", "localizations" : { @@ -173201,6 +174359,12 @@ "donate" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İanə ver" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -173213,6 +174377,12 @@ "value" : "Darovat" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spenden" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -173225,29 +174395,113 @@ "value" : "Donaci" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Faire un don" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "दान करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adományozás" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dona" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "寄付" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "후원하기" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doneer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wspomóż" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fazer uma doação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Донат" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağış yap" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримати" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "捐赠" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "捐款" + } } } }, @@ -186821,47 +188075,519 @@ } } }, + "enableNotifications" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show notifications when you receive new messages." + } + } + } + }, "enjoyingSession" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-dan zövq alırsınız?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Líbí se vám {app_name}?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gefällt dir {app_name}?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enjoying {app_name}?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Te está gustando {app_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Te está gustando {app_name}?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous aimez {app_name} ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप {app_name} का आनंद ले रहे हैं?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ti piace {app_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}を楽しんでいますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geniet je van {app_name}?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podoba Ci się {app_name}?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está a gostar do {app_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Îți place {app_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нравится {app_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gillar du {app_name}?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подобається {app_name}?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "喜欢使用 {app_name} 吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "喜歡使用 {app_name} 嗎?" + } } } }, "enjoyingSessionButtonNegative" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Təkmilləşməlidir {emoji}" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necessita feina {emoji}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potřebuje vylepšit {emoji}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbesserungswürdig {emoji}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Needs Work {emoji}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesita mejoras {emoji}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesita mejoras {emoji}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des améliorations seraient utiles {emoji}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सुधार की आवश्यकता है {emoji}" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Da migliorare {emoji}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "改善が必要です {emoji}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moet beter {emoji}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymaga poprawek {emoji}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precisa de melhorias {emoji}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mai e de lucru {emoji}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется доработка {emoji}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Behöver förbättras {emoji}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потребує доопрацювання {emoji}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要改进 {emoji}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "有待改進 {emoji}" + } } } }, "enjoyingSessionButtonPositive" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əladır {emoji}" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"És fantàstic {emoji}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skvělé {emoji}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Großartig {emoji}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "It's Great {emoji}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está genial {emoji}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está genial {emoji}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C’est génial {emoji}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बहुत बढ़िया {emoji}" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È fantastica {emoji}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すばらしいです {emoji}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geweldig {emoji}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jest świetnie {emoji}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está ótimo {emoji}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grozav {emoji}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отлично {emoji}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Det är fantastiskt {emoji}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Крутяк {emoji}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很棒 {emoji}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很棒 {emoji}" + } } } }, "enjoyingSessionDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqini bir müddətdir istifadə edirsiniz, necə gedir? Fikirlərinizi eşitmək bizim üçün çox dəyərli olardı." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estat utilitzant {app_name} durant una estona, com va? Ens agradaria escoltar els teus pensaments." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Používáte {app_name}
    a rádi bychom znali váš názor." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du nutzt {app_name} jetzt schon eine Weile – wie läuft’s? Wir würden uns sehr über dein Feedback freuen." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You've been using {app_name} for a little while, how’s it going? We’d really appreciate hearing your thoughts." } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estado usando {app_name} por un tiempo, ¿cómo va todo? Agradeceríamos mucho saber tu opinión." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estado usando {app_name} por un tiempo, ¿cómo va todo? Agradeceríamos mucho saber tu opinión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous utilisez {app_name} depuis un petit moment, comment ça se passe ? Nous aimerions beaucoup connaître votre avis." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आप कुछ समय से {app_name} का उपयोग कर रहे हैं, सब कैसा चल रहा है? हम आपके विचार जानकर बहुत आभारी होंगे।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stai usando {app_name} da un po', come ti trovi? Ci farebbe piacere conoscere la tua opinione." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}をご利用いただいてしばらく経ちましたね。調子はいかがですか?ご意見をお聞かせいただけると嬉しいです。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je gebruikt {app_name} al een tijdje, hoe gaat het? We horen graag je mening." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korzystasz z {app_name} już od jakiegoś czasu, jak Ci się podoba? Będziemy bardzo wdzięczni za Twoją opinię." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Já usa o {app_name} há algum tempo, como tem corrido? Gostaríamos muito de ouvir a sua opinião." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosești {app_name} de ceva timp, cum ți se pare? Ne-ar face mare plăcere să aflăm părerea ta." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уже некоторое время пользуетесь {app_name}, как оно? Нам действительно интересно узнать ваше мнение." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har använt {app_name} ett tag, hur går det? Vi skulle uppskatta om du delar dina tankar." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви вже деякий час користуєтесь {app_name}, які у вас враження? Нам би дуже хотілося дізнатися вашу думку." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已使用 {app_name} 一段时间了,感觉如何?非常希望能听到您的反馈。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已使用 {app_name} 一段時間,使用體驗如何?我們非常希望能聽到您的想法。" + } + } + } + }, + "enter" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter" + } } } }, @@ -187847,7 +189573,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databaza xətası" + "value" : "Veri bazası xətası" } }, "bal" : { @@ -188317,6 +190043,12 @@ "errorGeneric" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nəsə səhv getdi. Lütfən daha sonra yenidən sınayın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -188329,6 +190061,12 @@ "value" : "Něco se pokazilo. Zkuste to prosím později." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etwas ist schiefgelaufen. Bitte versuche es später erneut." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -188341,18 +190079,48 @@ "value" : "Io misfunkciis. Bonvolu reprovi poste." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Une erreur s'est produite. Veuillez réessayer plus tard." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Valami hiba történt. Próbálja meg később újra." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore. Riprova più tardi." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "問題が発生しました。後でもう一度お試しください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -188371,11 +190139,53 @@ "value" : "Coś poszło nie tak. Spróbuj ponownie później." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro. Por favor, tente novamente mais tarde." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ceva nu a mers bine. Te rugăm să încerci din nou mai târziu." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Что-то пошло не так. Попробуйте ещё раз позже." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Något gick fel. Försök igen senare." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir hata oluştu. Lütfen daha sonra tekrar deneyin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щось пішло не так. Будь ласка, спробуйте пізніше." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "出现问题。请稍后再试。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "發生錯誤。請稍後再試。" + } } } }, @@ -188915,6 +190725,18 @@ "value" : "Elŝutado fiaskis" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descarga fallida" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descarga fallida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -188939,6 +190761,18 @@ "value" : "Gagal mengunduh" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download non riuscito" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダウンロードに失敗しました" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -188957,6 +190791,18 @@ "value" : "Nie udało się pobrać" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Falha ao transferir" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descărcarea a eșuat" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -188969,6 +190815,12 @@ "value" : "Nedladdningen misslyckades" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İndirme başarısız" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -188986,6 +190838,12 @@ "state" : "translated", "value" : "下载失败" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "下載失敗" + } } } }, @@ -189468,6 +191326,28 @@ } } }, + "feedback" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + } + } + }, + "feedbackDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share your experience with {app_name} by completing a short survey." + } + } + } + }, "file" : { "extractionState" : "manual", "localizations" : { @@ -190429,573 +192309,177 @@ "followSystemSettings" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Volg stelselinstellings" - } - }, - "ar" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "طابق إعدادات النظام" + "value" : "Follow system settings." } - }, + } + } + }, + "forever" : { + "extractionState" : "manual", + "localizations" : { "az" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem ayarlarını izlə" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "آگاھی سسیتم پرات وٹ" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выкарыстоўвайце налады сістэмны" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Следвай системните настройки" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Follow system settings" + "value" : "Həmişəlik" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Seguir configuracions del sistema" + "value" : "Per sempre" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít nastavení systému" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dilyn gosodiadau'r system" + "value" : "Navždy" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Følg systemindstillinger" + "value" : "For altid" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Systemeinstellungen folgen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αντιστοίχιση ρυθμίσεων συστήματος" + "value" : "Für immer" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Follow system settings" + "value" : "Forever" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Sekvi sistemajn agordojn" + "value" : "Por ĉiam" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Seguir configuración del sistema" + "value" : "Para siempre" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Coincidir ajustes del sistema" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Järgi süsteemi sätteid" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jarraitu sistema ezarpenak" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "مطابق با تنظیمات سیستم" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seuraa järjestelmän asetusta" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sundin ang mga setting ng system" + "value" : "Para siempre" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Faire correspondre aux paramètres systèmes" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir a configuración do sistema" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bi saitunan tsarin" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "עקוב אחרי הגדרות המערכת" + "value" : "Définitivement" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम सेटिंग्स का पालन करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slijedite sistemske postavke" + "value" : "हमेशा के लिए" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Rendszerbeállítások követése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Հետևել համակարգի կարգավորումներին" + "value" : "Örökre" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Sesuaikan dengan pengaturan sistem" + "value" : "Selamanya" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizza le impostazioni di sistema" + "value" : "Per sempre" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "システム設定に合わせる" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "დაემორჩილეთ სისტემის კონფიგურაციას" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ផ្គូផ្គងការកំណត់ប្រព័ន្ធ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಅಂಕಣೋತ್ಸವವು ಪಾಲಿಸಲಾಗುವುದು" + "value" : "常に" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 설정 따르기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "شیاوی سیستەمەکان" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mîhengên sîstemê taqîb bike" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Goberera ssitula z'empuliziganya" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sekti sistemos nustatymus" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sekot sistēmas iestatījumiem" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Следи системски подесувања" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Системийн тохиргоонуудыг дагах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikut tetapan sistem" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စက်ရုပ်ဆီstem ဆက်တင်များကိုလိုက်နာပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "सहयोग पृष्ठमा जानुहोस्" + "value" : "영원히" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Systeeminstellingen volgen" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tsatira makonda a dongosolo" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸਿਸਟਮ ਸੈਟਿੰਗਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ" + "value" : "Voor altijd" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dopasuj do ustawień systemu" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د سیسټم تنظیمات تعقیب کړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acompanhar as configurações do sistema" + "value" : "Zawsze" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Alinhar com definições do sistema" + "value" : "Para sempre" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Urmează setările sistemului" + "value" : "Pentru totdeauna" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать настройки системы" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prati sistemske postavke" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "සැකසුමට ගළපන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rovnaké ako nastavenia systému" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sledi sistemskim nastavitvam" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndiq cilësimet e sistemit" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пратити системска подешавања" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prati sistemska podešavanja" + "value" : "Навсегда" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Följ systeminställningar" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fuata mipangilio ya mfumo" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கணினி அமைப்புகளை பின்பற்றுக" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సిస్టమ్ సెట్టింగ్‌లను అనుసరించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ตามการตั้งค่าระบบ" + "value" : "För alltid" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem ayarlarını takip et" + "value" : "Sonsuza dek" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати системні налаштування" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "سسٹم کی ترتیبات کو فالو کریں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tizim sozlamalariga ergashing" + "value" : "Завжди" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Theo cài đặt hệ thống" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Landela usetiwe lwenkqubo" + "value" : "Vĩnh viễn" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "匹配系统设置" + "value" : "永久" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "遵循系統設定" - } - } - } - }, - "forever" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Həmişəlik" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Per sempre" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Navždy" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "For altid" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forever" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Por ĉiam" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Définitivement" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Örökre" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selamanya" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "영원히" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voor altijd" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zawsze" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завжди" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vĩnh viễn" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "永久" + "value" : "永遠" } } } @@ -193404,22 +194888,250 @@ "giveFeedback" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əks-əlaqə vermək istəyirsiniz?" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dóna comentaris?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poskytnout zpětnou vazbu?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback geben?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Give Feedback?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Enviar comentarios?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Enviar comentarios?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donner votre avis ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिक्रिया देना चाहते हैं?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lascia un feedback?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィードバックを送りますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback geven?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przekazać opinię?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dar opinião?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oferi feedback?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оставите отзыв?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ge feedback?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Залишити відгук?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "愿意提供反馈吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供回饋?" + } } } }, "giveFeedbackDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} təcrübənizin yaxşı olmadığını eşitmək bizi məyus etdi. Fikirlərinizi qısa anket üzərindən bizimlə paylaşsanız minnətdar olarıq" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disculpa escoltar la teva experiència {app_name} no ha estat ideal. Ens agrairem si podries prendre un moment per compartir els teus pensaments en una breu enquesta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je nám líto, že vaše zkušenost se {app_name} nebyla ideální. Ocenili bychom, kdybyste věnovali chvíli vyplnění krátkého dotazníku" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es tut uns leid zu hören, dass deine Erfahrung mit {app_name} nicht ideal war. Wir würden uns freuen, wenn du dir einen Moment Zeit nimmst und deine Gedanken in einer kurzen Umfrage mit uns teilst." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Sorry to hear your {app_name} experience hasn’t been ideal. We'd be grateful if you could take a moment to share your thoughts in a brief survey" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que tu experiencia con {app_name} no ha sido ideal. Estaríamos agradecidos si pudieras tomarte un momento para compartir tus opiniones en una breve encuesta." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que tu experiencia con {app_name} no ha sido ideal. Estaríamos agradecidos si pudieras tomarte un momento para compartir tus opiniones en una breve encuesta." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes désolés d’apprendre que votre expérience avec {app_name} n’a pas été idéale. Nous vous serions reconnaissants si vous pouviez prendre un instant pour répondre à une brève enquête." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हमें खेद है कि आपका {app_name} अनुभव आदर्श नहीं रहा। यदि आप एक क्षण निकाल सकें और एक छोटे सर्वेक्षण में अपने विचार साझा करें, तो हम आभारी होंगे" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ci dispiace sapere che la tua esperienza con {app_name} non è stata ideale. Ti saremmo grati se prendessi un momento per condividere i tuoi pensieri in un breve sondaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} のご利用が理想的でなかったとのことで残念です。お時間があれば、簡単なアンケートでご意見をお聞かせください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jammer om te horen dat je ervaring met {app_name} niet ideaal was. We zouden het waarderen als je een momentje neemt om je mening te delen in een korte enquête" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przykro nam, że Twoje doświadczenie z {app_name} nie było idealne. Bylibyśmy wdzięczni, gdybyś poświęcił chwilę na podzielenie się swoją opinią w krótkiej ankiecie" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que a sua experiência com o {app_name} não foi ideal. Agradecíamos se pudesse partilhar a sua opinião através de um breve questionário" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne pare rău că experiența ta cu {app_name} nu a fost ideală. Am aprecia dacă ne-ai putea împărtăși părerea ta într-un scurt sondaj" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сожалеем, что ваш опыт использования {app_name} не был идеальным. Будем признательны, если вы уделите немного времени и поделитесь своими мыслями в кратком опросе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tråkigt att höra att din upplevelse med {app_name} inte varit optimal. Vi skulle uppskatta om du kunde ta ett ögonblick för att dela dina tankar i en kort undersökning." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шкодуємо, що ваш досвід користування {app_name} був неідеальним. Ми були б вдячні, якби ви змогли поділитися своїми думками у короткому опитуванні" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很遗憾听到您在 {app_name} 上的使用体验不佳。希望您抽空填写简短问卷,分享您的看法。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很遺憾聽到您在使用 {app_name} 時的體驗不盡理想。如果您能花點時間在簡短問卷中分享您的想法,我們將不勝感激" + } } } }, @@ -195378,6 +197090,12 @@ "value" : "Er du sikker på, at du vil slette {group_name}?

    Dette vil fjerne alle medlemmer og slette alt gruppe-indhold." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du {group_name} verlassen möchtest?

    Dadurch werden alle Mitglieder entfernt und alle Gruppendaten gelöscht." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -195414,12 +197132,24 @@ "value" : "क्या आप वाकई {group_name} को हटाना चाहते हैं?

    इससे सभी सदस्य हट जाएंगे और समूह की सारी सामग्री भी मिट जाएगी।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biztosan ki akar lépni a(z) {group_name} csoportból?

    Ez az összes tag eltávolításával és a csoport teljes tartalmának törlésével jár." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Apakah Anda yakin ingin menghapus {group_name}?

    Ini akan mengeluarkan semua anggota dan menghapus semua konten grup." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confermi di voler eliminare {group_name}?

    Questo eliminerà tutti i membri e cancellerà tutto il contenuto del gruppo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -195450,12 +197180,30 @@ "value" : "Czy na pewno chcesz usunąć {group_name}?

    Spowoduje to usunięcie wszystkich członków i całej zawartości grupy." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende eliminar {group_name}?

    Isto irá remover todos os membros e eliminar todo o conteúdo do grupo." + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "Sunteți sigur că vreți să ștergeți {group_name}?

    Aceasta va elimina toți membrii și va șterge tot conținutul grupului." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить {group_name}?

    Это приведет к удалению всех участников и всего содержимого группы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera {group_name}?

    Detta kommer att ta bort alla medlemmar och radera allt gruppinnehåll." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -195473,6 +197221,12 @@ "state" : "translated", "value" : "您确定要删除{group_name}吗?

    这将移除所有成员并删除所有群组内容。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除 {group_name} 嗎?

    這將移除所有成員並刪除所有群組內容。" + } } } }, @@ -195593,6 +197347,18 @@ "value" : "Czy jesteś pewny, że chcesz usunąć {group_name}?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende eliminar {group_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi {group_name}?" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -195628,6 +197394,12 @@ "state" : "translated", "value" : "你确定要删除群组 {group_name}吗?" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除 {group_name} 嗎?" + } } } }, @@ -195748,6 +197520,18 @@ "value" : "{group_name} została usunięta przez administratora grupy. Nie będzie można wysyłać więcej wiadomości." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} foi eliminado por um administrador do grupo. Não poderá enviar mais mensagens." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} a fost șters de către un administrator de grup. Nu veți mai putea trimite mesaje." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -195783,6 +197567,12 @@ "state" : "translated", "value" : "{group_name} 已被群组管理员删除。您将无法再发送任何信息。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} 已被群組管理員刪除。您將無法再傳送任何訊息。" + } } } }, @@ -201675,6 +203465,12 @@ "value" : "Invito non inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待が送信されていません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -201693,6 +203489,18 @@ "value" : "Zaproszenie niewysłane" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Convite não enviado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitația nu a fost trimisă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -201728,6 +203536,12 @@ "state" : "translated", "value" : "邀请未发送" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請未傳送" + } } } }, @@ -202670,6 +204484,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待状を送信中" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -202844,6 +204680,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando convite" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando convites" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -203057,6 +204921,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送邀請" + } + } + } + } + } + } } } }, @@ -203632,6 +205518,12 @@ "value" : "Stato invito sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待状のステータスが不明です" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -203650,6 +205542,18 @@ "value" : "Status zaproszenia nieznany" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado do convite desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statusul invitației necunoscut" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -203685,6 +205589,12 @@ "state" : "translated", "value" : "邀请状态未知" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請狀態未知" + } } } }, @@ -208693,7 +210603,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{name}{count} başqaları qrupu tərk etdi." + "value" : "{name}başqa {count} nəfər qrupu tərk etdi." } }, "bal" : { @@ -213001,6 +214911,18 @@ "value" : "Vi kaj {other_name} estis invititaj por aliĝi al la grupo. Historio de la babilejo estis diskonigita." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : " y {other_name} fueron invitados a unirse al grupo. Se ha compartido el historial del chat." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : " y {other_name} fueron invitados a unirse al grupo. Se ha compartido el historial del chat." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -213049,6 +214971,12 @@ "value" : "Ty i {other_name} zostaliście zaproszeni do grupy. Historia czatu została udostępniona." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você e {other_name} foram convidados a juntar-se ao grupo. O histórico da conversa foi partilhado." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -213090,6 +215018,12 @@ "state" : "translated", "value" : "{other_name}被邀请加入了群组。 聊天记录已共享。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{other_name} 加入了群組。聊天記錄已分享。" + } } } }, @@ -218143,6 +220077,12 @@ "value" : "Denne gruppe er ikke blevet opdateret i over 30 dage. Du kan opleve problemer med at sende beskeder eller se gruppeoplysninger." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Gruppe wurde seit über 30 Tagen nicht aktualisiert. Beim Senden von Nachrichten oder beim Anzeigen der Gruppeninformationen können Probleme auftreten." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -218185,6 +220125,18 @@ "value" : "A csoportot több mint 30 napja nem frissítették. Előfordulhat, hogy problémák merülnek fel az üzenetek küldésével vagy a csoportinformációk megtekintésével kapcsolatban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo gruppo non è stato aggiornato da oltre 30 giorni. Potresti riscontrare problemi nell'invio dei messaggi o nella visualizzazione delle informazioni del gruppo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このグループは30日以上更新されていません。メッセージの送信やグループ情報の表示に問題が発生する可能性があります。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -218203,6 +220155,30 @@ "value" : "Ta grupa nie była aktualizowana od ponad 30 dni. Mogą wystąpić problemy z wysyłaniem wiadomości lub wyświetlaniem informacji o grupie." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo não foi atualizado nos últimos 30 dias. Pode ter problemas ao enviar mensagens ou ao visualizar informações do grupo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup nu a fost actualizat de peste 30 de zile. Este posibil să întâmpinați probleme la trimiterea mesajelor sau vizualizarea informațiilor grupului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эта группа не обновлялась более 30 дней. У вас могут возникнуть проблемы с отправкой сообщений или просмотром информации о группе." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denna grupp har inte uppdaterats på över 30 dagar. Du kan uppleva problem med att skicka meddelanden eller visa gruppinformation." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -218220,6 +220196,12 @@ "state" : "translated", "value" : "此群组已超过 30 天未更新。您可能在发送消息或查看群组信息时遇到问题。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組已超過 30 天未更新。您可能在傳送訊息或查看群組資訊時遇到問題。" + } } } }, @@ -218789,6 +220771,12 @@ "value" : "Rimozione in corso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除保留中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -218807,6 +220795,18 @@ "value" : "Oczekuje na usunięcie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remoção pendente" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "În curs de eliminare" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -218842,6 +220842,12 @@ "state" : "translated", "value" : "待移除" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "等待移除" + } } } }, @@ -219914,6 +221920,12 @@ "value" : "Ty i {other_name} zostaliście administratorami." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você e {other_name} foram promovidos a Admin." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -219955,6 +221967,12 @@ "state" : "translated", "value" : "{other_name}已被授权为管理员。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{other_name} 被設置為管理員。" + } } } }, @@ -230598,12 +232616,30 @@ "value" : "कनेक्शन उम्मीदवारों को संभाला जा रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapcsolat jelöltek kezelése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menangani Kandidat Sambungan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestione dei candidati alla connessione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続候補を処理中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -230622,6 +232658,18 @@ "value" : "Obsługa kandydatów do połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A processar candidatos de ligação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se gestionează candidații pentru conexiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -230634,6 +232682,12 @@ "value" : "Hantera kontakt kandidater" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı Adayları İşleniyor" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -230645,6 +232699,18 @@ "state" : "translated", "value" : "Đang xử lý thông tin các kết nối khả dĩ" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "处理连接候选人" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在處理連線候選項目" + } } } }, @@ -231127,6 +233193,17 @@ } } }, + "helpFAQDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check the {app_name} FAQ for answers to common questions." + } + } + } + }, "helpHelpUsTranslateSession" : { "extractionState" : "manual", "localizations" : { @@ -231603,478 +233680,10 @@ "helpReportABug" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporteer 'n fout" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الإبلاغ عن خطأ" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bir xətanı bildir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "بگ رپورٹ کنت" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паведаміць аб памылцы" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Докладвай за проблем" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "বাগ রিপোর্ট করুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informeu d'un error" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nahlásit chybu" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adrodd nam" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en fejl" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Melde einen Fehler" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αναφορά Σφάλματος" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Report a bug" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raporti eraron" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar un error" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar Error" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teata veast" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akats bat salatu" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "گزارش خرابی" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ilmoita virheestä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mag-ulat ng bug" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signaler un bug" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informar dun erro" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bayyana bug" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "דווח על תקלה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "बग सूचित करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi bug" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiba jelentése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Հաղորդել սխալի մասին" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Laporkan Bug" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segnala un bug" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "バグを報告" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შეცდომის შესახებ" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "រាយការណ៍ពីកំហុសមួយ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ದೋಷವನ್ನು ವರದಿ ಮಾಡಿ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "버그 제보" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "راپۆرتی هەڵە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Çewtiyekê ragihîne" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alaga ekivvuuni" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pranešti apie klaidą" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ziņot par kļūdu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пријави грешка" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Алдааг мэдээлэх" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Laporkan pepijat" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ပြဿနာတစ်ခုကို အစီရင်ခံပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en feil" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en bug" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "बग रिपोर्ट गर्नुहोस्" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meld een bug" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en bug" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fotokozerani Chifukwa cholakwikacho" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਬੱਗ ਦੀ ਰਿਪੋਰਟ ਕਰੋ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zgłoś błąd" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "یو غلطي راپور کړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar um erro" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar um Erro" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raportează o eroare" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сообщить об ошибке" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi grešku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "දෝෂයක් වාර්තා කරන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nahlásiť chybu" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavite napako" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raporto një difekt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пријави грешку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi grešku" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapportera en bugg" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ripoti hitilafu" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "முடையைச் செய்யல்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "బగ్‌ను నివేదించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รายงานบั๊ก" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bir hata bildir" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомити про помилку" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "بگ کی اطلاع دیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xato haqida habar bering" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Báo cáo lỗi" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xela impazamo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bug反馈" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "回報錯誤" + "value" : "Report a Bug" } } } @@ -234010,478 +235619,10 @@ "helpReportABugExportLogsSaveToDesktopDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stoor hierdie lêer na jou lessenaar, en deel dit dan met die {app_name} ontwikkelaars." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "احفظ هذا الملف على سطح المكتب، ثم شاركه مع مطوري {app_name}." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu faylı masaüstündə saxlayıb {app_name} tərtibatçıları ilə paylaşın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کاردارانءَ ہَچّ اِی فائیل انت دسک ٹاپیْ، پَس اِے کِھ شیئر کَپُت." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захавайце гэты файл на сваім працоўным стале, затым падзяліцеся ім з распрацоўшчыкамі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Запазете този файл на работния плот и го споделете с разработчиците на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "এই ফাইলটি আপনার ডেস্কটপে সংরক্ষণ করুন, তারপর এটি {app_name} ডেভেলপারদের সাথে শেয়ার করুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deseu aquest fitxer a l'escriptori, i després compartiu-lo amb els desenvolupadors de {app_name}." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uložte tento soubor na plochu, poté jej sdílejte s vývojáři aplikace {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cadw'r ffeil hon i'ch bwrdd gwaith, yna ei rhannu gyda datblygwyr {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gemt denne fil til din desktop, og del den derefter med {app_name} udviklere." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Speichere diese Datei auf deinem Desktop und teile sie dann mit den Entwicklern von {app_name}." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αποθηκεύστε αυτό το αρχείο στην επιφάνεια εργασίας σας, και μετά κοινοποιήστε το στους προγραμματιστές του {app_name}." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konservu ĉi tiun dosieron al via labortablo, poste kunhavigu ĝin kun la zhviligistoj de {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este archivo en su escritorio, luego compártalo con los desarrolladores de {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este archivo en su escritorio y luego compártalo con los desarrolladores de {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salvesta see fail oma arvutisse ja jaga seda {app_name} arendajatega." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gorde fitxategi hau zure mahaigainean, eta ondoren partekatu {app_name} garatzaileekin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "این فایل را در دسکتاپ خود ذخیره کنید، سپس آن را با توسعه‌دهندگان {app_name} به اشتراک بگذارید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tallenna tämä tiedosto työpöydällesi ja jaa se sitten {app_name}in kehittäjille." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enregistrez ce fichier sur votre bureau, puis partagez-le avec les développeurs de {app_name}." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Garda este ficheiro no teu escritorio, logo compárteo cos desenvolvedores de {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajiye wannan fayil zuwa teburin kwamfutarka, sannan raba shi tare da masu haɓaka {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שמור את הקובץ הזה לשולחן העבודה שלך, ואז שתף אותו עם המפתחים של {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "इस फ़ाइल को अपने डेस्कटॉप पर सहेजें, फिर इसे {app_name} डेवलपर्स के साथ साझा करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spremi ovu datoteku na radnu površinu, zatim je podijeli s razvojnim timom {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mentse ezt a fájlt az asztalra, majd ossza meg a {app_name} fejlesztőivel." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Պահեք այս ֆայլը ձեր աշխատասեղանին, ապա կիսվեք այն {app_name} ծրագրավորողների հետ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simpan file ini ke desktop Anda, lalu bagikan dengan pengembang {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salva questo file sul tuo desktop, poi condividilo con gli sviluppatori di {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "このファイルをデスクトップに保存し、{app_name}開発者に共有してください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შეინახე ეს ფაილი დესკტოპზე, შემდეგ გაუზიარე მას {app_name}-ის დეველოპერებს." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "រក្សាទុកឯកសារនេះទៅក្នុងកុំព្យូទ័ររបស់អ្នក រួចចែករំលែកទៅអ្នកអភិវឌ្ឍ​ {app_name}" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಈ ಕಡತವನ್ನು ನಿಮ್ಮ ಡೆಸ್ಕ್‌ಟಾಪ್‌ಗೆ ಉಳಿಸಿ, ನಂತರ ಅದನ್ನು {app_name} ಡೆವಲಪರ್‌ಗಳಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "이 파일을 데스크탑에 저장한 후, {app_name} 개발자와 공유하세요." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ئەم فایلە پاشەکەوت بکە لە سەر ئۆفیسەکەت، پاشان بهێنە گیاندن بە پەرەودەکارانی {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vê pelê li sermaseyê xwe qeydkirin, piştî wê bi pêşkeftinên {app_name} ve parastin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kuuma fayiro eno ku desktop yo oluvannyuma ogigabane ne bakozi ba {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Įrašykite šį failą ant savo darbalaukio, tada pasidalinkite juo su {app_name} kūrėjais." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saglabājiet šo failu savā darbvirsmā, pēc tam dalieties ar to {app_name} izstrādātājiem." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зачувај ја оваа датотека на твоето работно место, потоа сподели ја со развивачите на {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Энэ файлыг таны ширээний компьютер дээр хадгалж, дараа нь {app_name} хөгжүүлэгчидтэй хуваалцаарай." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simpan fail ini ke desktop anda, kemudian kongsikannya dengan pembangun {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ဤဖိုင်ကို သင့်ကွန်ပျူတာဖော်ပြရာနေရာတွင် သိမ်း၊ ထို့နောက် {app_name} ဖွံ့ဖြိုးသူများနဲ့ ဝေမျှပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lagre denne filen til skrivebordet, deretter del den med {app_name}-utviklerne." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lagre denne filen til skrivebordet, og del den deretter med {app_name} utviklere." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "यो फाइल तपाईंको डेस्कटपमा बचत गर्नुहोस्, त्यसपछि यसलाई {app_name} विकासकर्ताहरूसँग साझा गर्नुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sla dit bestand op uw bureaublad op en deel het dan met {app_name} ontwikkelaars." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lagre denne fila til skrivebordet, og del ho med utviklarane av {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਇਸ ਫਾਈਲ ਨੂੰ ਆਪਣੇ ਡੈਸਕਟਾਪ 'ਤੇ ਸੇਵ ਕਰੋ, ਫਿਰ ਇਸ ਨੂੰ {app_name} ਡਿਵੈਲਪਰਾਂ ਨਾਲ ਸਾਂਝੀ ਕਰੋ ਜੀ." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zapisz plik na pulpicie, a następnie udostępnij go programistom aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "دا دوسیه خپل ډیسټاپ ته وساتئ ، بیا یې د {app_name} پراختیا کونکي سره شریکه کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salve este arquivo no seu desktop, então compartilhe-o com os desenvolvedores do {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este ficheiro no seu desktop e partilhe-o com os desenvolvedores do {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salvează acest fișier pe desktop-ul tău, apoi partajează-l cu dezvoltatorii {app_name}." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сохраните этот файл на рабочий стол, затем поделитесь им с разработчиками {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sačuvajte ovu datoteku na svom desktopu, a zatim je podijelite s {app_name} programerima." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මේ ගොනුව ඔබේ ඩෙස්ක්ටොප් එකට සුරකින්න, අවසානයේ එය {app_name} සංවර්ධකයන් සමඟ බෙදාගන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uložiť tento súbor na pracovnú plochu, potom ho zdieľajte s vývojármi aplikácie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shrani to datoteko na namizje in jo nato deli z razvijalci {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruaje këtë skedar në desktop-in tuaj, pastaj ndaje me zhvilluesit e {app_name}." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сачувајте овај фајл на вашој радној површини, затим поделите са {app_name} програмерима." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sačuvajte ovaj fajl na svom desktopu, zatim ga podelite sa programerima {app_name}." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spara denna fil till ditt skrivbord och dela den sedan med {app_name} utvecklarna." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hifadhi faili hili kwenye desktop yako, kisha shirikisha na watengenezaji wa {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "இந்த கோப்பை உங்கள் டெஸ்க்டாப் கணினியில் சேமிக்கவும், பின்னர் ஏதும் பொருந்தும் {app_name} அபிவிருத்தியாளர்களுக்கு பகிரவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ఈ దస్త్రాన్ని మీ డెస్క్‌టాప్‌లో సేవ్ చేసి, తరువాత దాన్ని {app_name} డెవలపర్‌లకు షేర్ చేయండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "บันทึกไฟล์นี้ลงในเดสก์ท็อปของคุณ จากนั้นแชร์กับนักพัฒนาของ {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu dosyayı masaüstüne kaydedin, ardından {app_name}'in geliştiricileriyle paylaşın." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Збережіть цей файл на робочому столі, а потім розділіть його з розробниками {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اس فائل کو اپنے ڈیسک ٹاپ پر محفوظ کریں، پھر اسے {app_name} کے ڈویلپرز کے ساتھ شیئر کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ushbu faylni ish stolingizga saqlang, so'ngra {app_name} ishlab chiquvchilari bilan ulashing." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lưu tệp này vào máy tính để bàn của bạn, rồi chia sẻ nó với các nhà phát triển {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gcina le fayile kwi desktop yakho, uze uyibonise abaphuhlisi be-{app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "将此文件保存到您的桌面,然后与{app_name}开发者分享" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "將次文檔存儲至您的桌面,並分享與 {app_name} 的開發者。" + "value" : "Save this file, then share it with {app_name} developers." } } } @@ -234965,6 +236106,17 @@ } } }, + "helpTranslateSessionDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Help translate {app_name} into over 80 languages!" + } + } + } + }, "helpWedLoveYourFeedback" : { "extractionState" : "manual", "localizations" : { @@ -235926,537 +237078,159 @@ "hideMenuBarDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wissel lêerstelsel werkbalk sigbaarheid" - } - }, - "ar" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "تبديل رؤية شريط قائمة النظام" + "value" : "Toggle system menu bar visibility." } - }, + } + } + }, + "hideNoteToSelfDescription" : { + "extractionState" : "manual", + "localizations" : { "az" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem menyu çubuğunun görünməsini dəyişdir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "System menu bar čārāw visibility" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Рэгуляваць бачнасць сістэмнага меню" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Превключване на видимостта на системната лента с менюта" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "সিস্টেম মেনুবার দৃশ্যমানতা টগল করুন" + "value" : "Özünə Qeydi söhbət siyahınızdan gizlətmək istədiyinizə əminsinizmi?" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Commuta la visibilitat de la barra de menú del sistema" + "value" : "Estàs segur que vols amagar Nota a Si Mateix de la teva llista de converses?" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Přepínač viditelnosti lišty systémového menu" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toglo gwelededd bar dewislen system" + "value" : "Opravdu chcete skrýt Poznámku sobě ze svého seznamu konverzací?" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slå synlighed for systemmenulinjen til/fra" + "value" : "Er du sikker på, at du vil skjule Egen note fra din samtaleliste?" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Schalte die Sichtbarkeit der Menüleiste um" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Εναλλαγή προβολής γραμμής μενού συστήματος" + "value" : "Möchtest du Notiz an mich wirklich aus deiner Unterhaltungsliste ausblenden?" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Toggle system menu bar visibility" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ŝalti la videblecon de la sistemmenuo" + "value" : "Are you sure you want to hide Note to Self from your conversation list?" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar visibilidad de la barra de menú del sistema" + "value" : "¿Estás seguro de que quieres ocultar Nota Personal de tu lista de conversaciones?" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar visibilidad de la barra de menú del sistema" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaheta süsteemi menüüriba nähtavust" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sistemaren menu barraren ikusgarritasuna piztu/itzali" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغییر نمایش نوار منوی سیستم" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Valitse järjestelmän valikkopalkin näkyvyys" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "I-toggle ang pagkakakita ng menu bar ng system" + "value" : "¿Estás seguro de que quieres ocultar Nota Personal de tu lista de conversaciones?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Afficher ou masquer la barre du menu système" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canza hasken menu ɗin tsarin" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "החלף את מצב הסתרת סרגל התפריט של המערכת" + "value" : "Êtes-vous sûr de vouloir masquer Note pour soi-même de votre liste de conversations ?" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम मेनू बार दृश्यता टॉगल करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uključivanje vidljivosti trake izbornika sustava" + "value" : "क्या आप वाकई अपनी बातचीत सूची से अपने लिए नोट छुपाना चाहते हैं?" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A rendszer menüsor láthatóságának be-/kikapcsolása" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Միացնել համակարգի ընտրացանկի տեսանելիությունը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alihkan visibilitas bilah menu sistem" + "value" : "Biztosan el akarja rejteni a Jegyzet magamnak jegyzetet a beszélgetési listából?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mostra/Nascondi la barra delle impostazioni" + "value" : "Sei sicuro di voler nascondere Note to Self dalla tua lista di conversazioni?" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メニューバーの表示を切り替える" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "სისტემის მენიუს ხილვადობის გადართვა" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បិទបើកប្រព័ន្ធរបារម៉ឺនុយដែលអាចមើលឃើញ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸಿಸ್ಟಮ್ ಮೆನು ಬಾರ್ ದೃಶ್ಯಮಾನತೆಯನ್ನು ಷೇಮರಿಸಲು ಟೊರ್ನ್ ಕ್ರಿಯಲೆಗೆ" + "value" : "自分用メモを会話リストから非表示にしますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 메뉴 바를 끄거나 켜기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "چالاکی بینینی پەڕەی سیستەم" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xuyabûna darikê menuyê ya sîstemê veke/bigire" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ganda ebbanga ly’amakubo ery’ebikozesebwa ebyenfuna" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Perjungti sistemos meniu juostos matomumą" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вклучување на менито на системот" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Системийн цэсийн мөрний харагдах байдлыг тохируулах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Togol kebolehlihatan bar menu sistem" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စနစ် မီနှူးဘား၏ မြင်ကွင်းကို အတိအကျပြပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तन्त्र प्रणाली मेनु बार दृश्यता टगल गर्नुहोस्" + "value" : "정말로 대화 목록에서 개인용 메모를 숨기시겠습니까?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Zichtbaarheid systeemmenubalk in-/uitschakelen" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sintha kuonekera kwa system menu bar" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸਿਸਟਮ ਮੀਨੂ ਬਾਰ ਵਿਖਾਈ ਦੇਣ ਦੀ ਸਥਿਤੀ ਟੌਗਲ ਕਰੋ" + "value" : "Ben je zeker dat je Bericht aan Jezelf in je conversatie lijst wilt verbergen?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przełącz widoczność systemowego paska menu" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د سیستم مینو بار لیدنه وښایاست" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alternar visibilidade da barra de menu do sistema" + "value" : "Czy na pewno chcesz ukryć Moje notatki na liście konwersacji?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Alternar a visibilidade da barra de menu do sistema" + "value" : "Tem a certeza de que pretende ocultar a Nota Pessoal da sua lista de conversas?" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Comută vizibilitatea barei de meniu a sistemului" + "value" : "Ești sigur/ă că dorești să ascunzi Notă personală din lista ta de conversații?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Переключить видимость панели системного меню" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prebacivanje vidljivosti sistemske trake s izbornicima" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "පද්ධති මෙනු තීරු දෘශ්‍යතාව ටොගල් කරන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prepnúť viditeľnosť systémového panela ponuky" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preklopite vidnost sistemske menijske vrstice" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndryshoni dukshmërinë e shiritit të menusë së sistemit" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пребаци видљивост системске траке са менијем" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uključi / isključi vidljivost sistemskog menija" + "value" : "Вы уверены, что хотите скрыть Заметки для Себя из списка бесед?" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Växla synlighet för systemmenyraden" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badilisha muonekano wa upau wa menyu ya mfumo" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "சிஸ்டம் மெனு பட்டியின் காட்சியைக் காட்டு" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సిస్టమ్ మెనూ బార్ విజిబిలిటిని టాగిల్ చేయండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "สลับการแสดงผลแถบเมนูระบบ" + "value" : "Är du säker på att du vill gömma Notera till mig själv från din konversationslista?" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem menü çubuğu görünürlüğünü değiştirin" + "value" : "Kendime Not'u sohbet listenizden gizlemek istediğinizden emin misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Увімкнути або вимкнути видимість панелі меню" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "سسٹم مینو بار کی مرئیت کو ٹوگل کریں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tizim menyusi paneli koʻrinishini oʻzgartirish" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chuyển đổi sự hiển thị thanh trình đơn hệ thống" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guqula ukubonakalisa kwebar yeendlela zenkqubo" + "value" : "Ви впевнені, що хочете приховати Нотатку для себе зі свого списку розмов?" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "切换系统菜单栏可见性" + "value" : "你确定要从对话列表中隐藏Note to Self吗?" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "切換系統選單列可見性" - } - } - } - }, - "hideNoteToSelfDescription" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Özünə Qeydi söhbət siyahınızdan gizlətmək istədiyinizə əminsinizmi?" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols amagar Nota a Si Mateix de la teva llista de converses?" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete skrýt Poznámku sobě ze svého seznamu konverzací?" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil skjule Egen note fra din samtaleliste?" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to hide Note to Self from your conversation list?" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir masquer Note pour soi-même de votre liste de conversations ?" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan el akarja rejteni a Jegyzet magamnak jegyzetet a beszélgetési listából?" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말로 대화 목록에서 개인용 메모를 숨기시겠습니까?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ben je zeker dat je Bericht aan Jezelf in je conversatie lijst wilt verbergen?" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz ukryć Moje notatki na liście konwersacji?" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете приховати Нотатку для себе зі свого списку розмов?" + "value" : "您確定要將 小筆記 從您的對話清單中隱藏嗎?" } } } @@ -237470,6 +238244,18 @@ "value" : "bildoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "imágenes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "imágenes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -237494,6 +238280,18 @@ "value" : "gambar" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "immagini" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -237512,6 +238310,18 @@ "value" : "obrazy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "imagens" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "imagini" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -237524,6 +238334,12 @@ "value" : "bilder" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "resimler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -237541,6 +238357,12 @@ "state" : "translated", "value" : "图片" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片" + } } } }, @@ -239861,6 +240683,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待に失敗しました" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -240007,6 +240851,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "O convite falhou" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os convites falharam" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -240220,6 +241092,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請失敗" + } + } + } + } + } + } } } }, @@ -240624,6 +241518,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待を送信できませんでした。再試行しますか?" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -240770,6 +241686,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "O convite não pôde ser enviado. Gostaria de tentar novamente?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os convites não puderam ser enviados. Gostaria de tentar novamente?" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -240983,6 +241927,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法傳送邀請,您要再試一次嗎?" + } + } + } + } + } + } } } }, @@ -243432,6 +244398,18 @@ "value" : "Tiu ĉi grupo nun estas nurlega. Rekreu tiun ĉi grupon por daŭrigi babiladon." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Vuelve a crear este grupo para seguir chateando." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Vuelve a crear este grupo para seguir chateando." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243486,6 +244464,18 @@ "value" : "Ta grupa jest teraz tylko do odczytu. Odtwórz grupę, żeby dalej rozmawiać." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo está agora apenas em leitura. Recrie este grupo para continuar a conversar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup este acum doar pentru citire. Recreează grupul pentru a continua conversația." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243527,6 +244517,12 @@ "state" : "translated", "value" : "此群组目前为只读状态。请重新创建此群组以继续聊天。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組目前為唯讀。請重新建立群組以繼續聊天。" + } } } }, @@ -243581,6 +244577,18 @@ "value" : "Tiu ĉi grupo nun estas nurlega. Petu administranton de la grupo rekrei tiun ĉi grupon por daŭrigi babiladon." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Pide al administrador del grupo que vuelva a crear este grupo para seguir chateando." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Pide al administrador del grupo que vuelva a crear este grupo para seguir chateando." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243635,6 +244643,18 @@ "value" : "Ta grupa jest teraz tylko do odczytu. Zapytaj administratora grupy, aby odtworzył grupę, żeby dalej rozmawiać." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo está agora apenas em leitura. Peça ao administrador do grupo para recriar este grupo e continuar a conversar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup este acum doar pentru citire. Cere administratorului grupului să recreeze acest grup pentru a continua conversația." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243676,6 +244696,12 @@ "state" : "translated", "value" : "此群组目前为只读状态。请要求管理员重新创建此群组以继续聊天。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組目前為唯讀。請要求群組管理員重新建立此群組以繼續聊天。" + } } } }, @@ -243724,6 +244750,18 @@ "value" : "Grupoj estis plibonigitaj! Rekreu tiun ĉi grupon por plibonigi la fidindecon. Tiu ĉi grupo fariĝos nurlega je {date}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Vuelve a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Vuelve a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243778,6 +244816,18 @@ "value" : "Grupy zostały ulepszone! Odtwórz tę grupę dla większej niezawodności. Ta grupa będzie tylko do odczytu od {date}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os grupos foram atualizados! Recrie este grupo para melhorar a fiabilidade. Este grupo passará a estar apenas em leitura em {date}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupurile au fost actualizate! Recreează acest grup pentru o fiabilitate îmbunătățită. Acest grup va deveni doar pentru citire la data de {date}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243813,6 +244863,12 @@ "state" : "translated", "value" : "群组已升级!请重新创建此群组以提高可靠性。此群组将于{date}变为只读状态。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已升級!請重新建立此群組以提升穩定性。本群組將於 {date} 起變為唯讀。" + } } } }, @@ -243861,6 +244917,18 @@ "value" : "Grupoj estis plibonigitaj! Petu administranton de la grupo rekrei la grupon por plibonigi la fidindecon. Tiu ĉi grupo fariĝos nurlega je {date}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Pide al administrador del grupo que vuelva a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Pide al administrador del grupo que vuelva a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243915,6 +244983,18 @@ "value" : "Grupy zostały ulepszone! Zapytaj administratora grupy, aby odtworzył tę grupę dla większej niezawodności. Ta grupa będzie tylko do odczytu od {date}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os grupos foram atualizados! Peça ao administrador do grupo para recriar este grupo e melhorar a fiabilidade. Este grupo passará a estar apenas em leitura em {date}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupurile au fost actualizate! Cere administratorului să recreeze acest grup pentru o fiabilitate îmbunătățită. Acest grup va deveni doar pentru citire la data de {date}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243950,6 +245030,12 @@ "state" : "translated", "value" : "群组已升级!请要求群组管理员重新创建此群组以提高可靠性。此群组将于{date}变为只读状态。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已升級!請請求群組管理員重新建立此群組以提升穩定性。本群組將於 {date} 起變為唯讀。" + } } } }, @@ -244004,6 +245090,18 @@ "value" : "Historio de babilejo ne estos transportita al la nova grupo. Vi ankoraŭ povas vidi tutan historion de la babilado en via malnova grupo." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El historial de chat no se transferirá al nuevo grupo. Todavía puedes ver todo el historial de chat en tu grupo antiguo." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El historial de chat no se transferirá al nuevo grupo. Todavía puedes ver todo el historial de chat en tu grupo antiguo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -244058,6 +245156,18 @@ "value" : "Historia czatu nie będzie przeniesiona do nowej grupy. Możesz wciąż zobaczyć całą historię czatu w swojej starej grupie." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O histórico da conversa não será transferido para o novo grupo. Ainda poderá ver todo o histórico no seu grupo antigo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Istoricul conversațiilor nu va fi transferat în noul grup. Totuși, poți vizualiza în continuare întreg istoricul în grupul tău vechi." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -244099,6 +245209,12 @@ "state" : "translated", "value" : "聊天记录被不会转移到新群组。您仍然可以查看旧群组中的所有聊天记录。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "聊天記錄不會轉移到新群組,您仍可在舊群組中查看所有聊天記錄。" + } } } }, @@ -250323,7 +251439,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Keçid önizləmələri göndərərkən tam metadata qorumasına sahib olmayacaqsınız." + "value" : "Keçid önizləmələri göndərərkən tam meta veri qorumasına sahib olmayacaqsınız." } }, "bal" : { @@ -257535,6 +258651,12 @@ "value" : "Administrer medlemmer" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mitglieder verwalten" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -257547,12 +258669,30 @@ "value" : "Administri membrojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar miembros" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar miembros" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer Membres" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सदस्यों का प्रबंधन करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -257565,6 +258705,18 @@ "value" : "Atur Anggota" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestisci membri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メンバーの管理" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -257583,6 +258735,36 @@ "value" : "Zarządzaj członkami" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerir membros" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionează membri" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управление участниками" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hantera medlemmar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üyeleri Yönet" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -257594,6 +258776,12 @@ "state" : "translated", "value" : "管理成员" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理成員" + } } } }, @@ -263512,7 +264700,7 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Meghívás küldése" + "value" : "Meghívó küldése" } }, "other" : { @@ -267816,6 +269004,17 @@ } } }, + "menuBar" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Bar" + } + } + } + }, "message" : { "extractionState" : "manual", "localizations" : { @@ -268298,6 +269497,12 @@ "messageBubbleReadMore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çox oxu" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -268310,23 +269515,130 @@ "value" : "Více" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weiterlesen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Read more" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leer más" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leer más" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lire plus" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "और पढ़ें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Tudjon meg többet" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leggi di più" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "続きを読む" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lees meer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeczytaj więcej" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ler mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citește mai mult" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Читать далее" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Läs mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devamını oku" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Читати далі" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解更多" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "閱讀更多" + } + } + } + }, + "messageCopy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy Message" + } } } }, @@ -268881,7 +270193,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichtenübermittlung gescheitert" + "value" : "Diese Nachricht konnte nicht zugestellt werden" } }, "el" : { @@ -277353,6 +278665,24 @@ } } }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem uma nova mensagem em {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem %lld novas mensagens em {group_name}." + } + } + } + } + }, "ro" : { "variations" : { "plural" : { @@ -277496,6 +278826,18 @@ } } } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "您在 {group_name} 中收到 %lld 則新訊息。" + } + } + } + } } } }, @@ -277981,6 +279323,12 @@ "messageRequestDisabledToastAttachments" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj Tələbiniz qəbul edilənə qədər qoşma göndərə bilməzsiniz" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -277993,24 +279341,60 @@ "value" : "Dokud není vaše žádost o komunikaci přijata, nemůžete posílat přílohy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst keine Anhänge versenden, bis deine Nachrichtanfrage akzeptiert wurde" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You cannot send attachments until your Message Request is accepted" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar archivos adjuntos hasta que se acepte tu solicitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar archivos adjuntos hasta que se acepte tu solicitud de mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vous ne pouvez pas envoyer de pièces jointes tant que votre demande de message n'est pas acceptée" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब तक आपका संदेश अनुरोध स्वीकार नहीं किया जाता, आप अटैचमेंट नहीं भेज सकते" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Addig nem küldhet mellékleteket, amíg az üzenetkérelmét el nem fogadják" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è possibile inviare allegati finché la richiesta di messaggio non sarà accettata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージリクエストが承認されるまで添付ファイルを送信できません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -278029,6 +279413,36 @@ "value" : "Nie można wysyłać załączników, dopóki prośba o wiadomość nie zostanie zaakceptowana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não é possível enviar anexos até que o seu Pedido de Mensagem seja aceite" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu poți trimite atașamente până când cererea de mesaj nu este acceptată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы не можете отправлять вложения, пока ваш запрос на сообщение не будет принят" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kan inte skicka bilagor förrän din meddelandeförfrågan har godkänts" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj İsteğiniz kabul edilene kadar ek gönderemezsiniz" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -278040,6 +279454,18 @@ "state" : "translated", "value" : "Bạn không thể gửi tệp đính kèm cho đến khi tin nhắn chờ của bạn được chấp nhận" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的消息请求被接受之前,你无法发送附件" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的訊息請求被接受之前,您無法傳送附件" + } } } }, @@ -278052,6 +279478,12 @@ "value" : "لا يمكنك إرسال رسائل صوتية حتى يتم قَبُول طلب الرسالة الخاص بك" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj Tələbiniz qəbul edilənə qədər səsli mesaj göndərə bilməzsiniz" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -278064,24 +279496,60 @@ "value" : "Dokud není vaše žádost o komunikaci přijata, nemůžete posílat hlasové zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst keine Sprachnachrichten senden, bis deine Nachrichtanfrage akzeptiert wurde" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You cannot send voice messages until your Message Request is accepted" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar mensajes de voz hasta que se acepte tu solicitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar mensajes de voz hasta que se acepte tu solicitud de mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vous ne pouvez pas envoyer de messages vocaux tant que votre demande de message n'est pas acceptée" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब तक आपका संदेश अनुरोध स्वीकार नहीं किया जाता, आप वॉयस संदेश नहीं भेज सकते" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Addig nem küldhet hangüzeneteket, amíg az üzenetkérelmét el nem fogadják" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è possibile inviare messaggi vocali finché la richiesta di messaggio non sarà accettata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージリクエストが承認されるまで音声メッセージを送信できません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -278100,6 +279568,36 @@ "value" : "Nie można wysyłać wiadomości głosowych, dopóki prośba o wiadomość nie zostanie zaakceptowana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não é possível enviar mensagens de voz até que o seu Pedido de Mensagem seja aceite" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu poți trimite mesaje vocale până când cererea de mesaj nu este acceptată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы не можете отправлять голосовые сообщения, пока ваш запрос на сообщение не будет принят" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kan inte skicka röstmeddelanden förrän din meddelandeförfrågan har godkänts" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj İsteğiniz kabul edilene kadar sesli mesaj gönderemezsiniz" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -278111,6 +279609,18 @@ "state" : "translated", "value" : "Bạn không thể gửi tin nhắn thoại cho đến khi tin nhắn chờ của bạn được chấp nhận" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的消息请求被接受之前,您无法发送语音消息" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的訊息請求被接受之前,您無法傳送語音訊息" + } } } }, @@ -281485,7 +282995,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Cəmiyyət Mesaj Tələbləri" + "value" : "İcma mesaj tələbləri" } }, "bal" : { @@ -282440,6 +283950,12 @@ "messageRequestsContactDelete" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj tələbini və əlaqəli kontaktı silmək istədiyinizə əminsiniz?" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -282452,6 +283968,12 @@ "value" : "Opravdu chcete smazat tuto žádost o komunikaci a s ní spojený kontakt?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du diese Nachrichtenanfrage und den zugehörigen Kontakt löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -282464,29 +283986,113 @@ "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝpeton kaj la asociitan kontakton?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar esta solicitud de mensaje y el contacto asociado?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar esta solicitud de mensaje y el contacto asociado?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Êtes-vous sûr de vouloir supprimer cette demande de message ainsi que le contact associé ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस संदेश अनुरोध और संबंधित संपर्क को हटाना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törölni szeretné ezt az üzenetkérést és a hozzá tartozó kapcsolatot?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare questa richiesta di messaggio e il contatto associato?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージリクエストおよび関連する連絡先を本当に削除してもよろしいですか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정말로 이 메시지 요청과 연결된 연락처를 삭제하시겠습니까?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je dit berichtverzoek en de bijbehorende contactpersoon wilt verwijderen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy na pewno chcesz usunąć to żądanie wiadomości i powiązany z nim kontakt?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza que pretende eliminar este pedido de mensagem e o contacto associado?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigur doriți să ștergeți această solicitare de mesaj și contactul asociat?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить этот запрос на сообщение и связанный с ним контакт?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill ta bort denna message request och tillhörande kontakt?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj isteğini ve ilişkili kişiyi silmek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви дійсно хочете видалити цей запит на надіслання повідомлення та пов'язаний з ним контакт?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要删除此消息请求和关联的联系人吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除此訊息請求以及相關聯的聯絡人嗎?" + } } } }, @@ -291091,6 +292697,24 @@ "modalMessageCharacterDisplayDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajların {limit} xarakter limiti var. %lld xarakteriniz qaldı." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajların {limit} xarakter limiti var. %lld xarakteriniz qaldı." + } + } + } + } + }, "ca" : { "variations" : { "plural" : { @@ -291139,6 +292763,24 @@ } } }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten haben ein Zeichenlimit von {limit} Zeichen. Du hast noch %lld Zeichen." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten haben ein Zeichenlimit von {limit} Zeichen. Du hast noch %lld Zeichen." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -291157,6 +292799,78 @@ } } }, + "es-419" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te queda %lld carácter." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te quedan %lld caracteres." + } + } + } + } + }, + "es-ES" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te queda %lld carácter." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te quedan %lld caracteres." + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les messages ont une limite de {limit} caractères. Il vous reste encore %lld caractères" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les messages ont une limite de {limit} caractères. Il vous reste %lld caractères." + } + } + } + } + }, + "hi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेशों की अक्षर सीमा {limit} वर्ण है। आपके पास %lld वर्ण शेष हैं।" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेशों की अक्षर सीमा {limit} वर्ण है। आपके पास %lld वर्ण शेष हैं।" + } + } + } + } + }, "hu" : { "variations" : { "plural" : { @@ -291175,6 +292889,24 @@ } } }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi hanno un limite di {limit} caratteri. Hai ancora %lld carattere." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi hanno un limite di {limit} caratteri. Hai ancora %lld caratteri." + } + } + } + } + }, "ja" : { "variations" : { "plural" : { @@ -291186,12 +292918,228 @@ } } } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten hebben een limiet van {limit} tekens. Je hebt nog %lld teken over." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten hebben een limiet van {limit} tekens. Je hebt nog %lld tekens over." + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaki. Pozostały %lld znaki." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaków. Pozostało %lld znaków." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znak. Pozostał %lld znak." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaków. Pozostało %lld znaków." + } + } + } + } + }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens têm um limite de {limit} caracteres. Resta %lld caractere." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens têm um limite de {limit} caracteres. Restam %lld caracteres." + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld caractere disponibile." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld caracter disponibil." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld de caractere disponibile." + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символа." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символов." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Остался %lld символ." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символов." + } + } + } + } + }, + "sv-SE" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden har en teckengräns på {limit} tecken. Du har %lld tecken kvar." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden har en gräns på {limit} tecken. Du har %lld tecken kvar." + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {limit} karakter ile sınırlıdır. %lld karakteriniz kaldı." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {limit} karakter ile sınırlıdır. %lld karakteriniz kaldı." + } + } + } + } + }, + "uk" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишився %lld символ" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + } + } + } + }, + "zh-CN" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息的字符限制为 {limit} 个字符。您还剩 %lld 个字符。" + } + } + } + } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息最多限制 {limit} 個字元。您還剩下 %lld 個字元可以使用。" + } + } + } + } } } }, "modalMessageCharacterDisplayTitle" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj uzunluğu" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291204,29 +293152,131 @@ "value" : "Délka zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichtenlänge" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Message Length" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud del mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longitud del mensaje" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longueur du message" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश की लंबाई" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Üzenet hossza" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lunghezza del messaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージの長さ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichtlengte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Długość wiadomości" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprimento da Mensagem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lungimea mesajului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Длина Сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelandelängd" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj Uzunluğu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Довжина повідомлення" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息长度" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息長度" + } } } }, "modalMessageCharacterTooLongDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj üçün xarakter limitini aşmısınız. Lütfən mesajınızı {limit} xarakter və ya daha az qədər qısaldın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291239,29 +293289,131 @@ "value" : "Překročili jste limit počtu znaků pro tuto zprávu. Zkraťte prosím svou zprávu na {limit} znaků nebo méně." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast das Zeichenlimit für diese Nachricht überschritten. Bitte kürze deine Nachricht auf {limit} Zeichen oder weniger." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has superado el límite de caracteres para este mensaje. Por favor, acorta tu mensaje a {limit} caracteres o menos." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has superado el límite de caracteres para este mensaje. Por favor, acorta tu mensaje a {limit} caracteres o menos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez dépassé la limite pour ce message. Merci de raccourcir votre message à {limit} caractères ou moins." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपने इस संदेश के लिए वर्ण सीमा को पार कर लिया है। कृपया अपने संदेश को {limit} वर्णों या कम में छोटा करें।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenet karakterszáma túllépte a megadott. Rövidítse le az üzenetet {limit} karakterekre vagy kevesebbre." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hai superato il limite di caratteri per questo messaggio. Riduci il tuo messaggio a {limit} caratteri o meno." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージは文字数制限を超えています。{limit}文字以内に短くしてください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt de tekenlimiet voor dit bericht overschreden. Verkort je bericht tot {limit} tekens of minder." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przekroczono limit znaków dla tej wiadomości. Skróć wiadomość do {limit} znaków lub mniej." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Excedeu o limite de caracteres para esta mensagem. Por favor, reduza a sua mensagem para {limit} caracteres ou menos." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ai depășit limita de caractere pentru acest mesaj. Te rugăm să scurtezi mesajul la {limit} caractere sau mai puțin." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы превысили лимит символов в сообщении. Пожалуйста, сократите сообщение до {limit} символов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har överskridit teckengränsen för detta meddelande. Förkorta ditt meddelande till {limit} tecken eller färre." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj için karakter sınırını aştınız. Lütfen mesajınızı {limit} karakter veya daha az olacak şekilde kısaltın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви перевищили максимальну кількість символів для цього повідомлення. Будь ласка, скоротіть ваше повідомлення до {limit} символів або менше." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已超出此消息的字符限制。请将消息缩短至 {limit} 个字符或更少。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已超過此訊息的字元限制。請將您的訊息縮短至 {limit} 個字元或更少。" + } } } }, "modalMessageCharacterTooLongTitle" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çox uzundur" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291274,29 +293426,131 @@ "value" : "Zpráva je příliš dlouhá" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht zu lang" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Message Too Long" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje demasiado largo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje demasiado largo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le message est trop long" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश बहुत लंबा है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenet túl hosszú" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messaggio troppo lungo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージが長すぎます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bericht te lang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomość jest za długa" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensagem muito longa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj prea lung" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщение слишком длинное" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelandet är för långt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çok uzun" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Задовге повідомлення" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息太长" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息過長" + } } } }, "modalMessageTooLongDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən mesajınızı {limit} xarakter və ya daha az qədər qısaldın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291309,29 +293563,131 @@ "value" : "Zkraťte prosím svou zprávu na {limit} znaků nebo méně." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte kürze deine Nachricht auf {limit} Zeichen oder weniger." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Please shorten your message to {limit} characters or less." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, acorta tu mensaje a {limit} caracteres o menos." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, acorta tu mensaje a {limit} caracteres o menos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez raccourcir votre message a {limit} caractères ou moins." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया अपने संदेश को {limit} वर्णों या कम में छोटा करें।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Rövidítse le az üzenetét {limit} karakterekre vagy kevesebbre." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riduci il tuo messaggio a {limit} caratteri o meno." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを{limit}文字以内に短くしてください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verkort je bericht tot {limit} tekens of minder." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróć wiadomość do {limit} znaków lub mniej." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, reduza a sua mensagem para {limit} caracteres ou menos." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să scurtezi mesajul la {limit} caractere sau mai puțin." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, сократите свое сообщение до {limit} символов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förkorta ditt meddelande till {limit} tecken eller färre." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen mesajınızı {limit} karakter veya daha az olacak şekilde kısaltın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Будь ласка, скоротіть повідомлення до {limit} символів або менше." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请将消息缩短至 {limit} 个字符或更少。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請將您的訊息縮短至 {limit} 個字元或更少。" + } } } }, "modalMessageTooLongTitle" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çox uzundur" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291344,23 +293700,119 @@ "value" : "Zpráva je příliš dlouhá" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht zu lang" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Message Too Long" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje demasiado largo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensaje demasiado largo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le message est trop long" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश बहुत लंबा है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenet túl hosszú" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messaggio troppo lungo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージが長すぎます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bericht te lang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomość jest za długa" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensagem muito longa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj prea lung" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщение слишком длинное" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelandet är för långt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çok uzun" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Задовге повідомлення" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息太长" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息過長" + } } } }, @@ -291894,6 +294346,12 @@ "value" : "Trieu un sobrenom per a {name}. Això us apareixerà a les vostres converses individuals i de grup." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyberte přezdívku pro {name}. Zobrazí se vám v konverzacích jeden na jednoho a ve skupinách." + } + }, "cy" : { "stringUnit" : { "state" : "translated", @@ -297333,6 +299791,17 @@ } } }, + "notificationDisplay" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notification Display" + } + } + } + }, "notificationsAllMessages" : { "extractionState" : "manual", "localizations" : { @@ -299723,7 +302192,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "仅显示发送者" + "value" : "仅显示发送者名称" } }, "zh-TW" : { @@ -300213,6 +302682,28 @@ } } }, + "notificationSenderNameAndPreview" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display the sender's name and a preview of the message content." + } + } + } + }, + "notificationSenderNameOnly" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display only the sender's name without any message content." + } + } + } + }, "notificationsFastMode" : { "extractionState" : "manual", "localizations" : { @@ -301216,6 +303707,18 @@ "value" : "You'll be notified of new messages reliably and immediately using Huawei’s notification servers." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Huawei." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Huawei." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -301234,6 +303737,18 @@ "value" : "A Huawei értesítési kiszolgálóinak segítségével megbízhatóan és azonnal értesítést fog kapni az új üzenetekről." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riceverai notifiche di nuovi messaggi in modo affidabile e immediato utilizzando i server di notifica di Huawei." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huaweiの通知サーバーを使用することで、新しいメッセージの通知を即時かつ確実に受け取ることができます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -301252,6 +303767,18 @@ "value" : "Będziesz otrzymywać powiadomienia o nowych wiadomościach niezawodnie i natychmiastowo, korzystając z serwerów powiadomień Huawei." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Será notificado de novas mensagens de forma fiável e imediata usando os servidores de notificação da Huawei." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vei fi notificat în legătură cu noile mesaje imediat și în mod fiabil folosind serverele de notificări Huawei." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -301264,6 +303791,12 @@ "value" : "Du kommer att meddelas om nya meddelanden på ett tillförlitligt sätt och omedelbart genom att använda Huawei’s aviseringsservrar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huawei'nin bildirim sunucuları kullanılarak yeni mesajlardan güvenilir bir şekilde ve anında haberdar edileceksiniz." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -301275,6 +303808,12 @@ "state" : "translated", "value" : "您将会收到由华为的通知服务器发出的即时可靠的新消息通知。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您將會透過華為的通知伺服器即時且可靠地收到新訊息通知。" + } } } }, @@ -301757,6 +304296,17 @@ } } }, + "notificationsGenericOnly" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display a generic {app_name} notification without the sender's name or message content." + } + } + } + }, "notificationsGoToDevice" : { "extractionState" : "manual", "localizations" : { @@ -303655,7 +306205,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{name} {conversation_name} üçün" + "value" : "{name} > {conversation_name}" } }, "bal" : { @@ -305080,6 +307630,17 @@ } } }, + "notificationsMakeSound" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play a sound when you receive receive new messages." + } + } + } + }, "notificationsMentionsOnly" : { "extractionState" : "manual", "localizations" : { @@ -307502,6 +310063,12 @@ "value" : "Notifikationer på pause i {time_large}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stummgeschaltet für {time_large}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -307514,12 +310081,30 @@ "value" : "Silentigita por {time_large}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Muet pour {time_large}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} के लिए म्यूट किया गया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -307532,6 +310117,18 @@ "value" : "Senyapkan selama {time_large}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenzia per {time_large}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} 間ミュート" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -307550,6 +310147,36 @@ "value" : "Wyciszony przez {time_large}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silențios pentru {time_large}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отключено на {time_large}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tysta i {time_large}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} süresince sessize alındı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -307567,6 +310194,12 @@ "state" : "translated", "value" : "已设置免打扰{time_large}" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "已靜音 {time_large}" + } } } }, @@ -307579,6 +310212,12 @@ "value" : "كتم حتى {date_time}" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} qədər səssizdə" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -307591,6 +310230,12 @@ "value" : "Ztlumeno do {date_time}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stummgeschaltet bis {date_time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -307603,12 +310248,30 @@ "value" : "Silentigita ĝis {date_time}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado hasta {date_time}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado hasta {date_time}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Muet jusqu'à {date_time}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} तक मौन किया गया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -307621,6 +310284,18 @@ "value" : "Senyapkan sampai {date_time}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenzioso fino alle {date_time}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time}までミュート" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -307639,11 +310314,53 @@ "value" : "Wyciszony do {date_time}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado até {date_time}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amuțit până la {date_time}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звук отключен {date_time}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tystades till {date_time}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} tarihine kadar sessize alındı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Стишено до {date_time}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "已禁言至 {date_time}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜音通知,直到 {date_time}" + } } } }, @@ -326355,11 +329072,125 @@ "openSurvey" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anketi aç" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enquesta oberta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřít dotazník" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umfrage starten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open Survey" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir encuesta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir encuesta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir le questionnaire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सर्वेक्षण खोलें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri sondaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アンケートを開く" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enquête openen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz ankietę" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir questionário" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deschide sondajul" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открытый опрос" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öppna undersökning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пройти опитування" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开调查问卷" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟問卷" + } } } }, @@ -326842,6 +329673,17 @@ } } }, + "password" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + } + } + }, "passwordChange" : { "extractionState" : "manual", "localizations" : { @@ -328779,484 +331621,10 @@ "passwordCreate" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skep jou wagwoord" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إنشاء كلمة سر" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolunuzu yaradın" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنی رمز بناؤ" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стварыць пароль" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Създай своя парола" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড তৈরি করুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea la teva contrasenya" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vytvořte si heslo" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creu eich cyfrinair" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opret din adgangskode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passwort erstellen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Δημιουργήστε τον κωδικό σας πρόσβασης" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Create your password" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreu vian pasvorton" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea tu contraseña" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea tu contraseña" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loo oma parool" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sortu zure pasahitza" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغییر گذرواژه" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Luo salasana" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lumikha ng password mo" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Créez votre mot de passe" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crear contrasinal" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ƙirƙiri kalmar sirrinka" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "צור סיסמה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपना पासवर्ड बनाएं" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izradite lozinku" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó létrehozása" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ստեղծել գաղտնաբառ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buat kata sandi Anda" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea password" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを作成してください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შექმენით თქვენი პაროლი" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បង្កើតពាក្យសម្ងាត់របស់អ្នក" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ರಚಿಸಿ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 만들기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تێپەڕوشەکەت دروست بکە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya xwe çêke" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kilira akakufulu ko" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ເກີງແທ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sukurti slaptažodį" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izveidot savu paroli" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Креирај ја твојата лозинка" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нууц үгээ оруулах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cipta kata laluan anda" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စကားဝှက်ကိုဖန်တီးကာ ပြုလုပ်ပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet ditt" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको पासवर्ड बनाउनुहोस्" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maak je wachtwoord aan" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet ditt" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pangani mawu achinsinsi anu" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਪਣਾ ਪਾਸਵਰਡ ਬਨਾਓ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utwórz hasło" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "حساب جوړول سمدستي، وړیا او بې نومه دی" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crie a sua senha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crie a sua palavra-passe" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creează-ți parola" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Создать пароль" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreiraj lozinku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය සාදන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vytvorte si heslo" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ustvari svoje geslo" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krijo fjalëkalimin tënd" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Креирајте вашу лозинку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreiraj svoju lozinku" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skapa ditt lösenord" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unda nenosiri lako" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் கடவுச்சொல்லை உருவாக்குக" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ సృష్టించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "Create your password" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolanızı oluşturun" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Створіть пароль" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنا پاس ورڈ بنائیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolni yarating" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tạo mật khẩu của bạn" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yenza iphasiwedi yakho" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "创建您的密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "建立你的密碼" + "value" : "Create Password" } } } @@ -329743,478 +332111,10 @@ "passwordDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vereis wagwoord om {app_name} oop te maak." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يتطلب كلمة السر لفتح {app_name}." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmaq üçün parol tələb et." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ درکار و بند را {app_name}." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неабходны пароль для разблакіроўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изисквайте парола за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে পাসওয়ার্ড প্রয়োজন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requereix contrasenya per a desbloquejar {app_name}." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžadovat heslo k odemknutí {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Angen cyfrinair i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kræv adgangskode for at låse {app_name} op." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zum Entsperren von {app_name} ist Passwort erforderlich." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Να απαιτείται κωδικός πρόσβασης για το ξεκλείδωμα του {app_name}." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Require password to unlock {app_name}." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Postuli pasvorton por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesita contraseña para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se requiere contraseña para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nõutav parool {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eskatu pasahitza {app_name} desblokeatzeko." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "برای بازشدن قفل {app_name} به رمز نیاز است." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaadi {app_name}in avaukseen salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nangangailangan ng password para i-unlock ang {app_name}." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mot de passe requis pour déverrouiller {app_name}." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requerir contrasinal para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buƙatar kalmar sirri don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "דרוש סיסמה לביטול נעילת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} को अनलॉक करने के लिए पासवर्ड की आवश्यकता है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtijevaj lozinku za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó szükséges {app_name} feloldásához." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Պահանջվում է գաղտնաբառ՝ {app_name}-ը ապակողպելու համար:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memerlukan kata sandi untuk membuka kunci {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiede la password per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} のロックを解除するにはパスワードが必要です" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "მოთხოვნათა პაროლის აპლიკაციის განბლოკვისათვის {app_name}." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "តម្រូវឲ្យមានពាក្យសម្ងាត់ដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಅನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಲು ಪಾಸ್ವರ್ಡ್ ಅಗತ್ಯವಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제에 비밀번호가 필요합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "پێویستە تێپەڕەوشە بکرێتە کار {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya ku vekirinîna {app_name} lazim bike." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tegeka okulaba nte ebeera na password eri okukuta {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reikalingas slaptažodis, kad atrakintumėte {app_name}." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nepieciešama parole, lai atbloķētu {app_name}." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Бара лозинка за отклучување на {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}-ыг нээхийн тулд нууц үг шаардана." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memerlukan kata laluan untuk membuka kunci {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ကို လော့ခ်ဖွင့်ရန် စကားဝှက် လိုအပ်သည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låse opp {app_name}." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låse opp {app_name}." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} अनलक गर्न पासवर्ड आवश्यक छ।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wachtwoord vereisen om {app_name} te ontgrendelen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funsani achinsinsi kuti mutsegule {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਣਲੌਕ ਕਰਨ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ ਜੀ." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wymagaj hasła, aby odblokować aplikację {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} خلاصول لپاره پاسورډ لازمي دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exigir senha para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitar palavra-passe para desbloquear {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesită parolă pentru a debloca {app_name}." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Требовать пароль для разблокировки {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtevaj lozinku za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීම සඳහා මුරපදයක් අවශ්‍ය වේ." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžadovať heslo na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtevaj geslo za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kërko fjalëkalimin për të zhbllokuar {app_name}-in." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај лозинку за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Potrebna je lozinka da otključate {app_name}." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kräv lösenord för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "omba nywila kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} இல்லாமல் திறக்க கடவுச்சொல்லைஉடன் வேண்டுகின்றேன்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ని అన్లాక్ చేయడానికి పాస్వర్డ్ అవసరం." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "จำเป็นต้องใช้รหัสผ่านในการปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} uygulamasının kilidini açmak için şifre iste." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вимагати пароль для розблокування {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے پاس ورڈ درکار ہے۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ni ochish uchun parolni talab qilish." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yêu cầu mật khẩu để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ifuna iphaswedi ukuvula {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要设置密码以解锁{app_name}。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要密碼才能解鎖 {app_name}。" + "value" : "Require password to unlock {app_name} on startup." } } } @@ -332138,11 +334038,113 @@ "passwordErrorLength" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parol, {min} ilə {max} xarakter uzunluğunda olmalıdır" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "La contrasenya ha d'estar entre {min} i {max} caràcters" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heslo musí mít od {min} do {max} znaků" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Passwort muss zwischen {min} und {max} Zeichen lang sein" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Password must be between {min} and {max} characters long" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "La contraseña debe tener entre {min} y {max} caracteres de longitud" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "La contraseña debe tener entre {min} y {max} caracteres de longitud" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le mot de passe doit contenir entre {min} et {max} caractères" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पासवर्ड की लंबाई {min} से {max} वर्णों के बीच होनी चाहिए" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La password deve essere lunga tra {min} e {max} caratteri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワードの長さを{min}文字から{max}文字にしてください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord moet tussen de {min} en {max} tekens lang zijn" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasło musi zawierać od {min} do {max} znaków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A palavra-passe deve ter entre {min} e {max} carateres" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parola trebuie să aibă între {min} și {max} caractere." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösenordet måste vara mellan {min} och {max} tecken långt" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "密码长度必须在{min}到{max}个字符之间" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "密碼必須介於 {min} 到 {max} 個字元之間" + } } } }, @@ -333583,6 +335585,17 @@ } } }, + "passwordNewConfirm" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm New Password" + } + } + } + }, "passwordRemove" : { "extractionState" : "manual", "localizations" : { @@ -334544,478 +336557,21 @@ "passwordRemoveDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwyder die wagwoord wat nodig is om {app_name} oop te maak." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إزالة كلمة السر المطلوبة لفتح {app_name}." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu sil." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ برس ک و بندروی {app_name} لایا وانتگ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выдаліце пароль, неабходны для разблакіроўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Премахнете паролата, необходима за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে প্রয়োজনীয় পাসওয়ার্ড সরান।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elimina la contrasenya necessària per desbloquejar {app_name}." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odebrat heslo pro odemykání {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tynnu'r cyfrinair sydd ei angen i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern adgangskoden, der kræves for at låse {app_name} op." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Passwort zum Entsperren von {app_name} entfernen." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αφαίρεση του κωδικού πρόσβασης που απαιτείται για το ξεκλείδωμα του {app_name}." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Remove the password required to unlock {app_name}." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forigi la pasvorton necesan por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar la contraseña necesaria para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar la contraseña requerida para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eemalda parool, mis on vajalik {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pasahitza kendu {app_name} desblokeatzeko beharrezkoa dena." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور مورد نیاز برای باز کردن {app_name} را حذف کن." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poista {app_name} avaukseen tarvittava salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alisin ang password na kinakailangan para i-unlock ang {app_name}." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Retirer le mot de passe requis pour déverrouiller {app_name}." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elimina o contrasinal necesario para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cire kalmar sirrin da ake buƙata don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסר את הסיסמה הנדרשת לביטול נעילת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड हटाएं जो {app_name} को अनलॉक करने के लिए आवश्यक है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uklonite lozinku potrebnu za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Távolítsd el a {app_name} alkalmazás jelszavát." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Փոխեք {app_name}-ն ապակողպելու համար պահանջվող գաղտնաբառը:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hapus kata sandi yang diperlukan untuk membuka kunci {app_name}." + "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rimuovi la password richiesta per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} のロックを解除するために必要なパスワードを削除します" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის მოხსნა {app_name}'ის განბლოკვისათვის." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្ដូរពាក្យសម្ងាត់ដែលបានតម្រូវឲ្យមានដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಅನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಲು ಅಗತ್ಯವಿರುವ ಪಾಸ್ವರ್ಡ್ ತೆಗೆದುಹಾಕಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제 시 필요한 비밀번호를 제거합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "لابردنی تێپەڕەوشەی پێویست بۆ کردنەوەی {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya ku ji bo vekirina qefila {app_name} lazim e rake." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ggyawo akatambi okwetengerera {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pašalinti slaptažodį, reikalingą {app_name} atrakinti." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noņemt paroli, lai atbloķētu {app_name}." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отстранете ја лозинката потребна за отклучување на {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}-ийг нээхэд шаардлагатай нууц үгийг устгах." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alih Keluar kata laluan yang diperlukan untuk membuka kunci {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ကိုလော့ခ်ဖွင့်ရန် လိုအပ်သော စကားဝှက်ကို ဖယ်ရှားပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern passordet som kreves for å låse opp {app_name}." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern passordet som kreves for å låse opp {app_name}." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} अनलक गर्न आवश्यक पासवर्ड हटाउनुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijder het wachtwoord dat nodig is om {app_name} te ontgrendelen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern passordet nødvendig for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chotsani achinsinsi omwe amafunika kutsegula {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਨਲੌਕ ਕਰਨ ਲਈ ਲੋੜੀਂ ਦਾ ਪਾਸਵਰਡ ਹਟਾਓ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuń hasło wymagane do odblokowania aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "هغه پاسورډ لرې کړئ چې د {app_name} خلاصولو لپاره اړین دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remova a senha requerida para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remova a palavra-passe necessária para desbloquear {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elimină parola necesară pentru a debloca {app_name}." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить пароль, необходимый для разблокировки {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukloni lozinku potrebnu za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීමට අවශ්‍ය මුරපදය ඉවත් කරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odstrániť heslo potrebné na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odstrani geslo, potrebno za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiqe fjalëkalimin e nevojshëm për të zhbllokuar {app_name}-in." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Уклони лозинку потребну за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukloni lozinku potrebnu za otključavanje {app_name}." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ta bort lösenordet som krävs för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ondoa nywila inayotakiwa kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} இற்கு அணுக அடியாக கடவுச்சொல்லை நீக்கவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ని అన్లాక్ చేయడానికి అవసరమైన పాస్వర్డ్ తొలగించు." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ลบรหัสผ่านที่ต้องใช้เพื่อปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} uygulamasının kilidini açmak için gereken şifreyi kaldırın." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Видалити пароль, який потрібен для розблокування {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے درکار پاس ورڈ کو ہٹا دیں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ni ochish uchun talab qilinadigan parolni olib tashlash." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xóa mật khẩu cần thiết để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Susa iphasiwedi efunekayo ukuze uvule {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "删除{app_name}的解锁密码。" - } - }, - "zh-TW" : { + } + } + }, + "passwords" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "去除解鎖 {app_name} 的密碼。" + "value" : "Passwords" } } } @@ -335502,478 +337058,87 @@ "passwordSetDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou wagwoord is gestel. Hou dit asseblief veilig." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم تعيين كلمة المرور الخاصة بك. احفظها في مامن من فضلك." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolunuz təyin edildi. Lütfən, onu güvəndə saxlayın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک پاسکوڈ برنکی. براہپس محفوظے کہ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль быў усталяваны. Захавайце яго ў бяспецы." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата парола беше зададена. Моля, пазете я безопасно." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড সেট করা হয়েছে। দয়া করে এটি নিরাপদ রাখুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra contrasenya s'ha definit. Mantingueu-la segura." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvé heslo bylo nastaveno. Pečlivě si ho odlož." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i osod. Cadwch ef yn ddiogel." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din adgangskode er blevet indstillet. Venligst hold den sikker." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dein Passwort wurde festgelegt. Bitte bewahre es sicher auf." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει οριστεί. Παρακαλώ κρατήστε τον ασφαλή." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your password has been set. Please keep it safe." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via pasvorto estas agordita. Bonvolu konservi ĝin sekura." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido establecida. Por favor, mantenla segura." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido establecida. Por favor, manténgala segura." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie parool on määratud. Hoidke seda turvaliselt." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure pasahitza ezarri da. Gorde seguru batean." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور شما فعال شد. لطفا آن را در جای امنی ذخیره کنید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanasi on asetettu. Pidä se turvassa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nabago na ang iyong password. Pakisuyong itago ito." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre mot de passe a été défini. Veuillez le conserver en sécurité." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu contrasinal foi configurado. Por favor, mantéñeo seguro." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "An saita kalmar sirrinku. Da fatan za a kiyaye shi lafiya." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמה שלך הוגדרה. שמור עליה בבטחה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपका पासवर्ड सेट कर दिया गया है। कृपया इसे सुरक्षित रखें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je postavljena. Molimo, čuvajte je na sigurnom." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszó be lett állítva. Tartsd biztonságos helyen!" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր գաղտնաբառը սահմանվել է։ Խնդրում ենք անվտանգ պահել։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata sandi anda telah disetel. Harap untuk menjaganya." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La tua password è stata impostata. Si prega di tenerla al sicuro." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードが設定されました。安全に保管してください。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი პაროლი დაყენებულია. გთხოვთ, შეინახეთ იგი უსაფრთხოდ." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យសម្ងាត់របស់អ្នកត្រូវបានកំណត់។ សូមរក្សាវាឲ្យមានសុវត្ថិភាព។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ಹೊಂದಿಸಲಾಗಿದೆ. ಅದು ಸುರಕ್ಷಿತವಾಗಿರಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 설정이 완료되었습니다. 안전히 관리하시기 바랍니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "وشەی پەرەسەدت دابینکرا. تکایە ئەوە بەندەن پارێزەر بێت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom In" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yo ekatebatibwawo. Kaakasa nti bagutemye mu kifo ekinyuuse eritassaneyebwa." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsų slaptažodis buvo nustatytas. Prašome saugoti jį saugiai." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsu parole tika iestatīta. Lūdzu, saglabājiet to drošībā." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата лозинка е поставена. Ве молиме чувајте ја безбедно." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны нууц үг томилогдсон байна. Нууц үгээ хамгаалж байгаарай." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan anda telah ditetapkan. Sila simpan dengan selamat." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်၏ စကားဝှက် ဖြင့်ထားသည်။ ထိန်းသိမ်းပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er stilt. Vennligst oppbevar det trygt." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet er blitt stilt. Vennligst oppbevar det trygt." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईँको पासवर्ड सेट गरिएको छ। कृपया यसलाई सुरक्षित राख्नुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uw wachtwoord is ingesteld. Hou het veilig." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er blitt satt. Vennligst oppbevar det trygt." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yanu yakhalapo. Chonde sungani mosamala." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਸੈਟ ਕੀਤਾ ਗਿਆ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਸੁਰੱਖਿਅਤ ਰੱਖੋ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ustawiono hasło. Zachowaj je w bezpiecznym miejscu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو پاسورډ ټاکل شوی دی. مهرباني وکړۍ، دا خوندي وساتئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sua senha foi definida. Por favor, mantenha-a segura." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A sua palavra-passe foi definida. Por favor, mantenha-a segura." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parola ta a fost setată. Te rugăm să o păstrezi în siguranță." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль установлен. Пожалуйста, храните его в безопасном месте." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoja šifra je postavljena. Molimo, čuvaj je na sigurnom." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය සකසා ඇත. කරුණාකර එය ආරක්ෂිතව තබා ගන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bolo nastavené. Uchovajte ho prosím v bezpečí." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše geslo je bilo nastavljeno. Prosim, hranite ga na varnem mestu." + "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi juaj është vendosur. Ju lutemi ta mbani të sigurt." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша лозинка је подешена. Молимо вас да је сачувате." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je podešena. Čuvajte je na sigurnom mestu." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ditt lösenord har angetts. Håll det säkert." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenosiri lako limewekwa. Tafadhali lihifadhi salama." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் அமைக்கப்பட்டுள்ளது. தயவுசெய்து அதை பாதுகாப்பாக வைத்திருங்கள்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ సెట్ చేయబడింది. దయచేసి దాన్ని సురక్షితంగా ఉంచండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านของคุณถูกตั้งแล้ว กรุณารักษาเอาไว้ให้ปลอดภัย" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şifreniz ayarlandı. Lütfen güvende tutunuz." - } - }, - "uk" : { + } + } + }, + "passwordStrengthCharLength" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль встановлено. Будь ласка, збережіть його в безпеці." + "value" : "Longer than 12 characters" } - }, - "ur-IN" : { + } + } + }, + "passwordStrengthIncludeNumber" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "آپ کا پاس ورڈ مقرر ہو گیا ہے۔ براہ کرم اسے محفوظ رکھیں۔" + "value" : "Includes a number" } - }, - "uz" : { + } + } + }, + "passwordStrengthIncludesLetter" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Parolingiz olib tashlandi." + "value" : "Includes a letter" } - }, - "vi" : { + } + } + }, + "passwordStrengthIncludesLowercase" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mật khẩu của bạn đã được đặt. Hãy giữ nó cẩn thận." + "value" : "Includes a lowercase letter" } - }, - "xh" : { + } + } + }, + "passwordStrengthIncludesUppercase" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Iphasiwedi yakho isetiwe. Nceda uyigcine ikhuselekile." + "value" : "Includes a uppercase letter" } - }, - "zh-CN" : { + } + } + }, + "passwordStrengthIndicator" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "您的密码已设定。请妥善保管。" + "value" : "Password Strength Indicator" } - }, - "zh-TW" : { + } + } + }, + "passwordStrengthIndicatorDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "您的密碼設定完成。請注意保管。" + "value" : "Setting a strong password helps protect your messages and attachments if your device is ever lost or stolen." } } } @@ -336446,7 +337611,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "粘帖" + "value" : "粘贴" } }, "zh-TW" : { @@ -336508,6 +337673,18 @@ "value" : "Ŝanĝi permesojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambio de permiso" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambio de permiso" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -336532,6 +337709,18 @@ "value" : "Persetujuan Diubah" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifica autorizzazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "権限の変更" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -336550,6 +337739,18 @@ "value" : "Zmiana uprawnień" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alteração de permissão" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modificare permisiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -336562,6 +337763,12 @@ "value" : "Ändra tillåtelse" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İzin Değişimi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -336579,6 +337786,12 @@ "state" : "translated", "value" : "授权变更" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "權限變更" + } } } }, @@ -336600,7 +337813,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Musiqi və səs\"i işə salın." + "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə erişməlidir, ancaq bu erişimə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Musiqi və səs\"i işə salın." } }, "bal" : { @@ -338516,7 +339729,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntülü zəng etmək üçün kameraya müraciət tələb olunur. Davam etmək üçün Ayarlarda \"Kamera\" icazəsini işə salın." + "value" : "Görüntülü zəng etmək üçün kameraya erişim tələb olunur. Davam etmək üçün Ayarlarda \"Kamera\" icazəsini işə salın." } }, "ca" : { @@ -338549,6 +339762,18 @@ "value" : "Camera access is required to make video calls. Toggle the \"Camera\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la cámara para realizar videollamadas. Activa el permiso de \"Cámara\" en Configuración para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la cámara para realizar videollamadas. Activa el permiso de \"Cámara\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -338567,6 +339792,18 @@ "value" : "A videohívások indításához kamerához való hozzáférés szükséges. Kapcsolja be a „Kamera” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla fotocamera è necessario per effettuare videochiamate. Per continuare, attiva l'autorizzazione \"Fotocamera\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビデオ通話を行うにはカメラへのアクセスが必要です。続行するには、設定で「カメラ」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -338585,6 +339822,18 @@ "value" : "Do prowadzenia rozmów wideo wymagany jest dostęp do kamery. Aby kontynuować, przełącz uprawnienia „Aparat” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso à câmara para fazer chamadas de vídeo. Ative a permissão \"Câmara\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este necesar accesul la cameră pentru a efectua apeluri video. Comută permisiunea \"Cameră\" în Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -338597,6 +339846,12 @@ "value" : "Kameraåtkomst krävs för att ringa videosamtal. Växla behörigheten \"Kamera\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görüntülü arama yapmak için kamera erişimi gereklidir. Devam etmek için Ayarlar'dan \"Kamera\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -338608,6 +339863,12 @@ "state" : "translated", "value" : "需要摄像头访问权限才能进行通话。在设置中允许“摄像头”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "進行視訊通話需要啟用相機權限。請在設定中開啟「相機」權限以繼續。" + } } } }, @@ -338623,7 +339884,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kamera müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Kamera\" icazəsini söndürün." + "value" : "Kamera erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Kamera\" icazəsini söndürün." } }, "ca" : { @@ -338656,6 +339917,18 @@ "value" : "Camera access is currently enabled. To disable it, toggle the \"Camera\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la cámara está activado actualmente. Para desactivarlo, desactiva el permiso de \"Cámara\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la cámara está activado actualmente. Para desactivarlo, desactiva el permiso de \"Cámara\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -338668,12 +339941,30 @@ "value" : "कैमरा एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"कैमरा\" अनुमति टॉगल करें।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A kamerához való hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Kamera” engedélyt a beállításokban." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akses kamera saat ini diaktifkan. Untuk mematikan, pilih izin \"Kamera\" dalam Pengaturan." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla fotocamera è attualmente abilitato. Per disattivarlo, disattiva l'autorizzazione \"Fotocamera\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カメラへのアクセスは現在有効になっています。無効にするには、設定で「カメラ」の許可をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -338692,6 +339983,18 @@ "value" : "Dostęp do aparatu jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Aparat” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso à câmara está ativado de momento. Para o desativar, altere a permissão \"Câmara\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la cameră este activat în prezent. Pentru a-l dezactiva, comută permisiunea \"Cameră\" în Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -338704,6 +340007,12 @@ "value" : "Kameraåtkomst är för närvarande aktiverad. För att inaktivera det, växla behörigheten \"Kamera\" i Inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kamera erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Kamera\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -338715,6 +340024,12 @@ "state" : "translated", "value" : "摄像头访问权限已允许。如需禁用,请在设置中禁用“摄像头”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用相機權限。若要停用,請在設定中關閉「相機」權限。" + } } } }, @@ -338736,7 +340051,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Foto və video göndərə bilməyiniz üçün {app_name} kameraya müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Kamera\"nı işə salın." + "value" : "Foto və video göndərə bilməyiniz üçün {app_name} kameraya erişməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Kamera\"nı işə salın." } }, "bal" : { @@ -339197,7 +340512,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntülü zənglər üçün kameraya müraciətə icazə ver." + "value" : "Görüntülü zənglər üçün kameraya erişimə icazə ver." } }, "ca" : { @@ -339236,6 +340551,18 @@ "value" : "Permesu aliron al kamerao por video vokoj." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso a la cámara para las videollamadas." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso a la cámara para las videollamadas." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -339260,6 +340587,18 @@ "value" : "Izinkan akses kamera untuk video call." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso alla fotocamera per le videochiamate." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビデオ通話のためにカメラへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -339278,6 +340617,18 @@ "value" : "Zezwól na dostęp do kamery na potrzeby rozmów wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso à câmara para chamadas de vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la cameră pentru apeluri video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -339290,6 +340641,12 @@ "value" : "Tillåt åtkomst till kamera för videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görüntülü aramalar için kamera erişimine izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -339301,6 +340658,12 @@ "state" : "translated", "value" : "请允许访问摄像头以进行视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許使用相機以進行視訊通話。" + } } } }, @@ -340265,484 +341628,10 @@ "permissionsKeepInSystemTrayDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} loop aan in die agtergrond wanneer jy die venster sluit" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} يستمر في العمل في الخلفية عندما تقوم بإغلاق النافذة" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pəncərəni bağladıqda {app_name} arxaplanda işləməyə davam edir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ھنوک پد پسینی چشماں بند بو تک پس زمینه أٹھے چا" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} працягвае працу ў фонавым рэжыме, калі вы закрываеце акно" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "При затваряне на прозореца с програмата {app_name}, тя остава включена и продължава паралелно да работи" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} উইন্ডো বন্ধ করলেও后台 চালু থাকে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua funcionant en segon pla quan tanqueu la finestra" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} pokračuje v běhu na pozadí, když zavřete okno" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} yn parhau i redeg yn y cefndir pan fyddwch yn cau'r ffenestr" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kører fortsat i baggrunden, når du lukker vinduet" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} läuft im Hintergrund weiter, wenn du das Fenster schließt" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Το {app_name} συνεχίζει να εκτελείται στο παρασκήνιο όταν κλείνετε το παράθυρο" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} continues running in the background when you close the window" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} daŭrigas funkcii en la fono kiam vi fermas la fenestron" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continúa ejecutándose en segundo plano al cerrar la ventana" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continúa ejecutándose en segundo plano cuando cierras la ventana" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} jätkab taustal töötamist, kui sulgete akna" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}(e)k lanean jarraitzen du atzealdean leihoa ixten duzunean" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} با بستن پنجره، همچنان در پس‌زمینه اجرا می‌شود." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} pysyy käynnissä taustalla, kun suljet ikkunan" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ay patuloy na gumagana sa background kapag isinara mo ang window" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continue à fonctionner en arrière-plan lorsque vous fermez la fenêtre" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} segue a executarse en segundo plano cando pechas a ventá" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} yana ci gaba da gudana a bango lokacin da ka rufe taga" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ממשיך לפעול ברקע כאשר אתה סוגר את החלון" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "जब आप विंडो बंद करते हैं तो {app_name} पृष्ठभूमि में चलता रहता है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja raditi u pozadini kada zatvorite prozor" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A(z) {app_name} az ablak bezárása után is tovább fut a háttérben" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} շարունակում է աշխատել ֆոնային ռեժիմում, երբ դուք փակում եք պատուհանը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} bekerja di background ketika anda menutup jendela" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continuerà ad essere eseguito in background quando chiudi la finestra" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}はウィンドウを閉じてもバックグラウンドで実行され続けます" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} განაგრძობს მუშაობას ფონის რეჟიმში, როდესაც ფანჯარას დახურავთ" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} នឹងបន្តដំណើរការនៅផ្ទៃខាងក្រោយនៅពេលអ្នកបិទផ្ទាំងបង្អួច" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಕಿಟಕಿ ಮುಚ್ಚಿದಾಗ ಹಿನ್ನೆಲೆ ಕಾರ್ಯನಿರ್ವಹಣೆ ಮುಂದುವರಿಸುತ್ತದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 창을 닫아도 백그라운드에서 실행됩니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} هەموو دایما کار دەکات لە پاشچاوە ڕوونکردنی دەروازەکان" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} di dema ku em pencereyê digirin, berdewam dike maju di piştî deran de bixebitîne" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ekwata mu kutambuzibwa munda nga tosendewo olukindu" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ໄດ້ດໍາເນີນການຕໍ່ໄປໃນພື້ນຫລັງໃນຂະນະທີ່ທ່ານປິດປ່ອງ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} toliau veikia fone, kai užveriate langą" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} turpina darboties fonā, kad aizverat logu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} продолжува да работи во заднина кога ќе го затворите прозорецот" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} нь та цонхыг хаахад арын горимд ажилласаар байна" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} terus berjalan di latar belakang apabila anda menutup tetingkap" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ကိုပိတ်လိုက်ချိန် ဝင်းဒိုးကိုပိတ်လိုက်ရင် မီးနောက်ဆုံးဝင်ရောက်ထဲမှာ ဆက်လက် လုပ်ဆောင်နေပါသည်" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} विन्डो बन्द गर्दा पृष्ठभूमिमा चलिरहन्छ" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} blijft op de achtergrond draaien wanneer je het venster sluit" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} imachitlikira ntchifukwa chakuti cikhale m'kumbuyo mukatseka zenera" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ ਚੱਲਦਾ ਰਹਿੰਦਾ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਖਿੜਕੀ ਬੰਦ ਕਰਦੇ ਹੋ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aplikacja {app_name} nadal działa w tle po zamknięciu okna" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} په شالید کې چلیږي کله چې تاسو کړکۍ وتړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua sendo executado em segundo plano quando você fecha a janela" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua a correr em segundo plano quando fecha a janela" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} va continua să ruleze în fundal după închiderea ferestrei" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} продолжает работать в фоновом режиме даже после закрытия окна" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja raditi u pozadini kada zatvorite prozor" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබ කවුළුව වැසූ විට {app_name} පසුබිමේ දිගටම ක්‍රියාත්මක වේ" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} bude pokračovať v behu na pozadí, keď zavrieš okno" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} se nadaljuje v ozadju, ko zaprete okno" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} vazhdon të funksionojë në sfond kur mbyllni dritaren" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} наставља да ради у позадини када затворите прозор" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja rad u pozadini kada zatvorite prozor" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsätter att köras i bakgrunden när du stänger fönstret" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} inaendelea kukimbia chinichini ukiwa umefunga dirisha" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} சாளரத்தை மூடிக்கொண்ட பின்னரும் பின்னணி செயல்பாடுகளில் தொடர்ச்சியாக இயங்கிவரும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} విండోను మూసినప్పుడు నేపథ్యంలో కొనసాగుతుంది" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ยังคงทำงานต่อไปในพื้นหลังเมื่อคุณปิดหน้าต่าง" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} pencereyi kapattığınızda arka planda çalışmaya devam eder" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} продовжує працювати у фоновому режимі, коли ви закриваєте вікно" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} پس منظر میں چلتا رہتا ہے جب آپ ونڈو بند کرتے ہیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} oyna yopilganda ham fon rejimida ishlashda davom etadi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} tiếp tục chạy nền khi bạn đóng cửa sổ" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} iqhubeka isebenza ngasemva xa uvala ifestile" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}会在您关闭窗口后继续在后台运行" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "當您關閉視窗之後,{app_name} 會繼續在系統後台執行" + "value" : "{app_name} continues running in the background when you close the window." } } } @@ -340765,7 +341654,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} davam etmək üçün foto kitabxanasına müraciət etməlidir. iOS ayarlarında müraciəti fəallaşdıra bilərsiniz." + "value" : "{app_name} davam etmək üçün foto kitabxanasına erişməlidir. Erişimi iOS ayarlarında fəallaşdıra bilərsiniz." } }, "bal" : { @@ -341238,7 +342127,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Zəngləri asanlaşdırmaq üçün Lokal şəbəkə müraciəti tələb olunur. Davam etmək üçün Ayarlarda \"Lokal şəbəkə\" icazəsini işə salın." + "value" : "Zəngləri asanlaşdırmaq üçün Lokal şəbəkə erişimi tələb olunur. Davam etmək üçün Ayarlarda \"Lokal şəbəkə\" icazəsini işə salın." } }, "ca" : { @@ -341271,6 +342160,18 @@ "value" : "Local Network access is required to facilitate calls. Toggle the \"Local Network\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la red local para facilitar las llamadas. Activa el permiso de \"Red local\" en Configuración para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la red local para facilitar las llamadas. Activa el permiso de \"Red local\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341289,6 +342190,18 @@ "value" : "A hívások lehetővé tételéhez szükséges a helyi hálózathoz való hozzáférés. Kapcsolja be a „Helyi hálózat” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla rete locale è necessario per facilitare le chiamate. Per continuare, attiva l'autorizzazione \"Rete locale\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話を行うにはローカルネットワークへのアクセスが必要です。続行するには、設定で「ローカルネットワーク」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341307,6 +342220,18 @@ "value" : "Aby móc wykonywać połączenia, wymagany jest dostęp do sieci lokalnej. Aby kontynuować, przełącz uprawnienia „Sieć lokalna” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso à rede local para permitir chamadas. Ative a permissão \"Rede local\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la rețeaua locală este necesar pentru a facilita apelurile. Activează permisiunea „Rețea locală” din Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341319,6 +342244,12 @@ "value" : "Lokal nätverksåtkomst krävs för att underlätta samtal. Växla behörigheten \"Lokalt nätverk\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aramaları sağlamak için Yerel Ağ erişimi gereklidir. Devam etmek için Ayarlar'dan \"Yerel Ağ\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341330,6 +342261,12 @@ "state" : "translated", "value" : "需要本地网络访问权限才能进行通话。在设置中允许“本地网络”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要存取本地網路才能進行通話。請在「設定」中切換「本地網路」權限以繼續。" + } } } }, @@ -341339,7 +342276,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "{app_name}, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -341378,6 +342315,18 @@ "value" : "{app_name} bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341396,6 +342345,18 @@ "value" : "A(z) {app_name} alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341414,6 +342375,18 @@ "value" : "{app_name} potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341426,6 +342399,12 @@ "value" : "{app_name} behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341443,6 +342422,12 @@ "state" : "translated", "value" : "{app_name}需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -341452,7 +342437,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Lokal şəbəkə müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Lokal şəbəkə\" icazəsini söndürün." + "value" : "Lokal şəbəkə erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Lokal şəbəkə\" icazəsini söndürün." } }, "ca" : { @@ -341485,6 +342470,18 @@ "value" : "Local Network access is currently enabled. To disable it, toggle the \"Local Network\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la red local está activado actualmente. Para desactivarlo, desactiva el permiso de \"Red local\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la red local está activado actualmente. Para desactivarlo, desactiva el permiso de \"Red local\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341497,6 +342494,24 @@ "value" : "स्थानीय नेटवर्क एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"स्थानीय नेटवर्क\" अनुमति टॉगल करें।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A helyi hálózati hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Helyi hálózat” engedélyt a beállításokban." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla rete locale è attualmente abilitato. Per disattivarlo, disattiva l'autorizzazione \"Rete locale\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワークへのアクセスは現在有効になっています。無効にするには、設定画面の「ローカルネットワーク」権限をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341515,6 +342530,18 @@ "value" : "Dostęp do sieci lokalnej jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Sieć lokalna” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso à rede local está atualmente ativado. Para desativar, desligue a permissão \"Rede local\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la rețeaua locală este activat în prezent. Pentru a-l dezactiva, comută permisiunea „Rețea locală” din Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341527,6 +342554,12 @@ "value" : "Lokal nätverksåtkomst är aktiverad. För att inaktivera det, växla behörigheten \"Lokalt nätverk\" i inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerel Ağ erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Yerel Ağ\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341538,6 +342571,12 @@ "state" : "translated", "value" : "本地网络访问权限已允许。如需禁用,请在设置中禁用“本地网络”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用本地網路存取。如需停用,請在「設定」中切換「本地網路」權限。" + } } } }, @@ -341553,7 +342592,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Səsli və görüntülü zəngləri asanlaşdırmaq üçün lokal şəbəkəyə müraciətə icazə verin." + "value" : "Səsli və görüntülü zəngləri asanlaşdırmaq üçün lokal şəbəkəyə erişimə icazə verin." } }, "ca" : { @@ -341592,6 +342631,18 @@ "value" : "Permesu aliron al loka reto por faciligi voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acceso a la red local para facilitar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acceso a la red local para facilitar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341610,6 +342661,18 @@ "value" : "Engedélyezze a helyi hálózathoz való hozzáférést a hang- és videohívások lehetővé tételéhez." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso alla rete locale per facilitare le chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音声通話およびビデオ通話を行うためにローカルネットワークへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341628,6 +342691,18 @@ "value" : "Zezwól na dostęp do sieci lokalnej, aby ułatwić połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso à rede local para facilitar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la rețeaua locală pentru a facilita apelurile vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341640,6 +342715,12 @@ "value" : "Tillåt åtkomst till lokala nätverk för att underlätta röst- och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sesli ve görüntülü aramaları sağlamak için yerel ağa erişime izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341651,6 +342732,12 @@ "state" : "translated", "value" : "请允许访问本地网络访问以进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許存取本地網路以便進行語音與視訊通話。" + } } } }, @@ -341705,6 +342792,18 @@ "value" : "Loka reto" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red local" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red local" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341729,6 +342828,18 @@ "value" : "Jaringan Lokal" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rete locale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワーク" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341747,6 +342858,18 @@ "value" : "Sieć lokalna" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rede local" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rețea locală" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341759,6 +342882,12 @@ "value" : "Lokalt Nätverk" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerel ağ" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341776,6 +342905,12 @@ "state" : "translated", "value" : "本地网络" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "本地網路" + } } } }, @@ -342276,7 +343411,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}, zəng etmək və səsli mesaj göndərmək üçün mikrofona müraciət etməlidir, ancaq bu müraciətə həmişəlik rədd cavabı verilib. Ayarlara → İcazələr bölməsinə gedin və \"Mikrofon\"u işə salın." + "value" : "{app_name}, zəng etmək və səsli mesaj göndərmək üçün mikrofona erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Ayarlara → İcazələr bölməsinə gedin və \"Mikrofon\"u işə salın." } }, "bal" : { @@ -342743,7 +343878,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Zəng etmək və səsli mesajları yazmaq üçün mikrofona müraciət tələb olunur. Davam etmək üçün Ayarlarda \"Mikrofon\" icazəsini işə salın." + "value" : "Zəng etmək və səsli mesajları yazmaq üçün mikrofona erişim tələb olunur. Davam etmək üçün Ayarlarda \"Mikrofon\" icazəsini işə salın." } }, "ca" : { @@ -342776,6 +343911,18 @@ "value" : "Microphone access is required to make calls and record audio messages. Toggle the \"Microphone\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso al micrófono para realizar llamadas y grabar mensajes de audio. Activa el permiso de \"Micrófono\" en Configuración para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso al micrófono para realizar llamadas y grabar mensajes de audio. Activa el permiso de \"Micrófono\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -342794,6 +343941,18 @@ "value" : "A hívások indításához és hangüzenetek rögzítéséhez mikrofonhoz való hozzáférés szükséges. Kapcsolja be a „Mikrofon” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso al microfono è necessario per effettuare chiamate e registrare messaggi audio. Per continuare, attiva l'autorizzazione \"Microfono\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話および音声メッセージの録音にはマイクへのアクセスが必要です。続行するには、設定で「マイク」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -342812,6 +343971,18 @@ "value" : "Do wykonywania połączeń i nagrywania wiadomości audio wymagany jest dostęp do mikrofonu. Aby kontynuować, przełącz uprawnienia „Mikrofon” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso ao microfone para fazer chamadas e gravar mensagens de áudio. Ative a permissão \"Microfone\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este necesar accesul la microfon pentru a efectua apeluri și a înregistra mesaje audio. Activează permisiunea „Microfon” din Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -342824,6 +343995,12 @@ "value" : "Tillgång till mikrofon krävs för att ringa samtal och spela in ljudmeddelanden. Växla behörigheten \"Mikrofon\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arama yapmak ve sesli mesaj kaydetmek için mikrofon erişimi gereklidir. Devam etmek için Ayarlar'dan \"Mikrofon\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -342835,6 +344012,12 @@ "state" : "translated", "value" : "需要麦克风访问权限以进行通话和录制语音消息。在设置中打开“麦克风”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "進行通話和錄製語音訊息需要啟用麥克風權限。請在設定中開啟「麥克風」權限以繼續。" + } } } }, @@ -342856,7 +344039,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} gizlilik ayarlarında mikrofona müraciəti fəallaşdıra bilərsiniz" + "value" : "{app_name} gizlilik ayarlarında mikrofona erişimi fəallaşdıra bilərsiniz" } }, "bal" : { @@ -343814,7 +344997,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofon müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Mikrofon\" icazəsini söndürün." + "value" : "Mikrofon erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Mikrofon\" icazəsini söndürün." } }, "ca" : { @@ -343847,18 +345030,54 @@ "value" : "Microphone access is currently enabled. To disable it, toggle the \"Microphone\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso al micrófono está activado actualmente. Para desactivarlo, desactiva el permiso de \"Micrófono\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso al micrófono está activado actualmente. Para desactivarlo, desactiva el permiso de \"Micrófono\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'accès au microphone est actuellement activé. Pour le désactiver, décochez l'autorisation \"Microphone\" dans les Paramètres." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "माइक्रोफोन एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"माइक्रोफोन\" अनुमति टॉगल करें।" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A mikrofonhoz való hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Mikrofon” engedélyt a beállításokban." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akses mikrofon saat ini diaktifkan. Untuk mematikan, pilih izin \"Mikrofon\" dalam Pengaturan." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso al microfono è attualmente abilitato. Per disabilitarlo, disattiva l'autorizzazione \"Microfono\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マイクへのアクセスは現在有効です。無効にするには、設定で「マイク」の許可をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -343877,6 +345096,18 @@ "value" : "Dostęp do mikrofonu jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Mikrofon” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso ao microfone está ativado de momento. Para o desativar, altere a permissão \"Microfone\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la microfon este activat în prezent. Pentru a-l dezactiva, comută permisiunea \"Microfon\" în Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -343889,6 +345120,12 @@ "value" : "Tillgång till mikrofon är för närvarande aktiverad. För att inaktivera det, växla behörigheten \"Mikrofon\" i inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mikrofon erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Mikrofon\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -343900,6 +345137,12 @@ "state" : "translated", "value" : "麦克风访问权限已允许。如需禁用,请在设置中禁用“麦克风”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用麥克風權限。若要停用,請在設定中關閉「麥克風」權限。" + } } } }, @@ -343921,7 +345164,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofona müraciətə icazə verin." + "value" : "Mikrofona erişim icazəsi verin." } }, "bal" : { @@ -344388,7 +345631,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Səsli zənglər və səsli mesajlar üçün mikrofona müraciətə icazə verin." + "value" : "Səsli zənglər və səsli mesajlar üçün mikrofona erişimə icazə verin." } }, "ca" : { @@ -344427,6 +345670,18 @@ "value" : "Permesu aliron al mikrofono por voĉaj vokoj kaj aŭdiomesaĝoj." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso al micrófono para llamadas de voz y mensajes de audio." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso al micrófono para llamadas de voz y mensajes de audio." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -344445,6 +345700,18 @@ "value" : "Engedélyezze a mikrofonhoz való hozzáférést hanghívások és hangüzenetek küldéséhez." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso al microfono per le chiamate vocali e i messaggi audio." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音声通話および音声メッセージのためにマイクへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -344463,6 +345730,18 @@ "value" : "Zezwól na dostęp do mikrofonu dla połączeń głosowych i wiadomości audio." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso ao microfone para chamadas de voz e mensagens de áudio." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la microfon pentru apeluri vocale și mesaje audio." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -344475,6 +345754,12 @@ "value" : "Tillåt åtkomst till mikrofon för röstsamtal och ljudmeddelanden." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sesli aramalar ve sesli mesajlar için mikrofon erişimine izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -344486,6 +345771,12 @@ "state" : "translated", "value" : "请允许访问麦克风以进行语音通话和录制语音消息。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許使用麥克風以進行語音通話與語音訊息。" + } } } }, @@ -344507,7 +345798,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə müraciət etməlidir." + "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə erişməlidir." } }, "bal" : { @@ -345453,7 +346744,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Foto və video göndərə bilməyiniz üçün {app_name} foto kitabxanasına müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Foto və videolar\"ı işə salın." + "value" : "Foto və video göndərə bilməyiniz üçün {app_name} foto kitabxanasına erişməlidir, ancaq bu erişimə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Foto və videolar\"ı işə salın." } }, "bal" : { @@ -346387,7 +347678,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "{app_name} qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { @@ -346872,7 +348163,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} foto və videoları saxlamaq üçün anbara müraciət etməlidir, ancaq bu icazəyə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedin, \"İcazələr\"i seçin və \"Anbar\" icazəsini fəallaşdırın." + "value" : "{app_name} foto və videoları saxlamaq üçün anbara erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedin, \"İcazələr\"i seçin və \"Anbar\" icazəsini fəallaşdırın." } }, "bal" : { @@ -347827,6 +349118,12 @@ "permissionsWriteCommunity" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu icmada yazma icazəniz yoxdur" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -347839,6 +349136,12 @@ "value" : "V této komunitě nemáte oprávnění k zápisu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast keine Schreibrechte in dieser Community" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -347851,29 +349154,113 @@ "value" : "Vi ne havas permesojn por skribi en tiu ĉi komunumo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No tienes permisos de escritura en esta comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No tienes permisos de escritura en esta comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous n'avez pas la permission d'écrire dans cette communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस Community में आपके पास लिखने की अनुमति नहीं है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Nincs írási jogosultsága ebben a közösségben" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non hai i permessi di scrittura in questa comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このコミュニティでは書き込み権限がありません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 커뮤니티에서 작정 권한이 없습니다" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt geen schrijfrechten in deze Community" + } + }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie masz uprawnień do zapisu w tej społeczności" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você não tem permissões de escrita nesta Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu aveți permisiune de scriere în această comunitate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У вас нет прав на отправку в этом сообществе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har inte skrivrättigheter i denna Community" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu toplulukta yazma izniniz yok" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вам не надано дозвіл на дописування у цій спільноті" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你没有在该社群中写入的权限" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您在此社群中沒有發文權限" + } } } }, @@ -349793,6 +351180,17 @@ } } }, + "preferences" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferences" + } + } + } + }, "preview" : { "extractionState" : "manual", "localizations" : { @@ -350272,213 +351670,1568 @@ } } }, + "previewNotification" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preview Notification" + } + } + } + }, "proActivated" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivləşdirildi" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Aktivováno" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiviert" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Activated" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सक्रिय किया गया" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attivato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクティベート済み" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geactiveerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktywowano" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ativado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активирован" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiverat" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkinleştirildi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "активовано" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "已激活" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "已啟用" + } } } }, "proAlreadyPurchased" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artıq yüksəltdiniz" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ja ho tens" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Už máte" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast bereits" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You’ve already got" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ya tienes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ya tienes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez déjà" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपके पास पहले से ही है" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hai già attivato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すでにご利用中です" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt al" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masz już" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Já tem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deja ai" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У вас уже есть" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har redan" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaten sahipsiniz" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "У вас вже є" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已拥有" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已擁有" + } } } }, "proAnimatedDisplayPicture" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getdik və ekran şəkliniz üçün GIF-lər və animasiyalı WebP təsvirləri yükləyin!" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Endavant i penja GIFs i imatges del webp animat per a la teva imatge de visualització!" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Jako váš zobrazovaný profilový obrázek nastavte animovaný GIF nebo WebP!" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lade GIF- und animierte WebP-Bilder als Profilbild hoch!" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Go ahead and upload GIFs and animated WebP images for your display picture!" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Adelante, sube GIFs e imágenes WebP animadas para tu imagen de perfil!" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Adelante, sube GIFs e imágenes WebP animadas para tu imagen de perfil!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Téléchargez des GIF et des images WebP animées pour votre photo de profil !" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आगे बढ़ें और अपनी डिस्प्ले तस्वीर के लिए GIF और एनिमेटेड WebP इमेज अपलोड करें!" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carica GIF e immagini WebP animate per la tua immagine del profilo!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ画像としてGIFやアニメーションWebP画像をアップロードできます!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upload nu GIF's en geanimeerde WebP-afbeeldingen voor je profielfoto!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możesz przesyłać GIF-y i animowane obrazy WebP jako swoje zdjęcie profilowe!" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agora pode enviar GIFs e imagens WebP animadas para a sua imagem de exibição!" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mergi mai departe și încarcă GIF-uri și imagini WebP animate pentru imaginea ta de profil!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вперёд! И загружай анимированные GIF и WebP для вашего изображения!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fortsätt och ladda upp GIF:ar och animerade WebP-bilder som visningsbild!" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hadi, profil resminiz için GIF'ler ve animasyonlu WebP görselleri yükleyin!" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не зволікайте і завантажуйте GIF та анімовані WebP картинки для свого аватара!" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "快去为头像上传 GIF 或动画 WebP 图片吧!" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以為您的顯示圖片上傳 GIF 或動畫 WebP 圖片了!" + } } } }, "proAnimatedDisplayPictureCallToActionDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} ilə animasiyalı ekran şəkillərini endirin və premium özəlliklərin kilidini açın" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obteniu imatges de visualització animada i desbloquegeu funcions premium amb {app_pro}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Získejte možnost nahrát animovaný zobrazovaný obrázek profilu a další prémiové funkce se Session Pro" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hole dir animierte Profilbilder und schalte Premium-Funktionen mit {app_pro} frei" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Get animated display pictures and unlock premium features with {app_pro}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consigue imágenes de perfil animadas y desbloquea funciones premium con {app_pro}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consigue imágenes de perfil animadas y desbloquea funciones premium con {app_pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenez des photos de profil animées et débloquez des fonctionnalités premium avec {app_pro}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एनीमेटेड डिस्प्ले तस्वीरें प्राप्त करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ottieni immagini del profilo animate e sblocca funzionalità premium con {app_pro}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アニメーションディスプレイ画像を取得し、{app_pro}でプレミアム機能を解除しましょう" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krijg geanimeerde profielfoto's en ontgrendel premiumfuncties met {app_pro}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zyskaj animowane zdjęcia profilowe i odblokuj funkcje premium dzięki {app_pro}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obtenha imagens de exibição animadas e desbloqueie funcionalidades premium com o {app_pro}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obține imagini de profil animate și deblochează funcționalități premium cu {app_pro}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Получите анимированное изображение профиля и другие разблокированные премиум функции с {app_pro}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skaffa animerade visningsbilder och lås upp premiumfunktioner med {app_pro}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animasyonlu profil resimleri edinin ve {app_pro} ile premium özelliklerin kilidini açın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отримайте анімовані аватари та розблокуйте преміальні функції з {app_pro}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "获取动画头像并使用 {app_pro} 解锁高级功能" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "取得動畫顯示圖片並透過 {app_pro} 解鎖進階功能" + } } } }, "proAnimatedDisplayPictureFeature" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animasiyalı profil şəkli" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imatge de pantalla animada" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animovaný zobrazovaný obrázek" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animiertes Profilbild" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Animated Display Picture" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagen de perfil animada" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagen de perfil animada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Photo de profil animée" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एनिमेटेड डिस्प्ले तस्वीर" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immagine del profilo animata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アニメーション表示画像" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geanimeerde profielfoto" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animowany obraz profilowy" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagem de exibição animada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poză de profil animată" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animerad visningsbild" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Анімоване зображення профілю" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "动画头像" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "動畫顯示圖片" + } } } }, "proAnimatedDisplayPictureModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "istifadəçiləri GIF-ləri yükləyə bilər" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "els usuaris poden penjar GIFs" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "uživatelé mohou nahrávat GIFy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nutzer können GIFs hochladen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "users can upload GIFs" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "los usuarios pueden subir GIFs" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "los usuarios pueden subir GIFs" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "les utilisateurs peuvent télécharger des GIFs" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उपयोगकर्ता GIF अपलोड कर सकते हैं" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "gli utenti possono caricare GIF" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーはGIFをアップロードできます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "gebruikers kunnen GIF's uploaden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "użytkownicy mogą przesyłać GIF-y" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "os utilizadores podem carregar GIFs" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "utilizatorii pot încărca GIF-uri" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "пользователи могут загружать GIF-файлы" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "användare kan ladda upp GIF:ar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "kullanıcılar GIF yükleyebilir" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "користувачі можуть завантажувати GIF" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户可上传 GIF" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "用戶可以上傳 GIF" + } } } }, "proAnimatedDisplayPicturesNonProModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF-ləri yükləyin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Penja els gifs amb" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Nahrajte GIFy se" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIFs hochladen mit" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Upload GIFs with" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sube GIFs con" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sube GIFs con" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Téléversez des GIF avec" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF अपलोड करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carica GIF con" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIFをアップロード(PRO)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upload GIF's met" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przesyłaj GIF-y z" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carregue GIFs com" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Încarcă GIF-uri cu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загружайте GIF с" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ladda upp GIF:ar med" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ile GIF Yükleyin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантажувати GIF з" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 PRO 上传 GIF" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 {app_pro} 上傳 GIF 圖片" + } } } }, "proBadge" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Nişanı" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Insígnia" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznak {app_pro}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro}-Abzeichen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Badge" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insignia de {app_pro}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insignia de {app_pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {app_pro}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} बैज" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {app_pro}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} バッジ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro}-badge" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaka {app_pro}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distintivo {app_pro}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insigna {app_pro}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro}-märke" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} значок" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} 徽章" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} 徽章" + } } } }, "proCallToActionLongerMessages" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha uzun mesajlar göndərmək istəyirsiniz? {app_pro} ilə daha çox mətn göndərin və premium özəlliklərin kilidini açın" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voleu enviar missatges més llargs? Envia més text i desbloqueja funcions premium amb {app_pro}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete posílat delší zprávy? Posílejte více textu odemknutím prémiových funkcí Session Pro" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du möchtest längere Nachrichten senden? Sende mehr Text und schalte Premium-Funktionen mit {app_pro} frei" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Want to send longer messages? Send more text and unlock premium features with {app_pro}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres enviar mensajes más largos? Envía más texto y desbloquea funciones premium con {app_pro}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres enviar mensajes más largos? Envía más texto y desbloquea funciones premium con {app_pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous voulez envoyer des messages plus longs ? Envoyez plus de messages et débloqué les fonctionnalités premium avec {app_pro}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लंबे संदेश भेजना चाहते हैं? अधिक टेक्स्ट भेजें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Szeretne hosszabb üzeneteket küldeni? Küldjön több szöveget és oldja fel a prémium funkciókat a {app_pro} szolgáltatással" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi inviare messaggi più lunghi? Invia più testo e sblocca funzionalità premium con {app_pro}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長文を送りたいですか?{app_pro}でより多くのテキストを送り、プレミアム機能を解除しましょう。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je langere berichten versturen? Verstuur meer tekst en ontgrendel premiumfuncties met {app_pro}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcesz wysyłać dłuższe wiadomości? Wyślij więcej tekstu i odblokuj funkcje premium dzięki {app_pro}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quer enviar mensagens mais longas? Envie mais texto e desbloqueie funcionalidades premium com {app_pro}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrei să trimiți mesaje mai lungi? Trimite mai mult text și deblochează funcții premium cu {app_pro}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хотите отправлять более длинные сообщения? Отправляйте больше текста и используйте премиум функции с {app_pro}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du skicka längre meddelanden? Skicka mer text och lås upp premiumfunktioner med {app_pro}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha uzun mesajlar mı göndermek istiyorsunuz? {app_pro} ile daha fazla metin gönderin ve premium özelliklerin kilidini açın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хочете відправляти довші повідомлення? Надсилайте більше тексту та розблокуйте преміальні функції застосунку з Session Pro" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "想发送更长的消息?使用 {app_pro} 发送更多文本并解锁高级功能" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "想傳送更長的訊息嗎?與 {app_pro} 一起傳送更多文字並解鎖進階功能" + } } } }, "proCallToActionPinnedConversations" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çoxunu sancmaq istəyirsiniz? {app_pro} ilə söhbətlərinizi təşkil edin və premium özəlliklərin kilidini açın" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vols més pins? Organitzes els teus xats i desbloqueges les funcions premium amb {app_pro}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete více připnutí? Organizujte své chaty a odemkněte prémiové funkce pomocí Session Pro" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr Anheftungen gewünscht? Organisiere deine Chats und schalte Premium-Funktionen mit {app_pro} frei" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Want more pins? Organize your chats and unlock premium features with {app_pro}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres más conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres más conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous voulez plus de messages épinglés ? Organisez vos chats et débloquez les fonctionnalités premium avec {app_pro}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अधिक पिन करना चाहते हैं? अपनी चैट व्यवस्थित करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi più chat bloccate? Organizza le tue chat e sblocca le funzionalità premium con {app_pro}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "さらにピン留めしますか?{app_pro}でチャットを整理して、プレミアム機能を解除しましょう" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je meer vastzetten? Organiseer je chats en ontgrendel premiumfuncties met {app_pro}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcesz przypinać więcej czatów? Zorganizuj konwersacje i odblokuj funkcje premium dzięki {app_pro}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quer fixar mais conversas? Organize os seus chats e desbloqueie funcionalidades premium com o {app_pro}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrei mai multe fixări? Organizează-ți conversațiile și deblochează funcționalități premium cu {app_pro}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хотите больше закреплений? Организуйте свои чаты и получайте доступ к премиум функциям с {app_pro}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du ha fler fästen? Organisera dina chattar och lås upp premiumfunktioner med {app_pro}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha fazla sabitleme mi istiyorsunuz? Sohbetlerinizi düzenleyin ve {app_pro} ile premium özelliklerin kilidini açın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потрібно більше закріплених бесід? Впорядкуйте свої чати та розблокуйте преміальні функції з {app_pro}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要固定更多对话?使用 {app_pro} 整理你的聊天并解锁高级功能" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要釘選更多對話嗎?使用 {app_pro} 整理您的聊天並解鎖進階功能" + } } } }, "proCallToActionPinnedConversationsMoreThan" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "5-dən çoxunu sancmaq istəyirsiniz? {app_pro} ilə söhbətlərinizi təşkil edin və premium özəlliklərin kilidini açın" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vols més de 5 pins? Organitzes els teus xats i desbloqueges les funcions premium amb {app_pro}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete více než 5 připnutí? Organizujte své chaty a odemkněte prémiové funkce pomocí Session Pro" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr als 5 Anheftungen gewünscht? Organisiere deine Chats und schalte Premium-Funktionen mit {app_pro} frei" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Want more than 5 pins? Organize your chats and unlock premium features with {app_pro}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres más de 5 conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres más de 5 conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous voulez plus que 5 messages épinglés ? Organisez vos chats et débloquez les fonctionnalités premium avec {app_pro}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "5 से अधिक पिन करना चाहते हैं? अपनी चैट व्यवस्थित करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi più di 5 chat bloccate? Organizza le tue chat e sblocca le funzionalità premium con {app_pro}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5件以上ピン留めしたいですか?{app_pro}でチャットを整理して、プレミアム機能を解除しましょう" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je meer dan 5 vastgezette gesprekken? Organiseer je chats en ontgrendel premiumfuncties met {app_pro}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcesz przypiąć więcej niż 5 czatów? Zorganizuj konwersacje i odblokuj funkcje premium dzięki {app_pro}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quer fixar mais de 5 conversas? Organize os seus chats e desbloqueie funcionalidades premium com {app_pro}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrei mai mult de 5 fixări? Organizează-ți conversațiile și deblochează funcționalități premium cu {app_pro}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нужно более 5 закреплений? С {app_pro} организуйте свои чаты и получите доступ к премиум функциям" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du ha mer än 5 fästisar? Organisera dina chattar och lås upp premiumfunktioner med {app_pro}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "5'ten fazla sabitleme mi istiyorsunuz? Sohbetlerinizi düzenleyin ve {app_pro} ile premium özelliklerin kilidini açın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потрібно понад 5 закріплених бесід? Впорядкуйте свої бесіди та розблокуйте преміальні функції з {app_pro}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要固定超过 5 个对话?使用 {app_pro} 整理你的聊天并解锁高级功能" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要釘選超過 5 則對話嗎?使用 {app_pro} 整理您的聊天並解鎖進階功能" + } } } }, "proFeatureListAnimatedDisplayPicture" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF və WebP ekran şəkilləri yüklə" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Nahrajte GIF a WebP jako zobrazovaný obrázek" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF- und WebP-Profilbilder hochladen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Upload GIF and WebP display pictures" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sube imágenes de perfil en formato GIF y WebP" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sube imágenes de perfil en formato GIF y WebP" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Téléversez des photos de profil au format GIF ou WebP" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF और WebP डिस्प्ले तस्वीरें अपलोड करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carica immagini profilo in formato GIF e WebP" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIFとWebPのディスプレイ画像をアップロード" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upload GIF- en WebP-profielfoto's" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prześlij obrazy profilowe w formacie GIF i WebP" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carregue imagens de exibição em GIF e WebP" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Încarcă imagini de profil GIF și WebP" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загрузка изображений в формате GIF и WebP" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ladda upp GIF- och WebP-visningsbilder" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GIF ve WebP profil resmi yükleme" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантажуйте GIF та WebP аватари" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "上传 GIF 和 WebP 头像" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "上傳 GIF 和 WebP 顯示圖片" + } } } }, "proFeatureListLargerGroups" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "300 üzvə qədər daha da böyük qrup söhbətləri" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -350491,29 +353244,131 @@ "value" : "Větší soukromé skupiny až 300 členů" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Größere Gruppenchats mit bis zu 300 Mitgliedern" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Larger group chats up to 300 members" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chats grupales más grandes de hasta 300 miembros" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chats grupales más grandes de hasta 300 miembros" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groupes de discussions plus larges, jusqu'à 300 participants" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "300 सदस्यों तक बड़े समूह चैट" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Nagyobb csoportos beszélgetések akár 300 taggal" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chat di gruppo maggiori fino a 300 membri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大300人の大型グループチャット" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groepsgesprekken tot wel 300 leden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Większe czaty grupowe do 300 członków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conversas de grupo maiores com até 300 membros" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conversații de grup mai mari, cu până la 300 de membri" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групповые чаты до 300 участников" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Större gruppchattar upp till 300 medlemmar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "300 üyeye kadar daha büyük grup sohbetleri" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Більша кількість — до 300 учасників — групових чатів" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更大的群组聊天,最多可容纳 300 名成员" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大支援 300 位成員的大型群組聊天室" + } } } }, "proFeatureListLoadsMore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üstəgəl, daha çox eksklüziv özəlliklər" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -350526,29 +353381,131 @@ "value" : "A další exkluzivních funkce" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Und viele weitere exklusive Funktionen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Plus loads more exclusive features" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y muchas funciones exclusivas más" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y muchas funciones exclusivas más" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus offre des fonctions exclusives additionnelles" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "साथ में कई और विशेष सुविधाएं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Plusz még több exkluzív funkció" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "E tante altre funzionalità esclusive" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "さらに多数の限定機能" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "En nog veel meer exclusieve functies" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "I wiele więcej ekskluzywnych funkcji" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "E muitas outras funcionalidades exclusivas" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Și multe alte funcționalități exclusive" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ множество эксклюзивных функций" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus många fler exklusiva funktioner" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrıca daha birçok özel özellik" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Та велика кількість ексклюзивних можливостей" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "还有更多专属功能等你解锁" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "以及更多獨家功能" + } } } }, "proFeatureListLongerMessages" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000 xarakterə qədər mesajlar" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -350561,46 +353518,250 @@ "value" : "Zprávy až do 10000 znaků" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten mit bis zu 10.000 Zeichen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Messages up to 10,000 characters" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes de hasta 10.000 caracteres" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensajes de hasta 10.000 caracteres" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messages jusqu'à 10,000 caractères" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000 वर्णों तक संदेश" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Legfeljebb 10 000 karakteres üzenetek" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messaggi fino a 10.000 caratteri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大10,000文字までのメッセージ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten tot 10.000 tekens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości do 10,000 znaków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mensagens até 10 000 caracteres" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaje de până la 10.000 de caractere" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщения до 10 тыс. символов" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden upp till 10 000 tecken" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "10.000 karaktere kadar mesajlar" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повідомлення до 10 000 символів" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息长度上限为 10,000 个字符" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "支援最多 10,000 字元的訊息" + } } } }, "proFeatureListPinnedConversations" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitsiz danışığı sancın" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin converses il·limitades" + } + }, "cs" : { "stringUnit" : { "state" : "translated", "value" : "Připněte neomezený počet konverzací" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbegrenzt viele Unterhaltungen anheften" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Pin unlimited conversations" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fija conversaciones sin límite" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fija conversaciones sin límite" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Épinglez un nombre illimité de conversations" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अनंत वार्तालाप पिन करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blocca un numero illimitato di conversazioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピン留め可能な会話が無制限" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbeperkt gesprekken vastzetten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przypinaj nieograniczoną liczbę konwersacji" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixe conversas ilimitadas" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixează un număr nelimitat de conversații" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрепление неограниченного количества бесед" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fäst obegränsat antal konversationer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sınırsız sohbet sabitleme" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закріплюйте необмежену кількість бесід" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "无限固定对话" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "釘選不限數量的對話" + } } } }, @@ -351527,7 +354688,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Встановити зображення для показу" + "value" : "Аватар" } }, "ur-IN" : { @@ -352485,7 +355646,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обрати картинку для показу" + "value" : "Встановити аватар" } }, "ur-IN" : { @@ -353077,7 +356238,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Aktualisierung des Profils fehlgeschlagen." + "value" : "Das Profil konnte nicht aktualisiert werden." } }, "el" : { @@ -353487,54 +356648,594 @@ "proGroupActivated" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qrup aktivləşdirildi" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup activat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupina aktivována" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppe aktiviert" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Group Activated" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo activado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groupe activé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह सक्रिय किया गया" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppo attivato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループが有効化されました" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groep geactiveerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupa została aktywowana" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo ativado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup activat" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupp aktiverad" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групу активовано" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "群组已激活" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已啟用" + } } } }, "proGroupActivatedDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu qurun tutumu artıb! Qrup admininin sayəsində artıq 300 nəfərə qədər üzvü dəstəkləyə bilər" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aquest grup ha ampliat la capacitat! Pot suportar fins a 300 membres perquè un administrador del grup té" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tato skupina má navýšenou kapacitu! Může podporovat až 300 členů, protože správce skupiny má" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Gruppe hat mehr Kapazität! Sie kann bis zu 300 Mitglieder unterstützen, da ein Gruppen-Administrator" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "This group has expanded capacity! It can support up to 300 members because a group admin has" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Este grupo tiene capacidad ampliada! Puede admitir hasta 300 miembros porque un administrador del grupo tiene" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Este grupo tiene capacidad ampliada! Puede admitir hasta 300 miembros porque un administrador del grupo tiene" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce groupe a une capacité étendue ! Il peut contenir jusqu’à 300 membres grâce à un administrateur qui dispose de" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस समूह की क्षमता बढ़ाई गई है! यह अब 300 सदस्यों तक का समर्थन कर सकता है क्योंकि एक समूह व्यवस्थापक के पास" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo gruppo ha una capacità estesa! Può supportare fino a 300 membri perché un amministratore del gruppo ha" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このグループは拡張されています!グループ管理者の設定により、最大300人のメンバーに対応できます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze groep heeft een grotere capaciteit! Er kunnen nu tot 300 leden deelnemen omdat een groepsbeheerder een" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta grupa ma zwiększoną pojemność! Może obsługiwać do 300 członków, ponieważ administrator grupy posiada" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo tem capacidade expandida! Pode suportar até 300 membros porque um administrador do grupo tem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup are capacitate extinsă! Poate susține până la 300 de membri deoarece un administrator de grup are" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Den här gruppen har utökad kapacitet! Den kan ha upp till 300 medlemmar eftersom en gruppadministratör har" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У цієї групи розширено можливості! Тепер вона може вміщати до 300 учасників, тому що адміністратор групи має" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "该群组已扩容!因管理员升级为 PRO,现支持最多 300 名成员" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組已擴充容量!由於群組管理員的設定,現在可支援多達 300 位成員" + } } } }, "proIncreasedAttachmentSizeFeature" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artırılmış qoşma ölçüsü" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Augment de la mida de l'adhesió" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšená velikost přílohy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erhöhte Anhangsgröße" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Increased Attachment Size" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del archivo adjunto aumentado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del archivo adjunto aumentado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taille de pièce jointe augmentée" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बढ़ाया गया अटैचमेंट आकार" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dimensione allegato aumentata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "添付ファイルサイズの増加" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verhoogde bijlagegrootte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwiększony rozmiar załączników" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maior tamanho de anexo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dimensiune mărită a atașamentului" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Större bilagegräns" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збільшений розмір вкладення" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件大小已增加" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件大小提升" + } } } }, "proIncreasedMessageLengthFeature" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artırılmış mesaj uzunluğu" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Augment de la longitud del missatge" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšená délka zprávy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erhöhte Nachrichtenlänge" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Increased Message Length" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayor longitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayor longitud de mensaje" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longueur de message augmentée" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बढ़ाई गई संदेश लंबाई" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lunghezza messaggio aumentata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージの文字数増加" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlengde berichtlengte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwiększona długość wiadomości" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maior comprimento de mensagem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lungime extinsă a mesajului" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förlängd meddelandelängd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збільшена довжина повідомлень" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息长度已增加" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息長度提升" + } } } }, "proMessageInfoFeatures" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesajda aşağıdakı {app_pro} özəllikləri istifadə olunub:" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aquest missatge va utilitzar les funcions següents {app_pro}:" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V této zprávě byly použity následující funkce {app_pro}:" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Nachricht verwendet die folgenden {app_pro}-Funktionen:" + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "This message used the following Session Pro features:" + "value" : "This message used the following {app_pro} features:" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este mensaje utilizó las siguientes funciones de {app_pro}:" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este mensaje utilizó las siguientes funciones de {app_pro}:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce message utilise les fonctionnalités suivantes de {app_pro} :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस संदेश में निम्नलिखित {app_pro} विशेषताएँ उपयोग की गईं हैं:" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo messaggio ha utilizzato le seguenti funzionalità di {app_pro}:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージには以下の {app_pro} 機能が使用されています:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dit bericht maakte gebruik van de volgende {app_pro}-functies:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta wiadomość zawierała następujące funkcje {app_pro}:" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta mensagem utilizou as seguintes funcionalidades do {app_pro}:" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest mesaj a folosit următoarele funcționalități {app_pro}:" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detta meddelande använde följande funktioner från {app_pro}:" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У цьому повідомленні наявні наступні функції Session Pro:" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "此消息使用了以下 {app_pro} 功能:" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "本訊息使用了以下 {app_pro} 功能:" } } } @@ -354419,6 +358120,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進に失敗しました" + } + } + } + } + } + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -354537,6 +358260,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "A promoção falhou" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "As promoções falharam" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -354750,6 +358501,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升失敗" + } + } + } + } + } + } } } }, @@ -355154,6 +358927,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進を適用できませんでした。再試行しますか?" + } + } + } + } + } + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -355272,6 +359067,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível aplicar a promoção. Gostaria de tentar novamente?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível aplicar as promoções. Gostaria de tentar novamente?" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -355485,12 +359308,40 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法套用晉升。您想要再試一次嗎?" + } + } + } + } + } + } } } }, "proSendMore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çoxunu göndərmək üçün" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -355503,34 +359354,238 @@ "value" : "Posílejte více se" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr senden mit" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Send more with" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía más con" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía más con" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyez plus avec" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इसके साथ और भेजें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Küldjön többet ezzel:" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invia di più con" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "さらに送信:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstuur meer met" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyślij więcej z" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envie mais com" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trimite mai mult cu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить еще с" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka mer med" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ile daha fazlasını gönderin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Надсилайте довші повідомлення з" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送更多内容,体验" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "升級後可傳送更多內容" + } } } }, "proUserProfileModalCallToAction" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqindən daha çox faydalanmaq istəyirsiniz? Daha güclü mesajlaşma təcrübəsi üçün {app_pro}-ya yüksəldin." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vols treure més informació de {app_name}? Actualitzes a {app_pro} per a una experiència de missatgeria més potent." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete z {app_name} získat více? Navyštee na {app_pro} pro výkonnější posílání zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Willst du mehr aus {app_name} herausholen? Upgrade auf {app_pro} für ein leistungsstärkeres Nachrichten-Erlebnis." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Want to get more out of {app_name}? Upgrade to {app_pro} for a more powerful messaging experience." } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres sacarle más provecho a {app_name}? Actualiza a {app_pro} para una experiencia de mensajería más potente." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres sacarle más provecho a {app_name}? Actualiza a {app_pro} para una experiencia de mensajería más potente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous voulez tirer le meilleur parti de {app_name} ? Passez à {app_pro} pour une expérience de messagerie améliorée." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप {app_name} से अधिक प्राप्त करना चाहते हैं? एक अधिक शक्तिशाली संदेश अनुभव के लिए {app_pro} में अपग्रेड करें।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi ottenere di più da {app_name}? Passa a {app_pro} per un'esperienza di messaggistica più potente." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}をもっと活用したいですか?より強力なメッセージ体験のために{app_pro}へアップグレードしましょう。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je meer uit {app_name} halen? Upgrade naar {app_pro} voor een krachtigere berichtbeleving." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcesz więcej z {app_name}? Uaktualnij do {app_pro}, aby uzyskać potężniejsze możliwości wiadomości." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quer aproveitar mais o {app_name}? Atualize para o {app_pro} e tenha uma experiência de mensagens mais poderosa." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrei să profiți mai mult de {app_name}? Fă upgrade la {app_pro} pentru o experiență de mesagerie mai puternică." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du få ut mer av {app_name}? Uppgradera till {app_pro} för en kraftfullare meddelandeupplevelse." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хочете отримати більше від {app_name}? Оновіться до {app_pro}, щоб мати потужніший досвід обміну повідомленнями." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "想充分体验 {app_name}?升级到 {app_pro} 享受更强大的消息体验。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要充分利用 {app_name}?升級為 {app_pro},享受更強大的訊息體驗。" + } } } }, @@ -359369,33 +363424,369 @@ "rateSession" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} qiymətləndirilsin?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohodnotit {app_name}?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} bewerten?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Rate {app_name}?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Calificar {app_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Calificar {app_name}?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noter {app_name} ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} को रेट करें?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valuta {app_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}を評価しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} beoordelen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oceń {app_name}?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaliar o {app_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evaluezi {app_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцените {app_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsätt {app_name}?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцінити {app_name}?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "给 {app_name} 评分?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "要為 {app_name} 評分嗎?" + } } } }, "rateSessionApp" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiqi qiymətləndir" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valoris l'aplicació" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohodnotit aplikaci" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App bewerten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Rate App" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calificar aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calificar aplicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noter l’application" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप को रेट करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valuta app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリを評価" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App beoordelen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oceń aplikację" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaliar aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evaluează aplicația" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцените ПО" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsätt appen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцінити застосунок" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "评分应用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "評分應用程式" + } } } }, "rateSessionModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çox şad olduq ki, {app_name} tətbiqindən həzz alırsınız. Bircə dəqiqəniz varsa, bizi {storevariant} üzərində qiymətləndirin, çünki bu, başqalarının şəxsi və təhlükəsiz mesajlaşmanı kəşf etməsinə kömək edir!" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ens alegrem que gaudiu {app_name}, si teniu un moment, ens classifiqueu a la {storevariant} ajuda els altres a descobrir missatgeria privada i segura!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jsme rádi, že se vám {app_name} líbí. Pokud máte chvíli času, ohodnoťte nás na {storevariant} abyste ostatním pomohli objevit soukromou a bezpečnou komunikaci!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es freut uns, dass du {app_name} genießt! Wenn du einen Moment Zeit hast, hilft eine Bewertung im {storevariant} anderen dabei, private und sichere Nachrichten zu entdecken." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "We're glad you're enjoying {app_name}, if you have a moment, rating us in the {storevariant} helps others discover private, secure messaging!" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nos alegra que estés disfrutando de {app_name}. Si tienes un momento, calificarnos en {storevariant} ayuda a otros a descubrir la mensajería privada y segura." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nos alegra que estés disfrutando de {app_name}. Si tienes un momento, calificarnos en {storevariant} ayuda a otros a descubrir la mensajería privada y segura." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes ravis que vous appréciiez {app_name}. Si vous avez un instant, une évaluation sur {storevariant} aiderait d'autres personnes à découvrir la messagerie privée et sécurisée !" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हमें खुशी है कि आपको {app_name} पसंद आ रहा है, यदि आपके पास एक क्षण है, तो {storevariant} पर हमारी रेटिंग देने से दूसरों को निजी, सुरक्षित मैसेजिंग खोजने में मदद मिलती है!" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siamo felici che ti stia piacendo {app_name}. Se hai un momento, lascia una valutazione su {storevariant}, aiuterà altri a scoprire la messaggistica privata e sicura!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} をご利用いただきありがとうございます。お時間があれば、{storevariant} で評価していただけると、他の方がプライベートで安全なメッセージを見つける手助けになります!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fijn dat je {app_name} leuk vindt! Als je een momentje hebt, helpt een beoordeling in de {storevariant} anderen om privé en veilig berichten te ontdekken!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cieszymy się, że podoba Ci się {app_name}. Jeśli masz chwilę, oceń nas w {storevariant}, aby pomóc innym odkryć prywatne i bezpieczne wiadomości!" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ficamos felizes por estar a gostar do {app_name}. Se tiver um momento, dar-nos uma avaliação na {storevariant} ajuda outros a descobrir mensagens privadas e seguras!" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne bucurăm că îți place {app_name}, dacă ai un minut, o evaluare în {storevariant} îi ajută și pe alții să descopere mesageria privată și sigură!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мы рады, что вам нравится {app_name}. Если у вас есть минутка, оцените нас в {storevariant}, это поможет другим открыть для себя конфиденциальный и безопасный обмен сообщениями!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vi är glada att du gillar {app_name}, om du har en stund hjälper det andra att upptäcka privat och säker meddelandehantering om du betygsätter oss i {storevariant}!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ми раді, що вам подобається {app_name}. Якщо маєте хвильку, оцініть нас у {storevariant} — це допоможе іншим знайти безпечний та приватний спосіб спілкування!" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很高兴您喜欢 {app_name},如果方便的话,请在 {storevariant} 上为我们评分,这将帮助他人发现私密、安全的消息方式!" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很高興您喜歡使用 {app_name},如果有空,請在 {storevariant} 中給我們評分,幫助更多人找到私密、安全的訊息工具!" + } } } }, @@ -361390,12 +365781,30 @@ "value" : "उत्तर प्राप्त हुआ" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fogadott válasz" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jawaban Diterima" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risposta ricevuta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答を受信しました" + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -361438,6 +365847,18 @@ "value" : "Odebrano odpowiedź" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resposta recebida" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Răspuns primit" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -361473,6 +365894,12 @@ "state" : "translated", "value" : "已收到应答" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "已接收回應" + } } } }, @@ -361521,6 +365948,18 @@ "value" : "Receiving Call Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta de llamada" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -361533,12 +365972,30 @@ "value" : "कॉल ऑफर प्राप्त हो रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hívás ajánlat fogadása" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menerima Penawaran Panggilan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricezione offerta di chiamata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話オファーを受信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -361557,6 +366014,18 @@ "value" : "Otrzymanie oferty połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A receber oferta de chamada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se primește oferta de apel" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -361586,6 +366055,12 @@ "state" : "translated", "value" : "正在接收通话邀请" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通話邀請" + } } } }, @@ -361628,6 +366103,18 @@ "value" : "Receiving Pre Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta previa" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta previa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -361640,12 +366127,30 @@ "value" : "प्री ऑफर प्राप्त हो रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előajánlás beérkeztetése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menerima Pra-Penawaran" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricezione pre-offerta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "事前オファーを受信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -361664,6 +366169,18 @@ "value" : "Oferta odbioru" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A receber pré-oferta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se primește oferta preliminară" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -361693,6 +366210,18 @@ "state" : "translated", "value" : "Đang có cuộc gọi đến" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通话邀请" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通話邀請" + } } } }, @@ -362193,7 +366722,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Hesabınıza müraciəti itirməmək üçün geri qaytarma parolunuzu saxlayın." + "value" : "Hesabınıza erişimi itirməmək üçün geri qaytarma parolunuzu saxlayın." } }, "bal" : { @@ -363136,478 +367665,10 @@ "recoveryPasswordDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gebruik jou herstelwagwoord om jou rekening op nuwe toestelle te laai.

    Jou rekening kan nie herstel word sonder jou herstelwagwoord nie. Maak seker jy stoor dit iewers veilig en moenie dit met enigiemand deel nie." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "استخدم كلمة المرور لاستعادة التحميل على أجهزة جديدة.

    لا يمكن استعادة الحساب بدون كلمة المرور. تأكد من تخزينها في مكان آمن وسري - ولا تشاركها مع أي أحد." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hesabınızı yeni cihazlara yükləmək üçün geri qaytarma parolunuzu istifadə edin.

    Geri qaytarma parolunuz olmadan hesabınız geri qaytarıla bilməz. Təhlükəsiz və etibarlı yerdə saxladığınıza əmin olun və heç kəslə paylaşmayın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنے اکاؤنٹ کو نئے ڈیوائس پر لوڈ کرنے کے لئے اپنا ریکوری پاس ورڈ استعمال کریں۔

    آپ کا اکاؤنٹ آپ کے ریکوری پاس ورڈ کے بغیر بازیافت نہیں کیا جا سکتا۔ اسے محفوظ اور محفوظ جگہ پر ذخیرہ کرنا یقینی بنائیں — اور اسے کسی کے ساتھ شیئر نہ کریں۔" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выкарыстоўвайце свой пароль для аднаўлення, каб загрузіць свой уліковы запіс на новых прыладах.

    Ваш уліковы запіс не можа быць адноўлены без вашага пароля для аднаўлення. Захоўвайце яго ў бяспечным і надзейным месцы — і не дзяліце з нікога." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Използвайте вашата парола за възстановяване, за да заредите акаунта си на нови устройства.

    Вашият акаунт не може да бъде възстановен без вашата парола за възстановяване. Уверете се, че е съхранена някъде на сигурно място — и не я споделяйте с никого." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "নতুন ডিভাইসে আপনার অ্যাকাউন্ট লোড করতে আপনার রিকভারি পাসওয়ার্ড ব্যবহার করুন।

    আপনার রিকভারি পাসওয়ার্ড ছাড়া আপনার অ্যাকাউন্ট পুনরুদ্ধার করা যাবে না। নিশ্চিত করুন এটি নিরাপদ এবং সুরক্ষিত জায়গায় সংরক্ষণ করা হয়েছে এবং এটি কারও সাথে শেয়ার করবেন না।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilitza la teva contrasenya de recuperació per carregar el teu compte en nous dispositius.

    No es pot recuperar el teu compte sense la teva contrasenya de recuperació. Assegura't que està emmagatzemada en un lloc segur i no la comparteixis amb ningú." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Použijte své heslo pro obnovení pro načtení účtu na nových zařízeních.

    Bez hesla pro obnovení nelze obnovit účet. Ujistěte se, že je uložené na bezpečném místě – a nesdílejte ho s nikým." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Defnyddiwch eich chyfrinair adfer i lwytho eich cyfrif ar ddyfeisiau newydd.

    Ni ellir adfer eich cyfrif heb eich cyfrinair adfer. Sicrhewch ei fod yn cael ei storio yn rhywle diogel - ac na fyddwch yn ei rannu ag unrhyw un." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brug din recovery password til at indlæse din konto på nye enheder.

    Din konto kan ikke gendannes uden din recovery password. Sørg for, at den er opbevaret et sikkert sted, og del den ikke med nogen." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwende dein Wiederherstellungspasswort, um deinen Account auf neuen Geräten zu laden.

    Dein Account kann ohne dein Wiederherstellungspasswort nicht wiederhergestellt werden. Stelle sicher, dass es an einem sicheren Ort aufbewahrt ist – und teile es niemandem mit." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Χρησιμοποιήστε τον κωδικό ανάκτησης για να φορτώσετε τον λογαριασμό σας σε νέες συσκευές.

    Ο λογαριασμός σας δεν μπορεί να ανακτηθεί χωρίς τον κωδικό ανάκτησης. Βεβαιωθείτε ότι τον αποθηκεύσατε σε ασφαλές μέρος — και μην τον μοιραστείτε με κανέναν." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Use your recovery password to load your account on new devices.

    Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uzu vian recuđer passvorton por ŝarĝi vian konton sur novaj aparatoj.

    Via konto ne povas esti rekuperita sen via recuđer passvorto. Certigu, ke ĝi estas stokita ien sekura kaj ne dividas ĝin kun iu ajn." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilice su contraseña de recuperación para cargar su cuenta en nuevos dispositivos.

    Su cuenta no se puede recuperar sin su contraseña de recuperación. Asegúrese de guardarla en un lugar seguro — y no la comparta con nadie." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usa tu contraseña de recuperación para cargar tu cuenta en nuevos dispositivos.

    Tu cuenta no se puede recuperar sin tu contraseña de recuperación. Asegúrate de que esté guardada en un lugar seguro y no la compartas con nadie." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konto laadimiseks kasutage oma taastamissalasõna uutes seadmetes.

    Teie kontot ei saa taastada ilma taastamissalasõnata. Veenduge, et see oleks turvaliselt salvestatud ja ärge jagage seda kellelegi." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erabili zure berreskurapen pasahitza zure kontua kargatzeko gailu berrietan.

    Zure kontua ezin da berreskuratu berreskurapen pasahitzik gabe. Ziurtatu leku seguru batean gordeta dagoela eta ez partekatu inorekin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "از گذرواژه بازیابی خود برای بارگذاری حساب کاربری خود در دستگاه‌های جدید استفاده کنید.

    حساب شما بدون گذرواژه بازیابی‌تان بازیابی نخواهد شد. مطمئن شوید که آن را در مکانی امن ذخیره کرده‌اید و با کسی به اشتراک نگذارید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Käytä palautussalasanaa ladataksesi tilisi uusiin laitteisiin.

    Tiliäsi ei voida palauttaa ilman palautussalasanaa. Varmista, että se on tallennettu turvallisesti ja älä jaa sitä kenellekään." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use your recovery password to load your account on new devices.

    Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilisez votre mot de passe de récupération pour charger votre compte sur de nouveaux appareils.

    Votre compte ne peut pas être récupéré sans votre mot de passe de récupération. Assurez-vous qu'il soit stocké en lieu sûr et sécurisé - et ne le partagez avec personne." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use your recovery password para cargar a túa conta en novos dispositivos.

    A túa conta non se pode recuperar sen a túa recovery password. Asegúrate de que estea gardada nalgún lugar seguro e non a compartas con ninguén." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yi amfani da kalmar wucewa ta dawo da asusun naka a kan sabbin na'urori.

    Ba za a iya dawo da asusunku ba tare da kalmar wucewarku ta dawo ba. Tabbatar an adana ta wani wuri mai tsaro kuma kada ku raba ta da kowa ba." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "השתמש בסיסמת שחזור שלך לטעינת חשבונך על מכשירים חדשים.

    לא ניתן לשחזר שלך חשבון ללא סיסמת השחזור שלך. ודא שהיא שמורה במקום בטוח ומאובטח - אל תשתף אותה עם אחרים." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपना अकाउंट नए डिवाइस पर लोड करने के लिए अपने रिकवरी पासवर्ड का उपयोग करें।

    आपका अकाउंट आपके रिकवरी पासवर्ड के बिना पुनर्प्राप्त नहीं किया जा सकता है। सुनिश्चित करें कि इसे कहीं सुरक्षित रखें और इसे किसी के साथ साझा न करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoju lozinku za oporavak za učitavanje računa na novim uređajima.

    Vaš račun nije moguće oporaviti bez lozinke za oporavak. Osigurajte da je pohranjena na sigurnom mjestu i ne dijelite je s nikim." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A visszaállítási jelszavaddal új eszközökön is betöltheted a felhasználódat.

    A felhasználód nem állítható vissza a visszaálltási jelszó nélkül. Gondoskodj róla, hogy biztonságos helyen tárolod — és ne oszd meg senkivel." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Օգտագործեք ձեր վերականգնման գաղտնաբառը ձեր հաշիվը նոր սարքերում ներբեռնելու համար.

    Ձեր հաշիվը հնարավոր չէ վերականգնել առանց վերականգնման գաղտնաբառի: Համոզվեք, որ այն պահված է անվտանգ տեղում և չկիսեք այն ուրիշների հետ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gunakan kata sandi pemulihan Anda untuk memuat akun Anda di perangkat baru.

    Akun Anda tidak dapat dipulihkan tanpa kata sandi pemulihan Anda. Pastikan disimpan di tempat yang aman dan terlindungi — dan jangan bagikan kepada siapa pun." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilizza la tua password di recupero per caricare il tuo account su nuovi dispositivi.

    Il tuo account non può essere ripristinato senza la tua password di recupero. Assicurati di conservarla in un luogo sicuro, riservato e di non condividerla con nessuno." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リカバリパスワードを使用して、新しいデバイスでアカウントを読み込みます。

    リカバリパスワードがないとアカウントを復元できません。 安全な場所に保管し、他の人と共有しないでください。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "გამოიყენეთ თქვენი აღდგენის პაროლი ანგარიშის ახალი მოწყობილობებზე ჩასატვირთად.

    თქვენი ანგარიში ვერ აღდგება თქვენი აღდგენის პაროლის გარეშე. დარწმუნდით, რომ ეს მრავალსაფრთილად და უსაფრთხოდ არის შენახული — და არ გააზიაროთ ეს სხვა ადამიანებთან." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្រើពាក្យសម្ងាត់ដើម្បីទាញយកគណនីរបស់អ្នកលើឧបករណ៍ថ្មីៗ.

    គណនីរបស់អ្នកមិនអាចទាញយកកនឡើងវិញដោយគ្មាន Recovery Password។ សូមរក្សាវានៅកន្លែងសុវត្ថិភាព ហើយកុំចែករំលែកវាជាមួយនរណាម្នាក់ទេ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೊಸ ಸಾಧನಗಳಲ್ಲಿ ಲೋಡ್ ಮಾಡಲು ನಿಮ್ಮ ಪುನಃ ಸ್ವಾಸ್ತ್ಯದ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬಳಸಿ.

    ನಿಮ್ಮ ಪುನಃ ಸ್ವಾಸ್ತ್ಯದ ಪಾಸ್‌ವರ್ಡ್ ಇಲ್ಲದೆ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪುನಃ ಪಡೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ. ಇದು ಸುರಕ್ಷಿತವಾಗಿ ಮತ್ತು ಭದ್ರವಾಗಿ ಸಂಗ್ರಹಿಸಿರುವುದನ್ನು ಖಚಿತಪಡಿಸಿಕೊಂಡು—ಯಾರೊಂದಿಗೂ ಅದನ್ನು ಹಂಚಿಕೊಳ್ಳಬೇಡಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "새 장치에서 계정을 로드하려면 복구 비밀번호를 사용하십시오.

    복구 비밀번호 없이는 계정을 복구할 수 없습니다. 안전하게 보관하고 타인과 공유하지 않도록 주의하세요." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "بەکارھێنانی تێپەڕەوشەی گەڕاندنەکەت بۆ بارکردنی هەژمارەکەت لەسەر ئامێرە نوێکان.

    ئەکاونتەکەت بێ بەکردنی تێپەڕەوشەی گەڕاندنەکەت ناتواندرێت بەسەردەبڕێت. پێویستەوە تەواوەتی پارێزگای دەستیفێڵ بکەیت و وتاپیشت نیبیت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ji bo barkirina hesabê xwe ya li ser amadekarên nû şîfreya recoverîya xwe bikar bînin.

    Hesabên we ne matur nirx bikin jî. Da sûçiyên guman dikin ku ew li ciheke nehiş jî au reş bike û bi kesî re nebibe." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kozesa oba Recovery password yo okuwonya account ku kiga kya njawulo.

    Account yo teyeetaagulaamu soola kusasaanya oba Recovery password yo. Kakatila nkumu kakati lwaki tekiriza musajja yenna." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naudokite savo atkūrimo slaptažodį, kad įkeltumėte savo paskyrą į naujus įrenginius.

    Jūsų paskyra negali būti atkurta be jūsų atkūrimo slaptažodžio. Įsitikinkite, kad jis yra saugomas saugioje vietoje — ir nesidalinkite juo su niekuo." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lietojiet savu atjaunošanas paroli, lai ielādētu savu kontu jaunās ierīcēs.

    Jūsu kontu nevar atjaunot bez jūsu atjaunošanas paroles. Pārliecinieties, ka tā ir uzglabāta drošā vietā — un nedalieties ar to." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Користете ја вашата лозинка за враќање за да го вчитајте вашиот профил на нови уреди.

    Вашиот профил не може да се поврати без вашата лозинка за враќање. Осигурајте се дека е безбедно и сигурно сочувана — и не ја споделувајте со никој." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны аккаунтыг шинэ төхөөрөмжүүдэд ачаалахын тулд сэргээх нууц үгийг ашиглана уу.

    Таны аккаунтыг сэргээх нууц үггүйгээр сэргээх боломжгүй. Энэ нууц үгийг аюулгүй газар хадгалаарай – хэзээ ч хүнд бүү өгөөрэй." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gunakan recovery password anda untuk memuatkan akaun anda pada peranti baharu.

    Akaun anda tidak boleh dipulihkan tanpa recovery password anda. Pastikan ia disimpan di tempat yang selamat dan selamat — dan jangan kongsikan dengan sesiapa." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင့်အကောင့်ကို နောက်ထပ်စက်ပစ္စည်းများပေါ်တွင် တင်ရန် သင့် recovery password ကို အသုံးပြုပါ။

    သင့် recovery password မရှိပဲ သင့်အကောင့်ကို ပြန်ယူ၍ မရနိုင်ပါ။ ၎င်းကို ဘေးကင်းသောနေရာတစ်ခုတွင် သိမ်းဆည်းထားပြီး သက်သေပြရန် မမျှဝေပါနှင့်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk din Recovery password for å laste inn kontoen din på nye enheter.

    Kontot kan ikke gjenopprettes uten din Recovery password. Sørg for at den er lagret et trygt og sikkert sted — og ikke del den med noen." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk recovery password for å laste inn kontoen din på nye enheter.

    Kontoen din kan ikke gjenopprettes uten recovery password. Sørg for at det er lagret et trygt og sikkert sted – og del det ikke med noen." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको खाता नयाँ उपकरणहरूमा लोड गर्न आफ्नो पुनर्प्राप्ति पासवर्ड प्रयोग गर्नुहोस्।

    तपाईँको खाता बिना पुनर्प्राप्ति पासवर्ड पुनः प्राप्त गर्न सकिन्न। यो सुरक्षित र सुरक्षित ठाउँमा राखिएको सुनिश्चित गर्नुहोस् - र यसलाई कसैसँग साझा नगर्नुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gebruik uw herstelwachtwoord om uw account op nieuwe apparaten te laden.

    Uw account kan niet worden hersteld zonder uw herstelwachtwoord. Zorg ervoor dat het ergens veilig is opgeslagen – en deel het niet met anderen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk gjenopprettingsløsenordet ditt for å laste kontoen din på nye enheter.

    Kontoen din kan ikke gjenopprettes uten gjenopprettingsløsenordet. Sørg for at det er lagret et trygt sted — og del det ikke med noen." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gwiritsani ntchito chinsinsi chobwezeretsanso akaunti yanu pazida zatsopano.

    Akaunti yanu siyingathe kubwezeretsedwa popanda chinsinsicho. Onetsetsani kuti yatero pamtendere ndi chitetezo - ndipo musagawane aliyense." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਪਣਾ ਖਾਤਾ ਨਵੇਂ ਜੰਤਰਾਂ ਤੇ ਲੋਡ ਕਰਨ ਲਈ ਆਪਣਾ ਰਿਕਵਰੀ ਪਾਸਵਰਡ ਵਰਤੋ।

    ਆਪਣਾ ਖਾਤਾ ਰਿਕਵਰੀ ਪਾਸਵਰਡ ਤੋਂ ਬਿਨਾ ਪੁਨਰ ਪ੍ਰਾਪਤ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ। ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਇਹ ਕਿਸੇ ਸੁਰੱਖਿਅਤ ਅਤੇ ਸੁਰੱਖਿਅਤ ਸਥਾਨ ਤੇ ਸਟੋਰ ਕੀਤਾ ਗਿਆ ਹੈ - ਅਤੇ ਇਸ ਨੂੰ ਕਿਸੇ ਨਾਲ ਸਾਂਝਾ ਨਾ ਕਰੋ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aby wczytać konto na nowych urządzeniach, użyj hasła odzyskiwania.

    Nie można odzyskać konta bez hasła odzyskiwania. Upewnij się, że jest ono przechowywane w bezpiecznym miejscu i nie udostępniaj go nikomu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د خپل حساب د بارولو لپاره خپل بیا رغونه Password وکاروئ.

    پرته له خپل بیا رغونه Password څخه ستاسو حساب بیا رغول نشي. ډاډ ترلاسه کړئ چې دا خوندي او خوند ځای کې ساتل شوی دی - او دا له هیچا سره شریک نه کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use sua senha de recuperação para carregar sua conta em novos dispositivos.

    Sua conta não pode ser recuperada sem sua senha de recuperação. Certifique-se de armazená-la em um lugar seguro e não a compartilhe com ninguém." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use a sua chave de recuperação para carregar a sua conta em novos dispositivos.

    A sua conta não pode ser recuperada sem a sua chave de recuperação. Certifique-se de que está armazenada num lugar seguro — e não a partilhe com ninguém." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folosește parola de recuperare pentru a încărca contul pe dispozitive noi.

    Contul nu poate fi recuperat fără parola de recuperare. Asigurați-vă că este stocată într-un loc sigur și securizat – și nu o împărtășiți nimănui." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Используйте пароль восстановления, чтобы загрузить свою учетную запись на новых устройствах.

    Ваша учетная запись не может быть восстановлена без пароля восстановления. Убедитесь, что он хранится в безопасном месте, и не передавайте его никому." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoju Recovery Password za učitavanje svog naloga na nove uređaje.

    Vaš račun ne može biti oporavljen bez vaše Recovery Password. Uverite se da je čuvana negde sigurno i bezbedno – i nemojte je podeliti ni sa kim." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ ගිණුම නැවත ආරම්භ කිරීමට නැවත ආරම්භ කරන්න පරිශීලන මුරපදය භාවිතා කරන්න.

    ඔබගේ පරිශීලන මුරපදය නැත්තහොත් ඔබේ ගිණුම නැවත ලබා ගැනීමට නොහැක. ආරක්ෂිත සහ ආරක්ෂිත ස්ථානයක එය ගබඩා කර ඇති බවට වග බලා ගන්න — එය කිසිවෙකුට හුවමාරු නොකරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Použite svoje recovery password na načítanie účtu na nových zariadeniach.

    Bez recovery password váš účet nebude možné obnoviť. Uistite sa, že je uložené na bezpečnom mieste a nezdieľajte ho s nikým." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uporabite geslo za obnovitev, da naložite svoj račun na novih napravah.

    Vašega računa ni mogoče obnoviti brez obnovitvenega gesla. Poskrbite, da bo shranjeno na varnem mestu — in ga ne delite z nikomer." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Përdorni fjalëkalimin e rikuperimit për të ngarkuar llogarinë tuaj në pajisje të reja.

    Llogaria juaj nuk mund të rikuperohet pa fjalëkalimin tuaj të rikuperimit. Sigurohuni që ta keni ruajtur në një vend të sigurt dhe mos e ndani me askënd." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Користите своју Recovery password да учитате свој налог на нове уређаје.

    Ваш налог не може бити опорављен без ваше Recovery password. Обавезно га чувајте на сигурном месту и не делите га са никим." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoj recovery password da učitate svoj nalog na novim uređajima.

    Vaš nalog se ne može oporaviti bez recovery password-a. Uverite se da je čuvan na sigurnom i ne delite ga ni sa kim." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Använd din återställningslösenord för att ladda ditt konto på nya enheter.

    Ditt konto kan inte återställas utan ditt återställningslösenord. Se till att det lagras säkert och dela det inte med någon." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tumia nywila ya urejeshaji kufungua akaunti yako kwenye vifaa vipya.

    Akaunti yako haiwezi kurejeshwa bila nywila yako ya urejeshaji. Hakikisha umeihifadhi mahali salama na siri — usishiriki na yeyote." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் விளம்பரச் சொந்தக் குறியீட்டை பயன்படுத்தி உங்கள் கணக்கை புதிய சாதனங்களில் ஏற்றுக.

    உங்கள் விளம்பரச் சொந்தக் குறியீடு இல்லாமல் உங்கள் கணக்கை மீட்டெடுக்க முடியாது. அது பாதுகாப்பான மற்றும் பாதுகாப்பான இடத்தில் சேமிக்கப்படுவதை உறுதிசெய்க - மற்றும் அதைப் பிறரிடம் பகிர வேண்டாம்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ ఖాతాను కొత్త పరికరాలపై లోడ్ చేయడానికి మీ రికవరీ పాస్‌వర్డ్‌ని ఉపయోగించండి.

    మీ రికవరీ పాస్‌వర్డ్ లేకుండా మీ ఖాతాను పునరుద్ధరించలేరు. అది ఎక్కడైనా సురక్షితంగా నిల్వ చేయబడిందని నిర్ధారించుకోండి – మరియు దానిని ఎవరికీ పంచుకోకండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ใช้รหัสผ่านการกู้คืนของคุณเพื่อโหลดบัญชีของคุณบนอุปกรณ์ใหม่.

    บัญชีของคุณจะไม่สามารถกู้คืนได้หากไม่มีรหัสผ่านการกู้คืนของคุณ. ตรวจสอบให้แน่ใจว่ามีการเก็บไว้อย่างปลอดภัยและไม่แบ่งปันให้ใคร." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hesabınızı yeni cihazlarda yüklemek için kurtarma şifrenizi kullanın.

    Kurtarma şifreniz olmadan hesabınız geri yüklenemez. Güvende olduğundan emin olun ve kimseyle paylaşmayın." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Використовуйте свій пароль для відновлення, щоб завантажити свій обліковий запис на нових пристроях.

    Ваш обліковий запис не може бути відновлений без вашого пароля для відновлення. Переконайтеся, що він зберігається десь у надійному місці та не передавайте його нікому." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنے اکاؤنٹ کو نئے آلات پر لوڈ کرنے کے لیے اپنی بازیابی کا پاس ورڈ استعمال کریں۔

    آپ کا اکاؤنٹ آپ کے بازیابی کے پاس ورڈ کے بغیر بازیافت نہیں ہو سکتا۔ یقینی بنائیں کہ یہ کہیں محفوظ اور محفوظ رکھا گیا ہے — اور کسی کے ساتھ اسے شیئر نہ کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hisobingizni yangi qurilmalarda yuklash uchun qayta parolingizdan foydalaning.

    Hisobingiz qayta parolsiz tiklanmaydi. Unga xavfsiz va ishonchli joyda saqlanganidan ishonch hosil qiling va hech kimga oshkor qilmang." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dùng mật khẩu khôi phục của bạn để tải tài khoản của bạn trên các thiết bị mới.

    Tài khoản của bạn không thể được khôi phục nếu không có mật khẩu khôi phục của bạn. Hãy chắc chắn rằng nó được lưu trữ ở một nơi an toàn và bảo mật — và đừng chia sẻ nó với bất kỳ ai." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Faka iphasiwedi yakho yokufumana kwakhona ukuze ufake iakhawunti yakho kwizixhobo ezintsha.

    Iakhawunti yakho ayinakufumaneka ngaphandle kwephasiwedi yakho yokufumana kwakhona. Qinisekisa ukuba iyonke indawo ekhuselekileyo kwaye ungayabelani nabani na." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用您的恢复密码在新设备上加载您的帐户。

    没有您的恢复密码,您的帐户将无法恢复。请确保将它存储在安全的地方,并且不要与任何人分享。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請使用您的恢復密碼在新裝置上載入您的帳戶。

    若沒有恢復密碼將無法恢復您的帳戶。請確保將其儲存於安全的地方—且不要與任何人分享。" + "value" : "Use your recovery password to load your account on new devices.

    Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." } } } @@ -364178,6 +368239,12 @@ "value" : "Hiba történt a helyreállítási jelszó betöltése közben.

    Exportálja a naplófájlokat, majd töltse fel azokat a(z) {app_name} segítségével az ügyfélszolgálatnak a probléma megoldása érdekében." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore durante il caricamento della tua password di recupero.

    Esporta i log, quindi invia il file tramite il Centro Assistenza di {app_name} per aiutare a risolvere il problema." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -364202,6 +368269,18 @@ "value" : "Wystąpił błąd podczas próby wczytania hasła odzyskiwania.

    Wyeksportuj swoje dzienniki, a następnie prześlij plik za pośrednictwem pomocy technicznej aplikacji {app_name}, aby pomóc w rozwiązaniu tego problemu." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro ao tentar carregar a sua palavra-passe de recuperação.

    Por favor exporte os seus logs e depois envie o ficheiro através do Centro de Ajuda do {app_name} para ajudar a resolver este problema." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "A apărut o eroare la încărcarea parolei de recuperare.

    Te rugăm să exporți jurnalele, apoi să încarci fișierul prin intermediul Biroului de asistență {app_name} pentru a ajuta la soluționarea acestei probleme." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -364237,6 +368316,12 @@ "state" : "translated", "value" : "尝试加载您的恢复密码时发生错误。

    请导出您的日志,然后通过{app_name}帮助服务台上传文件以帮助解决此问题。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "載入您的恢復密碼時發生錯誤。

    請匯出您的日誌,然後透過 {app_name} 的協助台上傳檔案以解決此問題。" + } } } }, @@ -367596,484 +371681,10 @@ "recoveryPasswordHidePermanentlyDescription2" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Is jy seker jy wil jou herstel wagwoord permanent op hierdie toestel versteek? Dit kan nie ongedaan gemaak word nie." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من إخفاء كلمة مرور الاسترداد الخاصة بك على هذا الجهاز نهائيًا؟ لا يمكن التراجع عن هذا." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geri qaytarma parolunuzu bu cihazda həmişəlik gizlətmək istədiyinizə əminsiniz? Bunun geri dönüşü yoxdur." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "دم کی لحاظ انت کہ ایی خفیہ استعانت کوڈ ایی ڈیوائیس سرمنداً چھپا بکنی؟ ایی خال ھچگاں نہ بیت." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы ўпэўненыя, што жадаеце пастаянна схаваць ваш канчатковы пароль аднаўлення на гэтай прыладзе? Гэта немагчыма адмяніць." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сигурен ли си, че искаш да скриеш своята възстановителна парола за постоянно на това устройство? Това действие не може да бъде отменено." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনি কি এই যন্ত্রে আপনার পুনরুদ্ধার পাসওয়ার্ড স্থায়ীভাবে গোপন করতে নিশ্চিত? এটি পূর্বাবস্থায় ফেরানো যাবে না।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esteu segur que voleu amagar permanentment la vostra contrasenya de recuperació en aquest dispositiu? Això no es pot desfer." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete trvale skrýt heslo pro obnovení na tomto zařízení? Tuto akci nelze vrátit." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ydych chi'n siŵr eich bod am guddio eich cyfrinair adfer am byth ar y ddyfais hon? Ni ellir dadwneud hyn." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du permanent vil skjule din gendannelseskode på denne enhed? Dette kan ikke fortrydes." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bist du sicher, dass du das Wiederherstellungspasswort auf diesem Gerät dauerhaft ausblenden möchtest? Dies kann nicht mehr rückgängig gemacht werden." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Είστε βέβαιοι ότι θέλετε να αποκρύψετε μόνιμα τον κωδικό σας ανάκτησης σε αυτήν τη συσκευή; Αυτό δεν μπορεί να αναιρεθεί." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to permanently hide your recovery password on this device? This cannot be undone." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ĉu vi certas, ke vi volas porĉiame kaŝi vian reakiraj pasvorton sur ĉi tiu aparato? Ĉi tio ne povas esti malfaro." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro que desea ocultar permanentemente su contraseña de recuperación en este dispositivo? Esto no se puede deshacer." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro que desea ocultar permanentemente su contraseña de recuperación en este dispositivo? Esto no se puede deshacer." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kas olete kindel, et soovite oma taastamisparooli sellel seadmel jäädavalt peita? Seda ei saa tühistada." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ziur zaude zure berreskurapen pasahitza gailu honetan betirako ezkutatu nahi duzula? Ezin da desegin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "آیا مطمئن هستید که می‌خواهید گذرواژه بازیابی خود را روی این دستگاه به صورت دائمی پنهان کنید؟ این کار قابل برگشت نیست." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haluatko varmasti piilottaa palautussalasanan pysyvästi tässä laitteessa? Tätä ei voi peruuttaa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sigurado ka bang gusto mong permanenteng itago ang iyong recovery password sa device na ito? Hindi na ito mababawi." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir masquer définitivement votre mot de passe de récupération sur cet appareil ? Cela ne peut pas être annulé." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tes a certeza de querer ocultar permanentemente o teu contrasinal de recuperación neste dispositivo? Isto non se pode desfacer." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ka tabbata kana so ka asirce kalmar dawowa dindindin a wannan na'ura? Wannan ba za a iya warwarewa ba." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "האם אתה בטוח שברצונך להסתיר את הסיסמה לשחזור שלך לצמיתות במכשיר זה? זה לא ניתן לביטול." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "क्या आप वाकई अपने रिकवरी पासवर्ड को इस डिवाइस पर स्थायी रूप से छिपाना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeste li sigurni da želite trajno sakriti zaporku za oporavak na ovom uređaju? To se ne može poništiti." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztos, hogy véglegesen el akarod rejteni a visszaállítási jelszavad ezen az eszközön? Ezt nem lehet visszafordítani." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Վստա՞հ եք, որ ուզում եք մշտապես թաքցնել Ձեր վերականգնման գաղտնաբառը այս սարքի վրա: Սա անհնար է հետքը քայլել:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apakah Anda yakin ingin menyembunyikan sandi pemulihan secara permanen di perangkat ini? Hal ini tidak dapat dibatalkan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sei sicuro di voler nascondere permanentemente la tua password di recupero su questo dispositivo? Questa azione non può essere annullata." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この端末でリカバリパスワードを永久に非表示にしてもよろしいですか? これは元に戻すことはできません。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "დარწმუნებული ხართ, რომ გსურთ აღდგენის პაროლის ამ მოწყობილობაზე სამუდამოდ დამალვა? ამის დაბრუნება შეუძლებელია." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "តើអ្នកប្រាកដទេថាចង់លាក់ពាក្យសម្ងាត់សង្គ្រោះរបស់អ្នកដោយស្នាក់នៅលើឧបករណ៍នេះជាអចិន្ត្រៃយ៍? វាមិនអាចត្រូវបានមិនធ្វើវិញបានទេ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನೀವು ಈ ಸಾಧನದಲ್ಲಿ ನಿಮ್ಮ ಪುನಃ ಪಡೆಯುವ ರಹಸ್ಯ ಪದವನ್ನು ಶಾಶ್ವತವಾಗಿ ಮರೆಮಾಡಲು ಖಚಿತವಾಗಿದ್ದೀರಾ? ಇದನ್ನು ರದ್ದುಮಾಡಲಾಗುವುದಿಲ್ಲ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말 이 장치에서 복구 비밀번호를 영구적으로 숨기겠습니까? 이 작업은 되돌릴 수 없습니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "دڵنیایت دەتەوێت تەنها نهێنی کلید وشەی گەڕاندنەوە لەسەر ئەم چەشەمەیە بسڕیتەوە؟ ئەمە بشێوی چارنەماوە." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu piştrast î ku tu dixwazî şîfreya xwe ya rizgarkirinê li ser vê cîhazê bi daîmî veşêrî? Ev nayê vegerandin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oli mukakafu nti oyagala okutereka ebisumuluzo by'okuddabiriza ku kidirisa kino emirembe gyonna? Kino tekijja kusoboka okujeemebwa." - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ທ່ານຫມັ່ນໃຈບໍ່ວ່າທ່ານຈະເມືອນຊົ່ວຫມົດ Recovery password ຂອງທ່ານເລັວ? ການນັ້ນບໍ່ສາມາດຖືກຄືນໄດ້." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ar tikrai norite visam laikui paslėpti savo atkūrimo slaptažodį šiame įrenginyje? To atšaukti negalima." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vai esat pārliecināts, ka vēlaties pastāvīgi slēpt savu atkopšanas paroli šajā ierīcē? Tas nav atgriezenisks." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дали сте сигурни дека сакате трајно да ја сокриете вашата лозинка за обновување на овој уред? Ова не може да се поништи." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Та энэхүү нууц үгийг энэ төхөөрөмжөөс нуухдаа итгэлтэй байна уу? Энэ үйлдлийг буцаах боломжгүй." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adakah anda yakin anda mahu menyembunyikan kata laluan pemulihan anda secara kekal pada peranti ini? Ini tidak boleh diundurkan." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ဤစက်ကိရိယာတွင် သင့် ပြန်လည်ရယူရေးစကားဝှက်ကို အပြီးဖျောက်လိုသည်မှာ သေချာပါသလား။ ၎င်းကို ပြန်ဆောင်ရွက်၍မရပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på at du vil permanent skjule ditt Recovery Password på denne enheten? Dette kan ikke angres." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på at du vil skjule gjenopprettingspassordet ditt permanent på denne enheten? Dette kan ikke angres." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंले आफ्नो पुनःस्थापना पासवर्ड स्थायी रूपमा यो उपकरणमा लुकाउन निश्चित हुनुहुन्छ? यो पूर्ववत गर्न सकिदैन।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet u zeker dat u uw herstelwachtwoord permanent wilt verbergen op dit apparaat? Dit kan niet ongedaan gemaakt worden." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på at du ønskjer å skjule ditt gjenopprettingspassord for godt på denne eininga? Dette kan ikkje angre." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mukutsimikizika kuti mukufuna kubisitsa chinsinsi chanu chobwezeretsanso pa chipangizo ichi? Izi sizingathe kusinthidwa." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਕੀ ਤੁਸੀਂ ਯਕੀਨਨ ਆਪਣੇ ਮਨੁੱਖੀ ਕਰੋੜੀ ਸੰਕੇਤਾਂ ਨੂੰ ਇਸ ਜੰਤਰ 'ਤੇ ਅਸਥਾਈ ਤੌਰ ਤੇ ਛੁਪਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਹ ਮੁੜ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz trwale ukryć hasło odzyskiwania na tym urządzeniu? Nie można tego cofnąć." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "آیا تاسو ډاډه یاست چې غواړئ خپل recovery password په دې وسیله کې دایمي پټ کړئ؟ دا نشي بیرته اخیستل کیدی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tem certeza de que deseja ocultar permanentemente sua senha de recuperação neste dispositivo? Isso não pode ser desfeito." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tem a certeza de que deseja esconder permanentemente a sua chave de recuperação neste dispositivo? Isso não pode ser desfeito." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ești sigur/ă că dorești ascunderea definitivă a parolei de recuperare de pe acest dispozitiv? Această acțiune nu poate fi anulată." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы уверены, что хотите навсегда скрыть ваш пароль восстановления на этом устройстве? Это действие не может быть отменено." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jesi li siguran da želiš trajno sakriti svoju recovery password na ovom uređaju? Ovo se ne može poništiti." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබේ ප්‍රතිසාධන මුරපදය මෙම උපාංගයෙන් ස්ථිරවම සඟවීමට අවශ්‍ය බව විශ්වාසද? මෙය හකුලා නොගත හැකි වේ." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naozaj chcete trvalo skryť frázu na obnovenie na tomto zariadení? Toto sa nedá vrátiť späť." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ali res želite trajno skriti svoje obnovitveno geslo na tej napravi? Tega ni mogoče razveljaviti." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jeni të sigurt që doni ta fshini përgjithmonë fjalëkalimin e rikuperimit në këtë pajisje? Kjo nuk mund të zhbëhet." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Да ли сте сигурни да желите трајно да сакријете вашу Recovery Password на овом уређају? Ово не може бити поништено." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Da li ste sigurni da želite da trajno sakrijete svoju Recovery password na ovom uređaju? Ovo ne može biti poništeno." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Är du säker på att du vill permanent dölja ditt återställningslösenord på denna enhet? Detta kan inte ångras." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je, una uhakika unataka kuficha recovery password yako kabisa kwenye kifaa hiki? Hii haiwezi kubatilishwa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் மீட்பு கடவுச்சொல்லை இந்த சாதனத்தில் நிரந்தரமாக மறைக்க விரும்புகிறீர்களா? இது ஆவணப்படுத்த முடியாது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీరు మీ రికవరీ పాస్వర్డ్‌ను ఈ పరికరంలో శాశ్వతంగా దాచాలనుకుంటున్నారా? ఇది rückgängig చేయడం సాధ్యం కాదు." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "คุณแน่ใจหรือไม่ว่าต้องการซ่อนไว้รหัสผ่านการกู้คืนบนอุปกรณ์นี้อย่างถาวร? ไม่สามารถย้อนกลับได้" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu cihazdaki kurtarma şifrenizi kalıcı olarak gizlemek istediğinizden emin misiniz? Bu geri alınamaz." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете назавжди приховати пароль для відновлення на цьому пристрої? Це не можна буде скасувати." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "کیا آپ واقعی اپنے ریکوری پاس ورڈ کو اس ڈیوائس پر مستقل طور پر چھپانا چاہتے ہیں؟ یہ رد نہیں ہو سکے گا۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haqiqatan ham tiklash parolingizni ushbu qurilmada doimiy tarzda yashirmoqchimisiz? Bu qaytarib bo'lmaydi." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn có chắc chắc rằng bạn muốn ẩn mật khẩu khôi phục của bạn vĩnh viễn trên thiết bị này? Điều này không thể hồi phục." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uqinisekile ukuba ufuna ukufihla rhoqo iphasiwedi yakho yokubuyisela kule sixhobo? Oku akunakubuyiselwa." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您确定要在此设备上永久隐藏您的恢复密码吗?该操作无法撤消。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您確定要在此裝置上永久隱藏您的恢復密碼嗎?此操作無法撤銷。" + "value" : "Are you sure you want to permanently hide your recovery password on this device?

    This cannot be undone." } } } @@ -369994,6 +373605,17 @@ } } }, + "recoveryPasswordVisibility" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recovery Password Visibility" + } + } + } + }, "recoveryPasswordWarningSendDescription" : { "extractionState" : "manual", "localizations" : { @@ -370012,7 +373634,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu, sizin geri qaytarma parolunuzdur. Kiməsə göndərsəniz, hesabınıza tam müraciət edə bilər." + "value" : "Bu, sizin geri qaytarma parolunuzdur. Kiməsə göndərsəniz, hesabınıza tam erişə bilər." } }, "bal" : { @@ -370524,6 +374146,18 @@ "value" : "Rekrei grupon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volver a crear grupo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volver a crear grupo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -370554,6 +374188,12 @@ "value" : "Ricrea gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループを再作成" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -370572,6 +374212,18 @@ "value" : "Odtwórz Grupę" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recriar grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recreează grup" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -370613,6 +374265,12 @@ "state" : "translated", "value" : "重新创建群组" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新建立群組" + } } } }, @@ -371098,6 +374756,12 @@ "remainingCharactersOverTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajın uzunluğunu {count} qədər azalt" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -371110,29 +374774,143 @@ "value" : "Zkraťte délku zprávy o {count}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht um {count} Zeichen kürzen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Reduce message length by {count}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduce la longitud del mensaje en {count}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduce la longitud del mensaje en {count}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réduis la longueur du message de {count}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश की लंबाई {count} तक घटाएं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenet hosszának csökkentése ennyivel: {count}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riduci la lunghezza del messaggio di {count}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージをあと{count}字削減してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verkort berichtlengte met {count}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróć wiadomość o {count} znaków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduza o comprimento da mensagem em {count}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redu lungimea mesajului cu {count}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уменьшить длину на {count}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förkorta meddelandet med {count} tecken" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj uzunluğunu {count} karakter azaltın" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скоротіть довжину повідомлення на {count}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "将消息长度减少 {count} 个字符" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請減少訊息長度 {count} 個字元" + } } } }, "remainingCharactersTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld xarakter qaldı" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld xarakter qaldı" + } + } + } + } + }, "ca" : { "variations" : { "plural" : { @@ -371181,6 +374959,24 @@ } } }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Zeichen verbleibt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Zeichen verbleiben" + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -371199,6 +374995,78 @@ } } }, + "es-419" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carácter restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "es-ES" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carácter restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractères restants" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractères restants" + } + } + } + } + }, + "hi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld वर्ण शेष" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld वर्ण शेष" + } + } + } + } + }, "hu" : { "variations" : { "plural" : { @@ -371217,6 +375085,24 @@ } } }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carattere rimanente" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caratteri rimanenti" + } + } + } + } + }, "ja" : { "variations" : { "plural" : { @@ -371235,10 +375121,214 @@ "value" : "{count}자 입력 가능" } }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld teken resterend" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tekens resterend" + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostały %lld znaki" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostało %lld znaków" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostał %lld znak" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostało %lld znaków" + } + } + } + } + }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractere restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractere rămase" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracter rămas" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld de caractere rămase" + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символа осталось" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символов осталось" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символ остался" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символов осталось" + } + } + } + } + }, + "sv-SE" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tecken kvar" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tecken kvar" + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld karakter kaldı" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld karakter kaldı" + } + } + } + } + }, "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{count} символів залишилось" + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символ залишився" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + } + } + } + }, + "zh-CN" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "还剩 %lld 个字符" + } + } + } + } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "剩餘 %lld 個字元" + } + } + } } } } @@ -375078,22 +379168,250 @@ "reviewLimit" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rəy limiti" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límit de revisió" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omezení hodnocení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bewertungsgrenze" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Review Limit" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límite de reseñas" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límite de reseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite d’évaluations" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समीक्षा सीमा" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite recensioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レビュー制限" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beoordelingslimiet" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limit opinii" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite de avaliações" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limită de recenzii" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ограничение на отзыв" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsättningsgräns" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ліміт на відгуки" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "评价次数已达上限" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "評分次數上限" + } } } }, "reviewLimitDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deyəsən, təzəlikcə {app_name} üçün rəy bildirmisiniz, əks-əlaqəniz üçün təşəkkürlər!" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sembla que ja has revisat {app_name} recentment, gràcies pels teus comentaris!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zdá se, že jste nedávno hodnotili {app_name}. Děkujeme vám za vaši zpětnou vazbu!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast {app_name} anscheinend kürzlich bewertet – danke für dein Feedback!" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "It looks like you've already reviewed {app_name} recently, thanks for your feedback!" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que ya has calificado {app_name} recientemente. ¡Gracias por tus comentarios!" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que ya has calificado {app_name} recientemente. ¡Gracias por tus comentarios!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il semble que vous ayez déjà évalué {app_name} récemment. Merci pour vos retours !" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐसा लगता है कि आपने हाल ही में {app_name} की समीक्षा पहले ही कर दी है, आपकी प्रतिक्रिया के लिए धन्यवाद!" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sembra che tu abbia già recensito {app_name} di recente, grazie per il tuo feedback!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近すでに {app_name} を評価いただいているようです。フィードバックありがとうございます!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het lijkt erop dat je {app_name} onlangs al hebt beoordeeld, bedankt voor je feedback!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wygląda na to, że ostatnio już oceniałeś {app_name}, dziękujemy za Twoją opinię!" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que já avaliou recentemente o {app_name}, obrigado pelo seu feedback!" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se pare că ai evaluat deja recent {app_name}, îți mulțumim pentru feedback!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Похоже, вы недавно уже оставляли отзыв о {app_name}, спасибо за ваш отзыв!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Det verkar som att du redan betygsatt {app_name} nyligen – tack för din feedback!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Здається, ви нещодавно вже залишали відгук про {app_name}. Дякуємо за ваш зворотний зв'язок!" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您最近似乎已经评价过 {app_name},感谢您的反馈!" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "看起來您最近已經對 {app_name} 給予評價,感謝您的回饋!" + } } } }, @@ -378504,7 +382822,7 @@ "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Požadovat upozornění, když kontakt pořídí snímek obrazovky v individuálním chatu." + "value" : "Požadovat upozornění, když kontakt pořídí snímek obrazovky chatu jeden na jednoho." } }, "cy" : { @@ -385760,6 +390078,12 @@ "value" : "حدد أيقونة التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu seç" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -385772,6 +390096,12 @@ "value" : "Vybrat ikonu aplikace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Symbol auswählen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -385784,12 +390114,30 @@ "value" : "Elektu piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar icono de la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar icono de la aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionner l'icône de l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन चुनें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -385802,6 +390150,18 @@ "value" : "Pilih ikon aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona icona dell'app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリアイコンを選択" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -385820,11 +390180,53 @@ "value" : "Wybierz ikonę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar ícone da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selectează pictograma aplicației" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выбрать иконку приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Välj appikon" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama ikonu seç" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оберіть значок застосунку" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择应用图标" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇應用程式圖示" + } } } }, @@ -386831,6 +391233,18 @@ "value" : "Sending Call Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando oferta de llamada" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando oferta de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -386843,12 +391257,30 @@ "value" : "कॉल ऑफर भेजा जा रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hívás ajánlás küldése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengirim Penawaran Panggilan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invio offerta di chiamata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話オファーを送信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -386879,6 +391311,18 @@ "value" : "Wysyłanie oferty połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar oferta de chamada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimite oferta de apel" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -386908,6 +391352,12 @@ "state" : "translated", "value" : "正在发送通话邀请" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送通話邀請" + } } } }, @@ -386950,6 +391400,18 @@ "value" : "Sending Connection Candidates" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando candidatos de conexión" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando candidatos de conexión" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -386962,12 +391424,30 @@ "value" : "कनेक्शन उम्मीदवार भेजे जा रहे हैं" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapcsolat jelöltek küldése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengirim Kandidat Sambungan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invio candidati per la connessione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続候補を送信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -386986,6 +391466,18 @@ "value" : "Wysyłanie kandydatów do połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar candidatos de ligação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit candidații pentru conexiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -387015,6 +391507,18 @@ "state" : "translated", "value" : "Đang nhận thông tin các kết nối khả dĩ" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送连接候选人" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送連線候選項目" + } } } }, @@ -388000,7 +392504,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Dataları təmizlə" + "value" : "Veriləri təmizlə" } }, "bal" : { @@ -390392,6 +394896,12 @@ "sessionNetworkCurrentPrice" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı {token_name_short} qiyməti" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390404,6 +394914,12 @@ "value" : "Aktuální cena {token_name_short}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktueller {token_name_short}-Preis" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -390416,12 +394932,30 @@ "value" : "Nuna prezo de {token_name_short}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precio actual de {token_name_short}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precio actual de {token_name_short}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Prix actuel du {token_name_short}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वर्तमान {token_name_short} मूल्य" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -390434,6 +394968,18 @@ "value" : "Harga {token_name_short} saat ini" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prezzo attuale di {token_name_short}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の {token_name_short} 価格" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390452,11 +394998,53 @@ "value" : "Aktualna {token_name_short} cena" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preço atual de {token_name_short}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preț curent {token_name_short}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текущая цена {token_name_short}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuvarande {token_name_short}-pris" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel {token_name_short} fiyatı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточна ціна {token_name_short}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前 {token_name_short} 价格" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前 {token_name_short} 價格" + } } } }, @@ -390469,6 +395057,12 @@ "value" : "يتم إرسال الرسائل باستخدام {network_name}. تتكون الشبكة من عقد محفزة مع {token_name_long}، الذي يحافظ على {app_name} لامركزي وآمن. اعرف المزيد {icon}" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {network_name} üzərindən göndərilir. Şəbəkə, {app_name} tətbiqini mərkəzsiz və təhlükəsiz saxlamaq üçün {token_name_long} ilə təşviq olunan node-lardan ibarətdir. Ətraflı öyrən {icon}" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390481,24 +395075,60 @@ "value" : "Zprávy se odesílají pomocí {network_name}. Síť se skládá ze serverů, jejichž provozovatelé jsou odměňováni pomocí {token_name_long}, což zajišťuje decentralizaci a bezpečnost {app_name}. Další informace {icon}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten werden über das {network_name} gesendet. Das Netzwerk besteht aus Knoten, die mit {token_name_long} incentiviert werden, wodurch {app_name} dezentral und sicher bleibt. Mehr erfahren {icon}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Messages are sent using the {network_name}. The network is comprised of nodes incentivized with {token_name_long}, which keeps {app_name} decentralized and secure. Learn More {icon}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se envían utilizando {network_name}. La red está compuesta por nodos incentivados con {token_name_long}, lo que mantiene a {app_name} descentralizado y seguro. Más información {icon}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se envían utilizando {network_name}. La red está compuesta por nodos incentivados con {token_name_long}, lo que mantiene a {app_name} descentralizado y seguro. Más información {icon}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les messages sont envoyés en utilisant l'{network_name}. Le réseau est composé de noeuds stimulés par {token_name_long}, qui permet à {app_name} d'être décentralisée et sécurisée. En savoir plus {icon}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश {network_name} का उपयोग करके भेजे जाते हैं। यह नेटवर्क {token_name_long} से प्रेरित नोड्स से बना है, जो {app_name} को विकेंद्रीकृत और सुरक्षित बनाए रखता है। और जानें {icon}" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenetek küldése a(z) {network_name} hálózaton keresztül történik. A hálózat a(z) {token_name_long} tokennel ösztönzött csomópontokat tartalmaz, amelyek decentralizálttá és biztonságossá teszik a(z) {app_name} alkalmazást.Tudjon meg többet {icon}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi vengono inviati utilizzando la {network_name}. La rete è composta da nodi incentivati con {token_name_long}, che mantengono {app_name} decentralizzata e sicura. Scopri di più {icon}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージは {network_name} を使用して送信されます。このネットワークは {token_name_long} によってインセンティブを受けたノードで構成されており、{app_name} の分散性と安全性を保っています。詳しくはこちら {icon}" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390517,17 +395147,65 @@ "value" : "Wiadomości są wysyłane za pośrednictwem sieci {network_name}. Sieć składa się z węzłów, które są motywowane tokenami {token_name_long}, co zapewnia, że {app_name} pozostaje zdecentralizowana i bezpieczna. Dowiedz się więcej {icon}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens são enviadas utilizando a {network_name}. A rede é composta por nós incentivados com {token_name_long}, o que mantém o {app_name} descentralizado e seguro. Saiba mais {icon}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele sunt trimise folosind {network_name}. Rețeaua este alcătuită din noduri stimulate cu {token_name_long}, ceea ce menține {app_name} descentralizat și sigur. Informații suplimentare {icon}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщения отправляются через {network_name}. Сеть состоит из узлов, стимулируемых {token_name_long}, что обеспечивает децентрализацию и безопасность {app_name}. Узнать больше {icon}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden skickas via {network_name}. Nätverket består av noder som är incitamenterade med {token_name_long}, vilket håller {app_name} decentraliserad och säker. Läs mer {icon}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar, {network_name} kullanılarak gönderilir. Ağ, {token_name_long} ile teşvik edilen düğümlerden oluşur; bu da {app_name} uygulamasını merkeziyetsiz ve güvenli tutar. Daha Fazla Bilgi {icon}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повідомлення надсилаються за допомогою {network_name}. Мережа складається з вузлів, заохочуваних {token_name_long}, що забезпечує децентралізацію та безпеку {app_name}. Дізнатися більше {icon}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息通过 {network_name} 发送。该网络由通过 {token_name_long} 激励的节点组成,使 {app_name} 实现去中心化并保持安全。了解更多 {icon}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息是透過 {network_name} 傳送。該網路由透過 {token_name_long} 提供激勵的節點所組成,這確保了 {app_name} 的去中心化與安全性。了解更多 {icon}" + } } } }, "sessionNetworkLearnAboutStaking" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Staking barədə ətraflı öyrən" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390540,24 +395218,60 @@ "value" : "Přečtěte si o stakingu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr über Staking erfahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Learn About Staking" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aprender sobre staking" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aprender sobre staking" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En savoir plus sur Staking" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्टेकिंग के बारे में जानें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Ismerje meg a lekötést" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scopri lo staking" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステーキングについて学ぶ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390576,17 +395290,65 @@ "value" : "Dowiedz się więcej o stakingu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saiba mais sobre Staking" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Află mai multe despre staking" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подробней о стейкинге" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lär dig mer om staking" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Staking Hakkında Bilgi Edinin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дізнайтеся про стейкінг" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解 Staking" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解質押" + } } } }, "sessionNetworkMarketCap" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazar dəyəri" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390599,24 +395361,60 @@ "value" : "Tržní kapitalizace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marktkapitalisierung" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Market Cap" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalización de mercado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalización de mercado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Capitalisation du marché" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बाज़ार पूंजीकरण" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Piaci sapka" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalizzazione di mercato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時価総額" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390635,17 +395433,65 @@ "value" : "Kapitalizacja" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalização de Mercado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalizare de piață" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рыночная капитализация" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marknadsvärde" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piyasa Değeri" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ринкова капіталізація" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "市值" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "市值" + } } } }, "sessionNetworkNodesSecuring" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} node-ları mesajlarınızın təhlükəsizliyini təmin edir" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390658,24 +395504,60 @@ "value" : "{app_name} servery zabezpečující vaše zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-Knoten sichern deine Nachrichten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes securing your messages" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} protegiendo tus mensajes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} protegiendo tus mensajes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes sécurisant vos messages" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपके संदेशों की सुरक्षा कर रहे {app_name} नोड्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} csomópontok védik az Ön üzeneteit" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodi {app_name} che proteggono i tuoi messaggi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分のメッセージを保護する {app_name} ノード" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390694,17 +395576,65 @@ "value" : "{app_name} Węzły zabezpieczające twoje wiadomości" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nós do {app_name} a proteger as suas mensagens" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noduri {app_name} care securizează mesajele tale" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узлы {app_name} защищающие ваши сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-noder som skyddar dina meddelanden" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlarınızı güvenceye alan {app_name} düğümler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Вузли, що захищають ваші повідомлення" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "保护你的消息的 {app_name} 节点" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 節點保護著 您的訊息" + } } } }, "sessionNetworkNodesSwarm" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Swarm-ınızdakı {app_name} node-ları" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390717,24 +395647,60 @@ "value" : "{app_name} servery ve vašem swarmu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-Knoten in deinem Cluster" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes in your swarm" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} en tu swarm" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} en tu swarm" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes dans votre essaim" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपके स्वार्म में {app_name} नोड्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} csomópontok vannak a saját bolyban" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodi {app_name} nel tuo swarm" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分のスウォーム内の {app_name} ノード" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390753,17 +395719,65 @@ "value" : "{app_name} Węzły w twoim roju" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nós do {app_name} no seu enxame" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noduri {app_name} din roiul tău" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узлов {app_name} в вашем рою" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-noder i din svärm" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Swarmınızdaki {app_name} Nodelar" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Вузли у вашому рої" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Swarm 中的 {app_name} 节点" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您的群集 中的 {app_name} 節點" + } } } }, "sessionNetworkNotificationLive" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} artıq aktivdir! Ayarlarda yeni {network_name} bölməsini kəşf edin və {token_name_long} necə Session-u gücləndirdiyini öyrənin." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390776,6 +395790,12 @@ "value" : "{token_name_long} funguje! Prozkoumejte novou sekci {network_name} v Nastavení a zjistěte, jak {token_name_long} zabezpečuje funkčnost Session." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} ist live! Entdecke den neuen Bereich {network_name} in den Einstellungen und erfahre, wie {token_name_long} Session antreibt." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -390788,18 +395808,48 @@ "value" : "{token_name_long} estas viva! Esploru la novan sekcion {network_name} en Agordoj por lerni kiel {token_name_long} funkciigas Session." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡{token_name_long} está activo! Explora la nueva sección {network_name} en Configuración para saber cómo {token_name_long} impulsa Session." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡{token_name_long} está activo! Explora la nueva sección {network_name} en Configuración para saber cómo {token_name_long} impulsa Session." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le {token_name_long} est en ligne ! Explorez la nouvelle section {network_name} dans les paramètres pour apprendre comment le {token_name_long} alimente Session." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} अब लाइव है! यह जानने के लिए सेटिंग्स में नए {network_name} अनुभाग का अन्वेषण करें कि {token_name_long} कैसे Session को शक्ति देता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{token_name_long} aktív! Fedezze fel az új {network_name} menüpontot a beállításokban, hogy megtudja, hogyan működteti a(z) {token_name_long} a Sessiont." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} è attivo! Esplora la nuova sezione {network_name} in Impostazioni per scoprire come {token_name_long} alimenta Session." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} が始動しました!設定内の新しい {network_name} セクションを確認して、{token_name_long} がどのように Session を支えているかを学びましょう。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390818,17 +395868,65 @@ "value" : "{token_name_long} jest już dostępny! Zapoznaj się z nową sekcją {network_name} w Ustawieniach, aby dowiedzieć się, jak {token_name_long} zasila Session." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} está ao vivo! Explore a nova seção {network_name} em Definições para saber como {token_name_long} impulsiona o Session." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} este activ! Explorează noua secțiune {network_name} din Setări pentru a afla cum {token_name_long} alimentează Session." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} уже доступен! Изучите новый раздел {network_name} в настройках, чтобы узнать, как {token_name_long} обеспечивает работу Session." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} är live! Utforska det nya avsnittet {network_name} i Inställningar för att lära dig hur {token_name_long} driver Session." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} yayında! {token_name_long} tokeninin Session'a nasıl güç verdiğini öğrenmek için Ayarlar'daki yeni {network_name} bölümünü keşfedin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{token_name_long} запущено! Ознайомтеся з новим розділом {network_name} у Налаштуваннях, щоб дізнатися, як {token_name_long} забезпечує роботу Session." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} 已上线!在设置中探索新的 {network_name} 部分,了解 {token_name_long} 如何为 Session 提供支持。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} 現已上線!前往「設定」中的 {network_name} 區段了解 {token_name_long} 如何為 Session 提供支援。" + } } } }, "sessionNetworkSecuredBy" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şəbəkənin təhlükəsizliyini təmin edən" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390841,6 +395939,12 @@ "value" : "Síť zabezpečuje" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Netzwerk gesichert durch" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -390853,12 +395957,30 @@ "value" : "Reto sekurigita de" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red protegida por" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red protegida por" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réseau sécurisé par" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सुरक्षित नेटवर्क" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -390871,6 +395993,18 @@ "value" : "Jaringan aman oleh" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rete protetta da" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワークのセキュリティ提供元" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390889,17 +396023,65 @@ "value" : "Sieć zabezpieczona przez" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rede protegida por" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rețea securizată de" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сеть защищена" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nätverket skyddas av" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağın güvenliğini sağlayan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мережа захищена" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "网络由以下保障" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "網路保護方式" + } } } }, "sessionNetworkTokenDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şəbəkənin təhlükəsizliyini təmin etmək üçün {token_name_long} stake etdiyiniz zaman, {staking_reward_pool} fondundan {token_name_short} ilə mükafatlandırılırsınız." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390912,24 +396094,60 @@ "value" : "{token_name_long} staking znamená zvyšovat bezpečnost sítě a získávat odměny v podobě {token_name_short} ze {staking_reward_pool}." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn du {token_name_long} stakest, um das Netzwerk zu sichern, erhältst du Belohnungen in {token_name_short} aus dem {staking_reward_pool}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "When you stake {token_name_long} to secure the network, you earn rewards in {token_name_short} from the {staking_reward_pool}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando haces stake de {token_name_long} para proteger la red, ganas recompensas en {token_name_short} desde el fondo {staking_reward_pool}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando haces stake de {token_name_long} para proteger la red, ganas recompensas en {token_name_short} desde el fondo {staking_reward_pool}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque vous stakez du {token_name_long} pour sécuriser le réseau, vous gagnez des récompenses en {token_name_short} de l’ {staking_reward_pool}." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब आप नेटवर्क को सुरक्षित करने के लिए {token_name_long} स्टेक करते हैं, तो आपको {staking_reward_pool} से {token_name_short} में पुरस्कार मिलते हैं।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Amikor leköti a(z) {token_name_long} tokent, hogy biztosítsa a hálózatot, akkor {token_name_short} tokenben fog jutalmat kapni a(z) {staking_reward_pool} gyűjtőből." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando esegui lo staking di {token_name_long} per proteggere la rete, ricevi premi in {token_name_short} dal {staking_reward_pool}." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワークの安全性を確保するために {token_name_long} をステーキングすると、{staking_reward_pool} から {token_name_short} の報酬を獲得できます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390948,17 +396166,65 @@ "value" : "Stawiając {token_name_long} w celu zabezpieczenia sieci, otrzymujesz nagrody w {token_name_short} z puli {staking_reward_pool}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando deposita {token_name_long} para proteger a rede, ganha recompensas em {token_name_short} do {staking_reward_pool}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Când stake-uiți {token_name_long} pentru a securiza rețeaua, primiți recompense în {token_name_short} din {staking_reward_pool}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Когда вы стейкаете {token_name_long} для защиты сети, вы получаете вознаграждение в {token_name_short} из {staking_reward_pool}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "När du stakar {token_name_long} för att säkra nätverket, tjänar du belöningar i {token_name_short} från {staking_reward_pool}." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağı güvence altına almak için {token_name_long} stake ettiğinizde, {staking_reward_pool} havuzundan {token_name_short} cinsinden ödüller kazanırsınız." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли ви робите стейкінг {token_name_long}, щоб захистити мережу, ви заробляєте винагороди у {token_name_short} з {staking_reward_pool}." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "当你质押 {token_name_long} 以保护网络时,你将从 {staking_reward_pool} 中获得以 {token_name_short} 计的奖励。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "當您質押 {token_name_long} 以保護網路時,您會從 {staking_reward_pool} 獲得 {token_name_short} 獎勵。" + } } } }, "sessionNew" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : " Yeni" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390971,6 +396237,12 @@ "value" : " Nové" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : " Neu" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -390983,18 +396255,48 @@ "value" : " Nova" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuevo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuevo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : " Nouveau" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : " नया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : " Új" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuovo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " 新規" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391012,6 +396314,54 @@ "state" : "translated", "value" : " Nowy" } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : " Novo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nou" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : " Новая" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : " Ny" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : " Yeni" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : " Нове" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : " 全新" + } } } }, @@ -393916,6 +399266,12 @@ "value" : "Vælg billede til fællesskabet" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Anzeigebild festlegen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -393928,12 +399284,30 @@ "value" : "Agordi bildon de la komunumo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer imagen de perfil de la Comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer imagen de perfil de la Comunidad" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Définir la photo de la communauté" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community की डिस्प्ले तस्वीर सेट करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -393946,6 +399320,18 @@ "value" : "Atur Tampilan Gambar Komunitas" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imposta immagine della Community" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティのプロフィール写真を設定" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -393964,6 +399350,36 @@ "value" : "Ustaw zdjęcie profilowe grupy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definir imagem de exibição da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setează imaginea afișată de Comunitate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Установить картинку для сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Community-visningsbild" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Topluluk Görünen Resmini Ayarla" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -393975,6 +399391,12 @@ "state" : "translated", "value" : "设置社群头像" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定社群顯示圖片" + } } } }, @@ -395906,7 +401328,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databazanı açarkən bir problem baş verdi. Lütfən, tətbiqi yenidən başladıb bir daha sınayın." + "value" : "Veri bazasını açarkən bir problem baş verdi. Lütfən, tətbiqi yenidən başladıb bir daha sınayın." } }, "bal" : { @@ -396478,6 +401900,18 @@ "value" : "Ups! Wygląda na to, że nie masz konta {app_name}.

    Będziesz musiał stworzyć konto w aplikacji {app_name}, zanim będziesz mógł to udostępnić." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ops! Parece que ainda não tem uma conta {app_name}.

    Será necessário criar uma na aplicação {app_name} antes de poder partilhar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ups! Se pare că nu ai încă un cont {app_name}.

    Va trebui să creezi unul în aplicația {app_name} înainte de a putea partaja." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -396513,6 +401947,12 @@ "state" : "translated", "value" : "哎呀!您似乎还没有{app_name}帐户。

    您需要先在{app_name}应用中创建一个帐户,然后才能分享。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "哎呀!您似乎尚未擁有 {app_name} 帳號。

    您需要先在 {app_name} 應用程式中建立帳號才能進行分享。" + } } } }, @@ -398453,6 +403893,12 @@ "value" : "Egen note" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "»Notiz an mich« anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -398465,12 +403911,30 @@ "value" : "Montri noton al mi mem" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Personal" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Personal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la note pour soi-même" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अपने लिए नोट दिखाएं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -398483,6 +403947,18 @@ "value" : "Lihat Catatan Pribadi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra note personali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分用メモを表示" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -398501,11 +403977,53 @@ "value" : "Pokaż moje notatki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Pessoal" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează Notă personală" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать Заметки для Себя" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa Notera till mig själv" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kendime Notu Göster" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати нотатку для себе" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示备忘录" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示小筆記" + } } } }, @@ -398536,24 +404054,60 @@ "value" : "Er du sikker på, at du vil vise Egen note i din samtaleliste?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du Notiz an mich wirklich in deiner Unterhaltungsliste anzeigen?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to show Note to Self in your conversation list?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas mostrar Nota Personal en tu lista de conversaciones?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas mostrar Nota Personal en tu lista de conversaciones?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir afficher Note pour soi-même dans votre liste de conversations ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई अपनी वार्तालाप सूची में अपने लिए नोट दिखाना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan meg akarja jeleníteni a Jegyzet magamnak jegyzetet a beszélgetési listában?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler mostrare Note to Self nella tua lista di conversazioni?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分用メモを会話リストに表示しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -398572,11 +404126,64 @@ "value" : "Czy na pewno chcesz wyświetlać Moje notatki na liście konwersacji?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende mostrar a Nota Pessoal na sua lista de conversas?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să afișezi Notă personală în lista de conversații?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите отобразить Заметку для себя в списке бесед?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill visa Notera till mig själv i din konversationslista?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kendime Not'u sohbet listenizde göstermek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете показувати Нотатку для себе у вашому списку розмов?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要在对话列表中显示 Note to Self吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要在對話列表中顯示 小筆記 嗎?" + } + } + } + }, + "spellChecker" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spell Checker" + } } } }, @@ -399059,6 +404666,28 @@ } } }, + "strength" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Strength" + } + } + } + }, + "supportDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Having issues? Explore help articles or open a ticket with {app_name} Support." + } + } + } + }, "supportGoTo" : { "extractionState" : "manual", "localizations" : { @@ -400062,6 +405691,18 @@ "value" : "Frapetu por reprovi" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca para reintentar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca para reintentar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -400086,6 +405727,18 @@ "value" : "Ketuk untuk mencoba lagi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tocca per riprovare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップして再試行" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -400104,6 +405757,18 @@ "value" : "Dotknij aby ponowić" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque para tentar novamente" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apasă pentru a reîncerca" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -400116,6 +405781,12 @@ "value" : "Tryck för ett nytt försök" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekrar denemek için tıkla" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -400133,6 +405804,12 @@ "state" : "translated", "value" : "点击以重试" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輕觸以重試" + } } } }, @@ -401591,25 +407268,298 @@ } } }, + "themePreview" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Theme Preview" + } + } + } + }, "tooltipAccountIdVisible" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} üçün Hesab ID-si əvvəlki qarşılıqlı əlaqələrə əsasən görünür" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'identificador del compte de {name} és visible en funció de les teves interaccions anteriors" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID účtu {name} je viditelné
    v závislosti na vašich předchozích interakcích" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Account-ID von {name} ist basierend auf deinen vorherigen Interaktionen sichtbar" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "The Account ID of {name} is visible based on your previous interactions" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ID de cuenta de {name} es visible basándose en tus interacciones anteriores" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ID de cuenta de {name} es visible basándose en tus interacciones anteriores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "L’ID de compte de {name} est visible en fonction de vos interactions précédentes" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} का Account ID आपकी पिछली इंटरैक्शन के आधार पर दृश्यमान है" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'Account ID di {name} è visibile in base alle interazioni precedenti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} のAccount IDは、以前のやり取りに基づき表示されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De Account ID van {name} is zichtbaar op basis van je eerdere interacties" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identyfikator konta {name} jest widoczny na podstawie wcześniejszych interakcji" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ID da Conta de {name} está visível com base nas suas interações anteriores" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID-ul contului {name} este vizibil în baza interacțiunilor anterioare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID аккаунта {name} виден
    на основе ваших предыдущих взаимодействий" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name}:s Account ID är synligt baserat på dina tidigare interaktioner" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} adlı kişinin Hesap Kimliği, önceki etkileşimlerinize dayanarak görünür durumdadır" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account ID {name} видимий через вашу попередню взаємодію" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "基于您与 {name} 之前的互动,其 Account ID 对您可见" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} 的 Account ID 因先前的互動而可見" + } } } }, "tooltipBlindedIdCommunities" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kor ID-lər, spamı azaltmaq və məxfiliyi artırmaq üçün icmalarda istifadə olunur" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Els ID cecs s'utilitzen a les comunitats per reduir el correu brossa i augmentar la privadesa" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskovaná ID jsou používána v komunitách
    ke snížení spamu a zvýšení soukromí" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschleierte IDs werden in Communities verwendet, um Spam zu reduzieren und die Privatsphäre zu erhöhen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Blinded IDs are used in communities to reduce spam and increase privacy" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ID cegados se utilizan en las comunidades para reducir el spam y aumentar la privacidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ID cegados se utilizan en las comunidades para reducir el spam y aumentar la privacidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les ID aveuglés sont utilisés dans les communautés pour réduire le spam et renforcer la confidentialité" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ब्लाइंडेड ID समुदायों में स्पैम को कम करने और गोपनीयता बढ़ाने के लिए उपयोग की जाती हैं" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gli ID offuscati vengono utilizzati nelle Community per ridurre lo spam e aumentare la privacy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラインドIDは、スパムを減らしプライバシーを高めるためにCommunityで利用されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geblindeerde ID's worden in Community's gebruikt om spam te verminderen en de privacy te vergroten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zanonimizowane identyfikatory są używane w społecznościach w celu ograniczenia spamu i zwiększenia prywatności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "IDs Ocultos são usados em Comunidades para reduzir spam e aumentar a privacidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID-urile cenzurate sunt utilizate în comunități pentru a reduce mesajele spam și a crește confidențialitatea" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрытые ID используются в сообществах
    для уменьшения спама и повышения конфиденциальности" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskerade ID används i Communitys för att minska spam och öka sekretessen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Körleştirilmiş Kimlikler, istenmeyen mesajları (spam) azaltmak ve gizliliği artırmak için topluluklarda kullanılır" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знеособлені ID використовуються у спільнотах задля зменшення кількості небажаних повідомлень та підвищення приватності" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "盲化 ID 在社区中用于减少垃圾信息并提高隐私性" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Community 中使用隱藏 ID 可減少垃圾訊息並提升隱私" + } + } + } + }, + "translate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Translate" + } + } + } + }, + "tray" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tray" + } } } }, @@ -403047,6 +408997,12 @@ "unavailable" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əlçatmazdır" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -403059,6 +409015,12 @@ "value" : "Nedostupné" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht verfügbar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -403071,12 +409033,30 @@ "value" : "Ne disponebla" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No disponible" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No disponible" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Indisponible" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उपलब्ध नहीं है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -403089,6 +409069,18 @@ "value" : "Tidak tersedia" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non disponibile" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用不可" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -403107,11 +409099,53 @@ "value" : "Niedostępny" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indisponível" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indisponibil" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Недоступно" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otillgänglig" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mevcut Değil" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Недоступно" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "不可用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法使用" + } } } }, @@ -404558,9 +410592,467 @@ } } }, + "updateCommunityInformation" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma məlumatlarını güncəllə" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualitzar la informació de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravit informace o komunitě" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Informationen aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Community Information" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar la información de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar la información de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour les informations de la communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सामुदायिक जानकारी अपडेट करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna le informazioni della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ情報を更新" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-informatie bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj informacje o społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar informações da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează informațiile comunității" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera communityinformation" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新社群信息" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新社群資訊" + } + } + } + }, + "updateCommunityInformationDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma adı və açıqlaması, bütün icma üzvlərinə görünür" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nom i la descripció de la comunitat són visibles per a tots els membres de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Název a popis komunity jsou viditelné pro všechny členy komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Name und -Beschreibung sind für alle Mitglieder der Community sichtbar." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community name and description are visible to all community members" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción de la comunidad son visibles para todos los miembros de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción de la comunidad son visibles para todos los miembros de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le nom et la description de la communauté sont visibles par tous les membres" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सामुदायिक नाम और विवरण सभी सामुदायिक सदस्यों को दिखाई देता है" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il nome e la descrizione della Comunità sono visibili a tutti i membri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名と説明はすべてのメンバーに表示されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Communitynaam en beschrijving zijn zichtbaar voor alle communityleden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nazwa i opis społeczności są widoczne dla wszystkich jej członków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O nome e a descrição da Comunidade são visíveis para todos os membros da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și descrierea comunității sunt vizibile pentru toți membrii comunității" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Communitynamn och beskrivning är synliga för alla communitymedlemmar" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "社群名称与描述对所有社群成员可见" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "社群名稱和描述對所有社群成員可見" + } + } + } + }, + "updateCommunityInformationEnterShorterDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, daha qısa icma açıqlaması daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu una descripció de la comunitat més curta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte prosím kratší popis komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib eine kürzere Community-Beschreibung ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a shorter community description" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce una descripción más corta de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce una descripción más corta de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer une description de la communauté plus courte" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा सामुदायिक विवरण दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione della Comunità più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "より短いコミュニティの説明を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een kortere communitybeschrijving in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź krótszy opis społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira uma descrição mais curta da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci o descriere a comunității mai scurtă" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange en kortare communitybeskrivning" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入更简短的社群描述" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入較短的社群描述" + } + } + } + }, + "updateCommunityInformationEnterShorterName" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, daha qısa icma adı daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat més curt" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte prosím kratší název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib einen kürzeren Community-Namen ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a shorter community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad más corto" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad más corto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer un nom de communauté plus court" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "より短いコミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een kortere communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź krótszą nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira um nome de Comunidade mais curto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci un nume al comunității mai scurt" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange ett kortare communitynamn" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入更简短的社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入較短的社群名稱" + } + } + } + }, "updated" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son güncəlləmə: {relative_time} əvvəl" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -404573,6 +411065,12 @@ "value" : "Naposledy aktualizováno před {relative_time}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zuletzt aktualisiert vor {relative_time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -404585,18 +411083,48 @@ "value" : "Laste ĝisdatigita antaŭ {relative_time}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última actualización hace {relative_time}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última actualización hace {relative_time}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dernière mise à jour il y a {relative_time}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अंतिम अद्यतन {relative_time} पहले" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Utoljára frissítve {relative_time}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ultimo aggiornamento {relative_time} fa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最終更新: {relative_time} 前" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -404615,11 +411143,53 @@ "value" : "Ostatnia aktualizacja {relative_time} temu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última atualização há {relative_time}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ultima actualizare acum {relative_time}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Последнее обновление {relative_time}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senast uppdaterad för {relative_time} sedan" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En son {relative_time} önce güncellendi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Останнє оновлення {relative_time} тому" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近更新于 {relative_time} 前" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次更新於 {relative_time} 前" + } } } }, @@ -406572,6 +413142,12 @@ "value" : "Opdatér gruppeoplysninger" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppeninformationen aktualisieren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -406584,12 +413160,30 @@ "value" : "Ĝisdatigi informon de la grupo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información del grupo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información del grupo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour les informations du groupe" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह जानकारी अपडेट करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -406602,6 +413196,18 @@ "value" : "Perbaharui Informasi Grup" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna informazioni del gruppo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループ情報を更新" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -406620,6 +413226,36 @@ "value" : "Aktualizuj informacje o grupie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar informações do grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează informațiile grupului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить информацию о группе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera gruppinformation" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup bilgisini güncelle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -406631,6 +413267,12 @@ "state" : "translated", "value" : "更新群组信息" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新群組資訊" + } } } }, @@ -406661,6 +413303,12 @@ "value" : "Gruppenavn og beskrivelse er synlig for alle gruppemedlemmer." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppenname und Beschreibung sind für alle Gruppenmitglieder sichtbar." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -406673,12 +413321,30 @@ "value" : "Grupa nomo kaj priskribo estas videbla al ĉiuj membroj de la grupo." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción del grupo son visibles para todos los miembros del grupo." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción del grupo son visibles para todos los miembros del grupo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le nom du groupe et la description sont visibles par tous les membres du groupe." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह का नाम और विवरण सभी समूह सदस्यों को दिखाई देता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -406691,6 +413357,18 @@ "value" : "Nama grup dan deskripsi dapat dilihat oleh semua anggota grup." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il nome e la descrizione del gruppo sono visibili a tutti i membri del gruppo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループ名と説明はすべてのグループメンバーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -406709,6 +413387,36 @@ "value" : "Nazwa i opis grupy są widoczne dla wszystkich jej członków." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O nome e a descrição do grupo estão visíveis para todos os membros do grupo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și descrierea grupului sunt vizibile pentru toți membrii grupului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название и описание группы видны всем участникам группы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppnamn och beskrivning är synliga för alla gruppmedlemmar." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup adı ve açıklaması tüm grup üyeleri tarafından görülebilir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -406720,6 +413428,12 @@ "state" : "translated", "value" : "群组名称与描述对所有群组成员可见。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組名稱與描述對所有群組成員可見。" + } } } }, @@ -406750,6 +413464,12 @@ "value" : "Indtast venligst en kortere gruppebeskrivelse" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib eine kürzere Gruppenbeschreibung ein" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -406762,12 +413482,30 @@ "value" : "Bonvolu enigi pli mallongan priskribon de la grupo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, ingresa una descripción del grupo más corta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, ingresa una descripción del grupo más corta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Veuillez entrer une description de groupe plus courte" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा समूह विवरण दर्ज करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -406780,6 +413518,18 @@ "value" : "Masukkan nama grup yang lebih pendek" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione del gruppo più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループの説明をもっと短く入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -406798,6 +413548,36 @@ "value" : "Wprowadź krótszy opis grupy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduza uma descrição mais curta do grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci o descriere mai scurtă a grupului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите более короткое описание группы" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange en kortare gruppbeskrivning" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen daha kısa bir grup açıklaması girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -406815,6 +413595,12 @@ "state" : "translated", "value" : "请输入更简短的群组描述" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入一個較短的群組描述" + } } } }, @@ -407378,12 +414164,24 @@ "value" : "{app_name} का एक नया संस्करण ({version}) उपलब्ध है।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elérhető a(z) {app_name} új, {version} verziója." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi terbaru ({version}) dari {app_name} telah tersedia." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È disponibile una nuova versione ({version}) di {app_name}." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -407420,6 +414218,12 @@ "value" : "Dostępna jest nowa wersja ({version}) aplikacji {app_name}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uma nova versão ({version}) de {app_name} está disponível." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -407461,6 +414265,34 @@ "state" : "translated", "value" : "{app_name}有新版本({version})可用。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 有新版本({version})可用。" + } + } + } + }, + "updateProfileInformation" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Profile Information" + } + } + } + }, + "updateProfileInformationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your display name and display picture are visible in all conversations." + } } } }, @@ -407943,6 +414775,17 @@ } } }, + "updates" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updates" + } + } + } + }, "updateSession" : { "extractionState" : "manual", "localizations" : { @@ -408907,9 +415750,26 @@ } } }, + "updating" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updating..." + } + } + } + }, "upgradeTo" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəlt" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -408922,23 +415782,119 @@ "value" : "Navýšit na" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgrade auf" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Upgrade to" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar a" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar a" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à niveau à" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अपग्रेड करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Frissítés erre:" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passa a" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップグレード先:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgraden naar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uaktualnij do" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar para" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează la" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить до" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppgradera till" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükselt" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підвищити до" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "升级到" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "升級為" + } } } }, @@ -413280,6 +420236,12 @@ "value" : "Vis mindre" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weniger anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413292,18 +420254,54 @@ "value" : "Vidi malpli" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir Moins" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कम देखें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kevesebb" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan Ringkas" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra meno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示を減らす" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -413322,6 +420320,36 @@ "value" : "Zobacz mniej" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează mai puțin" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посмотреть меньше" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa färre" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha az görüntüle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -413333,6 +420361,12 @@ "state" : "translated", "value" : "查看更少" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示較少" + } } } }, @@ -413363,6 +420397,12 @@ "value" : "Vis mere" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413375,18 +420415,54 @@ "value" : "Vidi pli" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver más" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver más" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir Plus" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "और देखें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Több" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Selengkapnya" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra di più" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もっと見る" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -413405,6 +420481,36 @@ "value" : "Zobacz więcej" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează mai mult" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посмотреть больше" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devamını Görüntüle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -413416,6 +420522,12 @@ "state" : "translated", "value" : "查看更多" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示更多" + } } } }, @@ -416292,6 +423404,39 @@ } } } + }, + "yourRecoveryPassword" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Recovery Password" + } + } + } + }, + "zoomFactor" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoom Factor" + } + } + } + }, + "zoomFactorDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adjust the size of text and visual elements." + } + } + } } }, "version" : "1.0" From affb3a9c3e6fc814322c548cf59035787b4963db Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 11 Aug 2025 15:43:57 +1000 Subject: [PATCH 041/244] Fixed an issue where attachments would be incorrectly scaled down --- .../Message Cells/VisibleMessageCell.swift | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 58dbf649b5..21a9517955 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -1175,42 +1175,39 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { guard let firstAttachment: Attachment = mediaAttachments.first, - var width: CGFloat = firstAttachment.width.map({ CGFloat($0) }), - var height: CGFloat = firstAttachment.height.map({ CGFloat($0) }), + let originalWidth: CGFloat = firstAttachment.width.map({ CGFloat($0) }), + let originalHeight: CGFloat = firstAttachment.height.map({ CGFloat($0) }), mediaAttachments.count == 1, - width > 0, - height > 0 + originalWidth > 0, + originalHeight > 0 else { return defaultSize } // Honor the content aspect ratio for single media - let size: CGSize = CGSize(width: width, height: height) - var aspectRatio = (size.width / size.height) + let originalSize: CGSize = CGSize(width: originalWidth, height: originalHeight) + var aspectRatio = (originalSize.width / originalSize.height) + // Clamp the aspect ratio so that very thin/wide content still looks alright let minAspectRatio: CGFloat = 0.35 let maxAspectRatio = 1 / minAspectRatio - let maxSize = CGSize(width: maxMessageWidth, height: maxMessageWidth) aspectRatio = aspectRatio.clamp(minAspectRatio, maxAspectRatio) + // Constraint the image + let constraintWidth = min(maxMessageWidth, originalSize.width) + let constraintHeight = min(maxMessageWidth, originalSize.height) + + var finalWidth: CGFloat + var finalHeight: CGFloat + if aspectRatio > 1 { - width = maxSize.width - height = width / aspectRatio + finalWidth = constraintWidth + finalHeight = finalWidth / aspectRatio } else { - height = maxSize.height - width = height * aspectRatio - } - - // Don't blow up small images unnecessarily - let minSize: CGFloat = 150 - let shortSourceDimension = min(size.width, size.height) - let shortDestinationDimension = min(width, height) - - if shortDestinationDimension > minSize && shortDestinationDimension > shortSourceDimension { - let factor = minSize / shortDestinationDimension - width *= factor; height *= factor + finalHeight = constraintHeight + finalWidth = finalHeight * aspectRatio } - return CGSize(width: width, height: height) + return CGSize(width: finalWidth, height: finalHeight) } static func getMaxWidth(for cellViewModel: MessageViewModel, includingOppositeGutter: Bool = true) -> CGFloat { From cc1a0535d51575fb504480226fd8ac9072d67419 Mon Sep 17 00:00:00 2001 From: Teamified Date: Mon, 11 Aug 2025 14:00:38 +0800 Subject: [PATCH 042/244] Fix incorrect duration for uploading voice message --- .../Content Views/VoiceMessageView.swift | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift b/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift index 71bfeb1790..d2b65b96d6 100644 --- a/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift @@ -168,7 +168,6 @@ public final class VoiceMessageView: UIView { } // MARK: - Updating - public func update( with attachment: Attachment, isPlaying: Bool, @@ -176,42 +175,49 @@ public final class VoiceMessageView: UIView { playbackRate: Double, oldPlaybackRate: Double ) { - switch attachment.state { - case .downloaded, .uploaded: - loader.isHidden = true - loader.stopAnimating() - - toggleImageView.image = (isPlaying ? UIImage(named: "Pause") : UIImage(named: "Play"))? - .withRenderingMode(.alwaysTemplate) - countdownLabel.text = max(0, (floor(attachment.duration.defaulting(to: 0) - progress))) - .formatted(format: .hoursMinutesSeconds) - - guard let duration: TimeInterval = attachment.duration, duration > 0, progress > 0 else { - return progressViewRightConstraint.constant = -VoiceMessageView.width - } - - let fraction: Double = (progress / duration) - progressViewRightConstraint.constant = -(VoiceMessageView.width * (1 - fraction)) - - // If the playback rate changed then show the 'speedUpLabel' briefly - guard playbackRate > oldPlaybackRate else { return } - - UIView.animate(withDuration: 0.25) { [weak self] in - self?.countdownLabel.alpha = 0 - self?.speedUpLabel.alpha = 1 - } - - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1250)) { - UIView.animate(withDuration: 0.25) { [weak self] in - self?.countdownLabel.alpha = 1 - self?.speedUpLabel.alpha = 0 - } - } - - default: - if !loader.isAnimating { - loader.startAnimating() - } + + // Updates countdown label to attachments duration + // Should be set regardless of attachment state + let remainingTime = max(0, floor(attachment.duration.defaulting(to: 0) - progress)) + + countdownLabel.text = remainingTime.formatted(format: .hoursMinutesSeconds) + + guard attachment.state == .downloaded || attachment.state == .uploaded else { + if !loader.isAnimating { + loader.startAnimating() + } + return + } + + loader.isHidden = true + loader.stopAnimating() + + toggleImageView.image = (isPlaying ? UIImage(named: "Pause") : UIImage(named: "Play"))? + .withRenderingMode(.alwaysTemplate) + + guard + let duration: TimeInterval = attachment.duration, + duration > 0, progress > 0 + else { + return progressViewRightConstraint.constant = -VoiceMessageView.width + } + + let fraction: Double = (progress / duration) + progressViewRightConstraint.constant = -(VoiceMessageView.width * (1 - fraction)) + + // If the playback rate changed then show the 'speedUpLabel' briefly + guard playbackRate > oldPlaybackRate else { return } + + UIView.animate(withDuration: 0.25) { [weak self] in + self?.countdownLabel.alpha = 0 + self?.speedUpLabel.alpha = 1 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1250)) { + UIView.animate(withDuration: 0.25) { [weak self] in + self?.countdownLabel.alpha = 1 + self?.speedUpLabel.alpha = 0 + } } } } From e4d4a26d68e1710f87f5182b5ba1d4a64ab05958 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 11 Aug 2025 16:53:11 +1000 Subject: [PATCH 043/244] renaming --- .../Components/ProfilePictureView.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index 370da7d6ac..a6f52293a7 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -167,7 +167,7 @@ public final class ProfilePictureView: UIView { case additional } - public var shouldAnimateForCurrentUserProUpgrade: CurrentUserProfileImage = .none + public var currentUserProfileImage: CurrentUserProfileImage = .none // MARK: - Constraints @@ -423,9 +423,9 @@ public final class ProfilePictureView: UIView { .sink( receiveValue: { [weak self] isPro in if isPro { - self?.startAnimatingIfNeeded() + self?.startAnimatingForCurrentUserIfNeeded() } else { - self?.stopAnimatingIfNeeded() + self?.stopAnimatingForCurrentUserIfNeeded() } } @@ -556,7 +556,7 @@ public final class ProfilePictureView: UIView { } if case .currentUser(_) = info.animationBehaviour { - self.shouldAnimateForCurrentUserProUpgrade = .main + self.currentUserProfileImage = .main } imageView.themeTintColor = info.themeTintColor @@ -610,7 +610,7 @@ public final class ProfilePictureView: UIView { } if case .currentUser(_) = additionalInfo.animationBehaviour { - self.shouldAnimateForCurrentUserProUpgrade = .additional + self.currentUserProfileImage = .additional } additionalImageView.themeTintColor = additionalInfo.themeTintColor @@ -648,8 +648,8 @@ public final class ProfilePictureView: UIView { additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2) } - public func startAnimatingIfNeeded() { - switch shouldAnimateForCurrentUserProUpgrade { + public func startAnimatingForCurrentUserIfNeeded() { + switch currentUserProfileImage { case .none: break case .main: @@ -659,8 +659,8 @@ public final class ProfilePictureView: UIView { } } - public func stopAnimatingIfNeeded() { - switch shouldAnimateForCurrentUserProUpgrade { + public func stopAnimatingForCurrentUserIfNeeded() { + switch currentUserProfileImage { case .none: break case .main: From 92a4b5ab49550be6f7bccccb7a52b5182f7a8de4 Mon Sep 17 00:00:00 2001 From: Teamified Date: Mon, 11 Aug 2025 15:12:00 +0800 Subject: [PATCH 044/244] Added accessibility identifier for app icons --- Session/Settings/AppIconViewModel.swift | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 9e02f0612d..2afc780138 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -24,6 +24,26 @@ enum AppIcon: String, CaseIterable { /// additional copies in order to render in the UI var previewImageName: String { "\(rawValue)-Preview" } + // stringlint:ignore_contents + var accessibilityLabel: String { + switch self { + case .session: + "Session option" + case .weather: + "Weather option" + case .stocks: + "Stocks option" + case .news: + "News option" + case .notes: + "Notes option" + case .meetings: + "Meetings option" + case .calculator: + "Calculator option" + } + } + // stringlint:ignore_contents init(name: String?) { switch name { @@ -137,6 +157,7 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl case .none: self?.updateAppIcon(.weather) } } + ) ] ), @@ -150,6 +171,10 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl selectedIcon: AppIcon(name: current), onChange: { icon in self?.updateAppIcon(icon) } ) + ), + accessibility: Accessibility( + identifier: AppIcon(name: current).accessibilityLabel, + label: AppIcon(name: current).accessibilityLabel ) ) ] From 1221a155aae852043a0c6a2ea3808ecbd48ccedc Mon Sep 17 00:00:00 2001 From: Teamified Date: Mon, 11 Aug 2025 12:09:47 +0800 Subject: [PATCH 045/244] Fix translated hide text button overflow --- Session/Settings/RecoveryPasswordScreen.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Session/Settings/RecoveryPasswordScreen.swift b/Session/Settings/RecoveryPasswordScreen.swift index 603878d164..1cc49fd8a9 100644 --- a/Session/Settings/RecoveryPasswordScreen.swift +++ b/Session/Settings/RecoveryPasswordScreen.swift @@ -210,6 +210,8 @@ struct RecoveryPasswordScreen: View { alignment: .leading ) + Spacer() + Button { hideRecoveryPassword() } label: { @@ -218,9 +220,11 @@ struct RecoveryPasswordScreen: View { .font(.system(size: Values.verySmallFontSize)) .foregroundColor(themeColor: .danger) .frame( - width: 55, height: Values.mediumSmallButtonHeight ) + .frame(minWidth: 50) + .padding(.horizontal, 7) + .padding(.vertical, 5) .overlay( Capsule() .stroke(themeColor: .danger) From 69cc2e4ee104c1ee95be722c792d00106948d879 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 09:17:58 +1000 Subject: [PATCH 046/244] Stopped some message types making conversations become visible --- .../Sending & Receiving/MessageReceiver.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 059032f9c6..912f308871 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -400,9 +400,9 @@ public enum MessageReceiver { // visible (the only other spot this flag gets set is when sending messages) let shouldBecomeVisible: Bool = { switch message { - case is ReadReceipt: return true - case is TypingIndicator: return true - case is UnsendRequest: return true + case is ReadReceipt: return false + case is TypingIndicator: return false + case is UnsendRequest: return false case is CallMessage: return (threadId != dependencies[cache: .general].sessionId.hexString) /// These are sent to the one-to-one conversation so they shouldn't make that visible From 559563c5ed34d3111e869b8314b85a03ac92fb0e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 10:45:41 +1000 Subject: [PATCH 047/244] Fixed an issue where the unread count could incorrectly get reset --- .../NotificationServiceExtension.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 70d2273560..25e2b81fce 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -62,6 +62,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension do { let mainAppUnreadCount: Int = try performSetup(notificationInfo) + notificationInfo = notificationInfo.with(mainAppUnreadCount: mainAppUnreadCount) notificationInfo = try extractNotificationInfo(notificationInfo, mainAppUnreadCount) try setupGroupIfNeeded(notificationInfo) @@ -1287,7 +1288,8 @@ private extension NotificationServiceExtension { requestId: String? = nil, content: UNMutableNotificationContent? = nil, contentHandler: ((UNNotificationContent) -> Void)? = nil, - metadata: PushNotificationAPI.NotificationMetadata? = nil + metadata: PushNotificationAPI.NotificationMetadata? = nil, + mainAppUnreadCount: Int? = nil ) -> NotificationInfo { return NotificationInfo( content: (content ?? self.content), @@ -1295,7 +1297,7 @@ private extension NotificationServiceExtension { contentHandler: (contentHandler ?? self.contentHandler), metadata: (metadata ?? self.metadata), data: data, - mainAppUnreadCount: mainAppUnreadCount + mainAppUnreadCount: (mainAppUnreadCount ?? self.mainAppUnreadCount) ) } } From c7083e27aa97fb7a3f4652db3bc560e474b5eebf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 10:49:09 +1000 Subject: [PATCH 048/244] Stopped passing `mainAppUnreadCount` to `extractNotificationInfo` --- .../NotificationServiceExtension.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 25e2b81fce..c705e91dcd 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -63,7 +63,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension do { let mainAppUnreadCount: Int = try performSetup(notificationInfo) notificationInfo = notificationInfo.with(mainAppUnreadCount: mainAppUnreadCount) - notificationInfo = try extractNotificationInfo(notificationInfo, mainAppUnreadCount) + notificationInfo = try extractNotificationInfo(notificationInfo) try setupGroupIfNeeded(notificationInfo) processedNotification = try processNotification(notificationInfo) @@ -152,7 +152,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: - Notification Handling - private func extractNotificationInfo(_ info: NotificationInfo, _ mainAppUnreadCount: Int) throws -> NotificationInfo { + private func extractNotificationInfo(_ info: NotificationInfo) throws -> NotificationInfo { let (maybeData, metadata, result) = PushNotificationAPI.processNotification( notificationContent: info.content, using: dependencies @@ -170,7 +170,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension contentHandler: info.contentHandler, metadata: metadata, data: data, - mainAppUnreadCount: mainAppUnreadCount + mainAppUnreadCount: info.mainAppUnreadCount ) default: throw NotificationError.processingError(result, metadata) From 47d6a6c8339c408ebbcd0ef9a5dd4c032ec4033f Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 08:51:01 +0800 Subject: [PATCH 049/244] Removed vertical padding Used `Values.smallSpacing` for the horizontal padding --- Session/Settings/RecoveryPasswordScreen.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Session/Settings/RecoveryPasswordScreen.swift b/Session/Settings/RecoveryPasswordScreen.swift index 1cc49fd8a9..9afd6dcd93 100644 --- a/Session/Settings/RecoveryPasswordScreen.swift +++ b/Session/Settings/RecoveryPasswordScreen.swift @@ -222,9 +222,11 @@ struct RecoveryPasswordScreen: View { .frame( height: Values.mediumSmallButtonHeight ) - .frame(minWidth: 50) - .padding(.horizontal, 7) - .padding(.vertical, 5) + .frame( + minWidth: Values.alertButtonHeight, + alignment: .center + ) + .padding(.horizontal, Values.smallSpacing) .overlay( Capsule() .stroke(themeColor: .danger) From ebfea40b7c6153366651f9a12b8494f2aa1a6f62 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 09:14:10 +0800 Subject: [PATCH 050/244] Removed accessibility label usage Remove unnecessary new line --- Session/Settings/AppIconViewModel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 2afc780138..704e945d9c 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -157,7 +157,6 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl case .none: self?.updateAppIcon(.weather) } } - ) ] ), @@ -173,8 +172,7 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl ) ), accessibility: Accessibility( - identifier: AppIcon(name: current).accessibilityLabel, - label: AppIcon(name: current).accessibilityLabel + identifier: AppIcon(name: current).accessibilityLabel ) ) ] From 47db7767b8e647263177c049f489a3fcbf377b20 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 11:35:06 +1000 Subject: [PATCH 051/244] Fixed an issue where notification settings wren't being respected for PNs --- .../NotificationServiceExtension.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 70d2273560..25409728c6 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -108,9 +108,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) SNMessagingKit.configure(using: dependencies) - /// The `NotificationServiceExtension` needs custom behaviours for it's notification presenter so set it up here - dependencies.set(singleton: .notificationsManager, to: NSENotificationPresenter(using: dependencies)) - /// Cache the users secret key dependencies.mutate(cache: .general) { $0.setSecretKey(ed25519SecretKey: userMetadata.ed25519SecretKey) @@ -128,6 +125,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) dependencies.set(cache: .libSession, to: cache) + /// The `NotificationServiceExtension` needs custom behaviours for it's notification presenter so set it up here + /// + /// **Note:** This **MUST** happen after we have loaded the `libSession` cache as the notification settings are + /// stored in there + dependencies.set(singleton: .notificationsManager, to: NSENotificationPresenter(using: dependencies)) + return userMetadata.unreadCount } From ce7803353d7880f3d000f20335f10549bfb73148 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 09:53:11 +0800 Subject: [PATCH 052/244] Fix reply does not cancel search state on conversation --- Session/Conversations/ConversationVC+Interaction.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 62baf8fb45..08139c0db7 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2211,6 +2211,10 @@ extension ConversationVC: model: quoteDraft, isOutgoing: (cellViewModel.variant == .standardOutgoing) ) + + // Dismiss search textfield + if isShowingSearchUI { hideSearchUI() } + _ = snInputView.becomeFirstResponder() completion?() } From 8dc7be1c2bdd94d8916c0c6a4ac83132cf3f3a9c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 12:35:18 +1000 Subject: [PATCH 053/244] Fixed a few bugs found when testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a mechanism to wait for a dependency to be set • Added a warning log when a dependency gets set to a no-op instance • Fixed an issue where the 'NotificationsManager' delegate wouldn't be set on initial launch • Fixed an issue where we wouldn't check the local network permission on launch because the libSession cache wouldn't have been initialised yet --- Session.xcodeproj/project.pbxproj | 4 ++ Session/Meta/AppDelegate.swift | 24 +++++--- .../Notifications/NotificationPresenter.swift | 6 ++ .../Calls/NoopSessionCallManager.swift | 3 +- .../LibSession+SessionMessagingKit.swift | 2 +- .../NotificationsManagerType.swift | 2 +- .../LibSession/LibSession+Networking.swift | 2 +- .../Dependency Injection/Dependencies.swift | 59 +++++++++++++++++-- .../Dependency Injection/NoopDependency.swift | 5 ++ SessionUtilitiesKit/General/AppContext.swift | 2 +- 10 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 SessionUtilitiesKit/Dependency Injection/NoopDependency.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5ed0e0cdfc..93a9aabe6b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -659,6 +659,7 @@ FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3937072E4AD3F800571F17 /* NoopDependency.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; @@ -1983,6 +1984,7 @@ FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD3937072E4AD3F800571F17 /* NoopDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopDependency.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -5012,6 +5014,7 @@ FDF01FAE2A9ED0C800CAF969 /* Dependency Injection */ = { isa = PBXGroup; children = ( + FD3937072E4AD3F800571F17 /* NoopDependency.swift */, FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */, FDC6D75F2862B3F600B04575 /* Dependencies.swift */, FD2272ED2C3521D6004D8A6C /* FeatureConfig.swift */, @@ -6405,6 +6408,7 @@ FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, + FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */, FD74434A2D07CA9F00862443 /* Codable+Utilities.swift in Sources */, FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */, FD74434B2D07CA9F00862443 /* CGFloat+Utilities.swift in Sources */, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 46cafadd53..111b1fb225 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -75,8 +75,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) Log.info(.cat, "Setting up environment.") - /// Create a proper `NotificationPresenter` and `SessionCallManager` for the main app (defaults to no-op versions) - dependencies.set(singleton: .notificationsManager, to: NotificationPresenter(using: dependencies)) + /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies)) // Setup LibSession @@ -168,10 +167,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD mainWindow.rootViewController = self.loadingViewController mainWindow.makeKeyAndVisible() - // This must happen in appDidFinishLaunching or earlier to ensure we don't - // miss notifications. - // Setting the delegate also seems to prevent us from getting the legacy notification - // notification callbacks upon launch e.g. 'didReceiveLocalNotification' + /// Create a proper `NotificationPresenter` for the main app (defaults to a no-op version) + /// + /// **Note:** This must happen in `appDidFinishLaunching` to ensure we don't miss notifications. Setting the delegate + /// also seems to prevent us from getting the legacy notification notification callbacks upon launch e.g. `didReceiveLocalNotification` + dependencies.set(singleton: .notificationsManager, to: NotificationPresenter(using: dependencies)) dependencies[singleton: .notificationsManager].setDelegate(self) NotificationCenter.default.addObserver( @@ -307,11 +307,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // On every activation, clear old temp directories. + /// On every activation, clear old temp directories. dependencies[singleton: .fileManager].clearOldTemporaryDirectories() - if dependencies.mutate(cache: .libSession, { $0.get(.areCallsEnabled) }) && dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] { - Permissions.checkLocalNetworkPermission(using: dependencies) + /// It's likely that on a fresh launch that the `libSession` cache won't have been initialised by this point, so detatch a task to + /// wait for it before checking the local network permission + Task.detached { [dependencies] in + try? await dependencies.waitUntilInitialised(cache: .libSession) + + if dependencies.mutate(cache: .libSession, { $0.get(.areCallsEnabled) }) && dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] { + Permissions.checkLocalNetworkPermission(using: dependencies) + } } } diff --git a/Session/Notifications/NotificationPresenter.swift b/Session/Notifications/NotificationPresenter.swift index 17ec5ac5ce..c335c898bd 100644 --- a/Session/Notifications/NotificationPresenter.swift +++ b/Session/Notifications/NotificationPresenter.swift @@ -31,6 +31,12 @@ public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate, /// Populate the notification settings from `libSession` and the database Task.detached(priority: .high) { [weak self] in + do { try await dependencies.waitUntilInitialised(cache: .libSession) } + catch { + Log.error("[NotificationPresenter] Failed to wait until libSession initialised: \(error)") + return + } + typealias GlobalSettings = ( sound: Preferences.Sound, previewType: Preferences.NotificationPreviewType diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift index 7db2b8b1d4..613ece9d01 100644 --- a/SessionMessagingKit/Calls/NoopSessionCallManager.swift +++ b/SessionMessagingKit/Calls/NoopSessionCallManager.swift @@ -2,8 +2,9 @@ import Foundation import CallKit +import SessionUtilitiesKit -internal struct NoopSessionCallManager: CallManagerProtocol { +internal struct NoopSessionCallManager: CallManagerProtocol, NoopDependency { var currentCall: CurrentCallProtocol? func setCurrentCall(_ call: CurrentCallProtocol?) {} diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index f90e4f37b6..8bbd82e93b 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -1197,7 +1197,7 @@ public extension LibSessionCacheType { } } -private final class NoopLibSessionCache: LibSessionCacheType { +private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { let dependencies: Dependencies let userSessionId: SessionId = .invalid let isEmpty: Bool = true diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift index aa0cdcefa0..3cad0390c1 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift @@ -413,7 +413,7 @@ public extension NotificationsManagerType { // MARK: - NoopNotificationsManager -public struct NoopNotificationsManager: NotificationsManagerType { +public struct NoopNotificationsManager: NotificationsManagerType, NoopDependency { public let dependencies: Dependencies public init(using dependencies: Dependencies) { diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index 326b5e729f..c445d3c433 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -917,7 +917,7 @@ public extension LibSession { func snodeCacheSize() -> Int } - class NoopNetworkCache: NetworkCacheType { + class NoopNetworkCache: NetworkCacheType, NoopDependency { public var isSuspended: Bool { return false } public var networkStatus: AnyPublisher { Just(NetworkStatus.unknown).eraseToAnyPublisher() diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index acdca842a6..9151b37dc2 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -13,6 +13,10 @@ public class Dependencies { @ThreadSafeObject private static var cachedIsRTLRetriever: (requiresMainThread: Bool, retriever: () -> Bool) = (false, { false }) @ThreadSafeObject private var storage: DependencyStorage = DependencyStorage() + private typealias DependencyChange = (Dependencies.DependencyStorage.Key, DependencyStorage.Value?) + private let dependecyChangeStream: AsyncStream + private let dependecyChangeContinuation: AsyncStream.Continuation + // MARK: - Subscript Access public subscript(singleton singleton: SingletonConfig) -> S { getOrCreate(singleton) } @@ -37,6 +41,11 @@ public class Dependencies { // MARK: - Initialization public static func createEmpty() -> Dependencies { return Dependencies() } + private init() { + let (stream, continuation) = AsyncStream.makeStream(of: DependencyChange.self) + dependecyChangeStream = stream + dependecyChangeContinuation = continuation + } // MARK: - Functions @@ -112,7 +121,7 @@ public class Dependencies { return elements.popRandomElement() } - // MARK: - Instance replacing + // MARK: - Instance management public func warmCache(cache: CacheConfig) { _ = getOrCreate(cache) @@ -134,6 +143,26 @@ public class Dependencies { public static func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) { _cachedIsRTLRetriever.set(to: (requiresMainThread, isRTLRetriever)) } + + private func waitUntilInitialised(targetKey: Dependencies.DependencyStorage.Key) async throws { + /// If we already have an instance (which isn't a `NoopDependency`) then no need to observe the stream + guard !_storage.performMap({ $0.instances[targetKey]?.isNoop == false }) else { return } + + for await (key, instance) in dependecyChangeStream { + /// If the target instance has been set (and isn't a `NoopDependency`) then we can stop waiting (observing the stream) + if key == targetKey && instance?.isNoop == false { + break + } + } + } + + public func waitUntilInitialised(singleton: SingletonConfig) async throws { + try await waitUntilInitialised(targetKey: DependencyStorage.Key.Variant.singleton.key(singleton.identifier)) + } + + public func waitUntilInitialised(cache: CacheConfig) async throws { + try await waitUntilInitialised(targetKey: DependencyStorage.Key.Variant.cache.key(cache.identifier)) + } } // MARK: - Cache Management @@ -196,6 +225,7 @@ public extension Dependencies { removeValue(feature.identifier, of: .feature) /// Notify observers + dependecyChangeContinuation.yield((key, nil)) notifyAsync(events: [ ObservedEvent(key: .feature(feature), value: nil), ObservedEvent(key: .featureGroup(feature), value: nil) @@ -244,6 +274,15 @@ private extension Dependencies { case userDefaults(UserDefaultsType) case feature(any FeatureType) + var isNoop: Bool { + switch self { + case .singleton(let value): return value is NoopDependency + case .userDefaults(let value): return value is NoopDependency + case .feature(let value): return value is NoopDependency + case .cache(let value): return value.performMap { $0 is NoopDependency } + } + } + func distinctKey(for identifier: String) -> Key { switch self { case .singleton: return Key(identifier, of: .singleton) @@ -340,18 +379,30 @@ private extension Dependencies { /// Convenience method to store a dependency instance in memory in a thread-safe way @discardableResult private func setValue(_ value: T, typedStorage: DependencyStorage.Value, key: String) -> T { - return _storage.performUpdateAndMap { storage in - storage.instances[typedStorage.distinctKey(for: key)] = typedStorage + let finalKey: DependencyStorage.Key = typedStorage.distinctKey(for: key) + let result: T = _storage.performUpdateAndMap { storage in + storage.instances[finalKey] = typedStorage return (storage, value) } + + /// We generally _shouldn't_ be setting a dependency to a no-op value so log a warning when we do so + if typedStorage.isNoop { + Log.warn("Setting noop dependency for \(key)") + } + + dependecyChangeContinuation.yield((finalKey, typedStorage)) + return result } /// Convenience method to remove a dependency instance from memory in a thread-safe way private func removeValue(_ key: String, of variant: DependencyStorage.Key.Variant) { + let finalKey: DependencyStorage.Key = variant.key(key) _storage.performUpdate { storage in - storage.instances.removeValue(forKey: variant.key(key)) + storage.instances.removeValue(forKey: finalKey) return storage } + + dependecyChangeContinuation.yield((finalKey, nil)) } } diff --git a/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift b/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift new file mode 100644 index 0000000000..7907a37de6 --- /dev/null +++ b/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift @@ -0,0 +1,5 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol NoopDependency {} diff --git a/SessionUtilitiesKit/General/AppContext.swift b/SessionUtilitiesKit/General/AppContext.swift index 867627f3a1..cf47d1f0c3 100644 --- a/SessionUtilitiesKit/General/AppContext.swift +++ b/SessionUtilitiesKit/General/AppContext.swift @@ -54,7 +54,7 @@ public extension AppContext { func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {} } -private final class NoopAppContext: AppContext { +private final class NoopAppContext: AppContext, NoopDependency { let mainWindow: UIWindow? = nil let frontMostViewController: UIViewController? = nil From 0a6bb77b097cd254cf3df70f18fd2476ec59755d Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 11:06:56 +0800 Subject: [PATCH 054/244] Fix hash id displayed instead of contact name in apple watch call --- .../Call Management/SessionCallManager+CXCallController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 36ae7cb867..06b36c9b6f 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -16,7 +16,8 @@ extension SessionCallManager { reportOutgoingCall(call) if callController != nil { - let handle = CXHandle(type: .generic, value: call.sessionId) + // Show contact name opening outgoing call in apple watch + let handle = CXHandle(type: .generic, value: call.contactName) let startCallAction = CXStartCallAction(call: call.callId, handle: handle) startCallAction.isVideo = false From 2f37ad77bbee4ff90f7f48f91029e2e59086c646 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 11:17:49 +0800 Subject: [PATCH 055/244] Renamed `accessibilityLabel` to `accessibilityIdentifier` --- Session/Settings/AppIconViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 704e945d9c..122cfe1af9 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -25,7 +25,7 @@ enum AppIcon: String, CaseIterable { var previewImageName: String { "\(rawValue)-Preview" } // stringlint:ignore_contents - var accessibilityLabel: String { + var accessibilityIdentifier: String { switch self { case .session: "Session option" @@ -172,7 +172,7 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl ) ), accessibility: Accessibility( - identifier: AppIcon(name: current).accessibilityLabel + identifier: AppIcon(name: current).accessibilityIdentifier ) ) ] From 1d4d106683b6cac03f8727b9d62d68adc637b8dd Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 12 Aug 2025 11:42:41 +0800 Subject: [PATCH 056/244] Fix bugs on reply and search switching --- Session/Conversations/ConversationVC+Interaction.swift | 3 +-- Session/Conversations/ConversationVC.swift | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 08139c0db7..8933cff6cd 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2212,8 +2212,7 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - // Dismiss search textfield - if isShowingSearchUI { hideSearchUI() } + if isShowingSearchUI { willManuallyCancelSearchUI() } _ = snInputView.becomeFirstResponder() completion?() diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e3a7228511..3328f01c6d 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1978,6 +1978,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa becomeFirstResponder() reloadInputViews() } + + // Manually cancel the search and clear the text to remove hightlights + func willManuallyCancelSearchUI() { + searchController.uiSearchController.isActive = false + searchController.uiSearchController.searchBar.text = "" + } func didDismissSearchController(_ searchController: UISearchController) { hideSearchUI() From cf3bbce61be052003c996dda08772643b4b851d5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 12 Aug 2025 13:50:58 +1000 Subject: [PATCH 057/244] Fixed a unit test build issue --- SessionUtilitiesKit/Dependency Injection/Dependencies.swift | 6 ++++-- _SharedTestUtilities/TestDependencies.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 9151b37dc2..8103d8bfe0 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -40,8 +40,10 @@ public class Dependencies { // MARK: - Initialization - public static func createEmpty() -> Dependencies { return Dependencies() } - private init() { + public static func createEmpty() -> Dependencies { return Dependencies(forTesting: false) } + + /// This constructor should not be used directly (except for `TestDependencies`), use `Dependencies.createEmpty()` instead + internal init(forTesting: Bool) { let (stream, continuation) = AsyncStream.makeStream(of: DependencyChange.self) dependecyChangeStream = stream dependecyChangeContinuation = continuation diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 0815ea21df..3ac46b51b4 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -105,7 +105,7 @@ public class TestDependencies: Dependencies { // MARK: - Initialization public init(initialState: ((TestDependencies) -> ())? = nil) { - super.init() + super.init(forTesting: true) initialState?(self) } From 9841df993fddeb09206e1f30a2ed6bd6cb94f59e Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 12 Aug 2025 15:41:53 +1000 Subject: [PATCH 058/244] clean up and merge last profile updated timestamps --- Session.xcodeproj/project.pbxproj | 4 ++ .../ConversationVC+Interaction.swift | 3 +- Session/Onboarding/Onboarding.swift | 3 +- Session/Settings/SettingsViewModel.swift | 6 +-- SessionMessagingKit/Configuration.swift | 3 +- .../_030_LastProfileUpdateTimestamp.swift | 22 ++++++++ .../Database/Models/Profile.swift | 50 +++++++------------ .../Jobs/DisplayPictureDownloadJob.swift | 6 +-- .../Jobs/UpdateProfilePictureJob.swift | 4 +- .../Config Handling/LibSession+Contacts.swift | 40 +++++++-------- .../LibSession+GroupMembers.swift | 7 +-- .../Config Handling/LibSession+Shared.swift | 9 ++-- .../LibSession+UserProfile.swift | 1 - .../MessageReceiver+Groups.swift | 5 +- .../MessageReceiver+MessageRequests.swift | 2 - .../MessageReceiver+VisibleMessages.swift | 1 - .../Sending & Receiving/MessageSender.swift | 6 +-- .../Utilities/DisplayPictureManager.swift | 2 +- .../Utilities/Profile+CurrentUser.swift | 37 ++++++-------- .../Components/ProfilePictureView.swift | 2 + 20 files changed, 101 insertions(+), 112 deletions(-) create mode 100644 SessionMessagingKit/Database/Migrations/_030_LastProfileUpdateTimestamp.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 74d23532aa..fc7217e4c3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942256A02C23F90700C0FDBF /* CustomTopTabBar.swift */; }; 9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */; }; 942ADDD42D9F9613006E0BB0 /* NewTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */; }; + 942BA9BF2E4ABBA1007C4595 /* _030_LastProfileUpdateTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942BA9BE2E4ABB9F007C4595 /* _030_LastProfileUpdateTimestamp.swift */; }; 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; }; 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */; }; 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; @@ -1543,6 +1544,7 @@ 942256A02C23F90700C0FDBF /* CustomTopTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTopTabBar.swift; sourceTree = ""; }; 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTagView.swift; sourceTree = ""; }; + 942BA9BE2E4ABB9F007C4595 /* _030_LastProfileUpdateTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_LastProfileUpdateTimestamp.swift; sourceTree = ""; }; 94367C422C6C828500814252 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+DisappearingMessages.swift"; sourceTree = ""; }; 943C6D832B86B5F1004ACE64 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; @@ -4051,6 +4053,7 @@ FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */, FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */, 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */, + 942BA9BE2E4ABB9F007C4595 /* _030_LastProfileUpdateTimestamp.swift */, ); path = Migrations; sourceTree = ""; @@ -6653,6 +6656,7 @@ FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD245C56285065EA00B966DD /* SNProto.swift in Sources */, FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */, + 942BA9BF2E4ABBA1007C4595 /* _030_LastProfileUpdateTimestamp.swift in Sources */, FDE754F12C9BB08B002A2623 /* Crypto+LibSession.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 81789ed7c4..72cc1bc87b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -800,8 +800,7 @@ extension ConversationVC: fallback: .none, using: dependencies ), - profileUpdateTimestamp: (currentUserProfile.lastNameUpdate ?? sentTimestamp), - sentTimestamp: sentTimestamp, + profileUpdateTimestamp: (currentUserProfile.profileLastUpdated ?? sentTimestamp), using: dependencies ) } diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 8bad85d79a..481fee065d 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -405,14 +405,13 @@ extension Onboarding { .upsert(db) try Profile .filter(id: userSessionId.hexString) - .updateAll(db, Profile.Columns.lastNameUpdate.set(to: nil)) + .updateAll(db, Profile.Columns.profileLastUpdated.set(to: nil)) try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, displayNameUpdate: .currentUserUpdate(displayName), displayPictureUpdate: .none, profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 0b52821b5e..8f8ade916a 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -723,11 +723,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } self?.updateProfile( - displayPictureUpdate: .currentUserUploadImageData( - data: imageData, - sessionProProof: !isAnimatedImage ? nil : - dependencies.mutate(cache: .libSession, { $0.getProProof() }) - ), + displayPictureUpdate: .currentUserUploadImageData(data: imageData), onComplete: { [weak modal] in modal?.close() } ) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 128fe2579f..31cac47c2a 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -48,7 +48,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API [ _027_MoveSettingsToLibSession.self, _028_RenameAttachments.self, - _029_AddProMessageFlag.self + _029_AddProMessageFlag.self, + _030_LastProfileUpdateTimestamp.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_030_LastProfileUpdateTimestamp.swift b/SessionMessagingKit/Database/Migrations/_030_LastProfileUpdateTimestamp.swift new file mode 100644 index 0000000000..b12a2caa05 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_030_LastProfileUpdateTimestamp.swift @@ -0,0 +1,22 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _030_LastProfileUpdateTimestamp: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "LastProfileUpdateTimestamp" + static let minExpectedRunDuration: TimeInterval = 0.1 + static var createdTables: [(FetchableRecord & TableRecord).Type] = [] + + static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws { + try db.alter(table: "Profile") { t in + t.drop(column: "lastNameUpdate") + t.drop(column: "lastBlocksCommunityMessageRequests") + t.rename(column: "displayPictureLastUpdated", to: "profileLastUpdated") + } + + MigrationExecution.updateProgress(1) + } +} diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index d8ba4276e8..43e378b725 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -21,15 +21,14 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet case id case name - case lastNameUpdate case nickname case displayPictureUrl case displayPictureEncryptionKey - case displayPictureLastUpdated + + case profileLastUpdated case blocksCommunityMessageRequests - case lastBlocksCommunityMessageRequests case sessionProProof } @@ -40,9 +39,6 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet /// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message). public let name: String - /// The timestamp (in seconds since epoch) that the name was last updated - public let lastNameUpdate: TimeInterval? - /// A custom name for the profile set by the current user public let nickname: String? @@ -54,16 +50,14 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet /// The key with which the profile is encrypted. public let displayPictureEncryptionKey: Data? - /// The timestamp (in seconds since epoch) that the profile picture was last updated - public let displayPictureLastUpdated: TimeInterval? + /// The timestamp (in seconds since epoch) that the profile was last updated + public let profileLastUpdated: TimeInterval? /// A flag indicating whether this profile has reported that it blocks community message requests public let blocksCommunityMessageRequests: Bool? - /// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated - public let lastBlocksCommunityMessageRequests: TimeInterval? - /// The Pro Proof for when this profile is updated + // TODO: Implement this when the structure of Session Pro Proof is determined public let sessionProProof: String? // MARK: - Initialization @@ -71,24 +65,20 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet public init( id: String, name: String, - lastNameUpdate: TimeInterval? = nil, nickname: String? = nil, displayPictureUrl: String? = nil, displayPictureEncryptionKey: Data? = nil, - displayPictureLastUpdated: TimeInterval? = nil, + profileLastUpdated: TimeInterval? = nil, blocksCommunityMessageRequests: Bool? = nil, - lastBlocksCommunityMessageRequests: TimeInterval? = nil, sessionProProof: String? = nil ) { self.id = id self.name = name - self.lastNameUpdate = lastNameUpdate self.nickname = nickname self.displayPictureUrl = displayPictureUrl self.displayPictureEncryptionKey = displayPictureEncryptionKey - self.displayPictureLastUpdated = displayPictureLastUpdated + self.profileLastUpdated = profileLastUpdated self.blocksCommunityMessageRequests = blocksCommunityMessageRequests - self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests self.sessionProProof = sessionProProof } } @@ -111,13 +101,11 @@ extension Profile: CustomStringConvertible, CustomDebugStringConvertible { Profile( id: \(id), name: \(name), - lastNameUpdate: \(lastNameUpdate.map { "\($0)" } ?? "null"), nickname: \(nickname.map { "\($0)" } ?? "null"), displayPictureUrl: \(displayPictureUrl.map { "\"\($0)\"" } ?? "null"), displayPictureEncryptionKey: \(displayPictureEncryptionKey?.toHexString() ?? "null"), - displayPictureLastUpdated: \(displayPictureLastUpdated.map { "\($0)" } ?? "null"), + profileLastUpdated: \(profileLastUpdated.map { "\($0)" } ?? "null"), blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null"), - lastBlocksCommunityMessageRequests: \(lastBlocksCommunityMessageRequests.map { "\($0)" } ?? "null") sessionProProof: \(sessionProProof.map { "\($0)" } ?? "null") ) """ @@ -145,13 +133,11 @@ public extension Profile { self = Profile( id: try container.decode(String.self, forKey: .id), name: try container.decode(String.self, forKey: .name), - lastNameUpdate: try? container.decode(TimeInterval?.self, forKey: .lastNameUpdate), nickname: try? container.decode(String?.self, forKey: .nickname), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: displayPictureKey, - displayPictureLastUpdated: try? container.decode(TimeInterval?.self, forKey: .displayPictureLastUpdated), + profileLastUpdated: try? container.decode(TimeInterval?.self, forKey: .profileLastUpdated), blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests), - lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval?.self, forKey: .lastBlocksCommunityMessageRequests), sessionProProof: try? container.decode(String?.self, forKey: .sessionProProof) ) } @@ -161,13 +147,11 @@ public extension Profile { try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) - try container.encodeIfPresent(lastNameUpdate, forKey: .lastNameUpdate) try container.encodeIfPresent(nickname, forKey: .nickname) try container.encodeIfPresent(displayPictureUrl, forKey: .displayPictureUrl) try container.encodeIfPresent(displayPictureEncryptionKey, forKey: .displayPictureEncryptionKey) - try container.encodeIfPresent(displayPictureLastUpdated, forKey: .displayPictureLastUpdated) + try container.encodeIfPresent(profileLastUpdated, forKey: .profileLastUpdated) try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) - try container.encodeIfPresent(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests) try container.encodeIfPresent(sessionProProof, forKey: .sessionProProof) } } @@ -189,6 +173,10 @@ public extension Profile { // TODO: Add ProProof if needed } + if let profileLastUpdated: TimeInterval = profileLastUpdated { + profileProto.setProfileUpdateTimestamp(UInt64(profileLastUpdated)) + } + do { dataMessageProto.setProfile(try profileProto.build()) return try dataMessageProto.build() @@ -230,13 +218,12 @@ public extension Profile { return Profile( id: id, name: "", - lastNameUpdate: nil, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, + profileLastUpdated: nil, blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + sessionProProof: nil ) } @@ -446,13 +433,12 @@ public extension Profile { return Profile( id: id, name: (name ?? self.name), - lastNameUpdate: lastNameUpdate, nickname: (nickname ?? self.nickname), displayPictureUrl: (displayPictureUrl ?? self.displayPictureUrl), displayPictureEncryptionKey: displayPictureEncryptionKey, - displayPictureLastUpdated: displayPictureLastUpdated, + profileLastUpdated: profileLastUpdated, blocksCommunityMessageRequests: blocksCommunityMessageRequests, - lastBlocksCommunityMessageRequests: lastBlocksCommunityMessageRequests + sessionProProof: self.sessionProProof ) } } diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index 77e3be6c83..2e840e1a61 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -192,7 +192,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { db, Profile.Columns.displayPictureUrl.set(to: url), Profile.Columns.displayPictureEncryptionKey.set(to: encryptionKey), - Profile.Columns.displayPictureLastUpdated.set(to: details.timestamp), + Profile.Columns.profileLastUpdated.set(to: details.timestamp), using: dependencies ) db.addProfileEvent(id: id, change: .displayPictureUrl(url)) @@ -302,7 +302,7 @@ extension DisplayPictureDownloadJob { let key: Data = profile.displayPictureEncryptionKey, let details: Details = Details( target: .profile(id: profile.id, url: url, encryptionKey: key), - timestamp: (profile.displayPictureLastUpdated ?? 0) + timestamp: (profile.profileLastUpdated ?? 0) ) else { return nil } @@ -347,7 +347,7 @@ extension DisplayPictureDownloadJob { guard let latestProfile: Profile = try? Profile.fetchOne(db, id: id) else { return false } return ( - timestamp >= (latestProfile.displayPictureLastUpdated ?? 0) || ( + timestamp >= (latestProfile.profileLastUpdated ?? 0) || ( encryptionKey == latestProfile.displayPictureEncryptionKey && url == latestProfile.displayPictureUrl ) diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index b71048a2c9..5d4915579e 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -40,11 +40,11 @@ public enum UpdateProfilePictureJob: JobExecutor { lastUploadDate.map({ dependencies.dateNow.timeIntervalSince($0) > (14 * 24 * 60 * 60) }) == true { /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` - let (profile, sessionProProof) = dependencies.mutate(cache: .libSession) { ($0.profile, $0.getProProof()) } + let profile = dependencies.mutate(cache: .libSession) { $0.profile } let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } .map { dependencies[singleton: .fileManager].contents(atPath: $0) } - .map { .currentUserUploadImageData(data: $0, sessionProProof: sessionProProof)} + .map { .currentUserUploadImageData(data: $0)} .defaulting(to: .none) Profile diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index f0b905ecc3..187f58a473 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -24,6 +24,7 @@ internal extension LibSession { Profile.Columns.nickname, Profile.Columns.displayPictureUrl, Profile.Columns.displayPictureEncryptionKey, + Profile.Columns.profileLastUpdated, DisappearingMessagesConfiguration.Columns.isEnabled, DisappearingMessagesConfiguration.Columns.type, DisappearingMessagesConfiguration.Columns.durationSeconds @@ -62,24 +63,20 @@ internal extension LibSessionCacheType { // observation system can't differ between update calls which do and don't change anything) let contact: Contact = Contact.fetchOrCreate(db, id: sessionId, using: dependencies) let profile: Profile = Profile.fetchOrCreate(db, id: sessionId) - let profileNameShouldBeUpdated: Bool = ( - !data.profile.name.isEmpty && - profile.name != data.profile.name && - (profile.lastNameUpdate ?? 0) < (data.profile.lastNameUpdate ?? 0) - ) - let profilePictureShouldBeUpdated: Bool = ( - ( - profile.displayPictureUrl != data.profile.displayPictureUrl || - profile.displayPictureEncryptionKey != data.profile.displayPictureEncryptionKey - ) && - (profile.displayPictureLastUpdated ?? 0) < (data.profile.displayPictureLastUpdated ?? 0) - ) if - profileNameShouldBeUpdated || - profile.nickname != data.profile.nickname || - profilePictureShouldBeUpdated + (profile.profileLastUpdated ?? 0) < (data.profile.profileLastUpdated ?? 0) || + profile.nickname != data.profile.nickname { + let profileNameShouldBeUpdated: Bool = ( + !data.profile.name.isEmpty && + profile.name != data.profile.name + ) + let profilePictureShouldBeUpdated: Bool = ( + profile.displayPictureUrl != data.profile.displayPictureUrl || + profile.displayPictureEncryptionKey != data.profile.displayPictureEncryptionKey + ) + try profile.upsert(db) try Profile .filter(id: sessionId) @@ -89,9 +86,6 @@ internal extension LibSessionCacheType { (!profileNameShouldBeUpdated ? nil : Profile.Columns.name.set(to: data.profile.name) ), - (!profileNameShouldBeUpdated ? nil : - Profile.Columns.lastNameUpdate.set(to: data.profile.lastNameUpdate) - ), (profile.nickname == data.profile.nickname ? nil : Profile.Columns.nickname.set(to: data.profile.nickname) ), @@ -102,7 +96,7 @@ internal extension LibSessionCacheType { Profile.Columns.displayPictureEncryptionKey.set(to: data.profile.displayPictureEncryptionKey) ), (!profilePictureShouldBeUpdated ? nil : - Profile.Columns.displayPictureLastUpdated.set(to: data.profile.displayPictureLastUpdated) + Profile.Columns.profileLastUpdated.set(to: data.profile.profileLastUpdated) ) ].compactMap { $0 }, using: dependencies @@ -343,6 +337,7 @@ public extension LibSession { contact.set(\.nickname, to: info.nickname) contact.set(\.profile_pic.url, to: info.displayPictureUrl) contact.set(\.profile_pic.key, to: info.displayPictureEncryptionKey) + contact.set(\.profile_updated, to: (info.profileLastUpdated ?? Int64(dependencies.dateNow.timeIntervalSince1970))) // Attempts retrieval of the profile picture (will schedule a download if // needed via a throttled subscription on another thread to prevent blocking) @@ -740,6 +735,7 @@ extension LibSession { let nickname: String? let displayPictureUrl: String? let displayPictureEncryptionKey: Data? + let profileLastUpdated: Int64? let disappearingMessagesInfo: DisappearingMessageInfo? let priority: Int32? @@ -775,6 +771,7 @@ extension LibSession { nickname: profile?.nickname, displayPictureUrl: profile?.displayPictureUrl, displayPictureEncryptionKey: profile?.displayPictureEncryptionKey, + profileLastUpdated: profile?.profileLastUpdated.map({ Int64($0) }), disappearingMessagesInfo: disappearingMessagesConfig.map { DisappearingMessageInfo( isEnabled: $0.isEnabled, @@ -797,6 +794,7 @@ extension LibSession { nickname: String? = nil, displayPictureUrl: String? = nil, displayPictureEncryptionKey: Data? = nil, + profileLastUpdated: Int64? = nil, disappearingMessagesInfo: DisappearingMessageInfo? = nil, priority: Int32? = nil, created: TimeInterval? = nil @@ -810,6 +808,7 @@ extension LibSession { self.nickname = nickname self.displayPictureUrl = displayPictureUrl self.displayPictureEncryptionKey = displayPictureEncryptionKey + self.profileLastUpdated = profileLastUpdated self.disappearingMessagesInfo = disappearingMessagesInfo self.priority = priority self.created = created @@ -875,11 +874,10 @@ internal extension LibSession { let profileResult: Profile = Profile( id: contactId, name: contact.get(\.name), - lastNameUpdate: TimeInterval(contact.profile_updated), nickname: contact.get(\.nickname, nullIfEmpty: true), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - displayPictureLastUpdated: TimeInterval(contact.profile_updated) + profileLastUpdated: TimeInterval(contact.profile_updated) ) let configResult: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration( threadId: contactId, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 5523efe6ea..ea1a048e02 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -134,8 +134,7 @@ internal extension LibSessionCacheType { publicKey: profile.id, displayNameUpdate: .contactUpdate(profile.name), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), - profileUpdateTimestamp: (profile.displayPictureLastUpdated ?? 0), - sentTimestamp: TimeInterval(Double(serverTimestampMs) * 1000), + profileUpdateTimestamp: (profile.profileLastUpdated ?? 0), using: dependencies ) } @@ -523,14 +522,12 @@ internal extension LibSession { Profile( id: member.get(\.session_id), name: member.get(\.name), - lastNameUpdate: TimeInterval(Double(serverTimestampMs) / 1000), nickname: nil, displayPictureUrl: member.get(\.profile_pic.url, nullIfEmpty: true), displayPictureEncryptionKey: (member.get(\.profile_pic.url, nullIfEmpty: true) == nil ? nil : member.get(\.profile_pic.key) ), - displayPictureLastUpdated: TimeInterval(member.profile_updated), - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: TimeInterval(member.profile_updated) ) ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 522957def1..318c9b5522 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -782,11 +782,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: String(cString: profileNamePtr), - lastNameUpdate: nil, nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : displayPic.get(\.key)), - displayPictureLastUpdated: nil + profileLastUpdated: nil ) } @@ -812,11 +811,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: (displayNameInMessage ?? member.get(\.name)), - lastNameUpdate: nil, nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : member.get(\.profile_pic.key)), - displayPictureLastUpdated: nil + profileLastUpdated: nil ) } @@ -838,11 +836,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: (displayNameInMessage ?? contact.get(\.name)), - lastNameUpdate: nil, nickname: contact.get(\.nickname, nullIfEmpty: true), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - displayPictureLastUpdated: nil + profileLastUpdated: nil ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 29b30d108c..4f4bd20321 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -75,7 +75,6 @@ internal extension LibSessionCacheType { ) }(), profileUpdateTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), - sentTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 9cb37a963e..1e2397e4e8 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -148,7 +148,6 @@ extension MessageReceiver { displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, profileUpdateTimestamp: profileUpdateTimestamp, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) } @@ -254,7 +253,6 @@ extension MessageReceiver { displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, profileUpdateTimestamp: profileUpdateTimestamp, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) } @@ -616,7 +614,6 @@ extension MessageReceiver { displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, profileUpdateTimestamp: profileUpdateTimestamp, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), using: dependencies ) } @@ -633,7 +630,7 @@ extension MessageReceiver { name: $0, displayPictureUrl: profile.profilePictureUrl, displayPictureEncryptionKey: profile.profileKey, - displayPictureLastUpdated: (Double(sentTimestampMs) / 1000) + profileLastUpdated: (Double(sentTimestampMs) / 1000) ) } }, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index 3e58042901..5ef994af97 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -27,7 +27,6 @@ extension MessageReceiver { // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { let messageSentTimestampMs: UInt64 = message.sentTimestampMs ?? 0 - let messageSentTimestamp: TimeInterval = TimeInterval(Double(messageSentTimestampMs) / 1000) let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? messageSentTimestampMs) / 1000) try Profile.updateIfNeeded( @@ -36,7 +35,6 @@ extension MessageReceiver { displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), profileUpdateTimestamp: profileUpdateTimestamp, - sentTimestamp: messageSentTimestamp, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 6c23c7b46c..b10c6e6958 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -46,7 +46,6 @@ extension MessageReceiver { displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, profileUpdateTimestamp: profileUpdateTimestamp, - sentTimestamp: messageSentTimestamp, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index f29f9a568a..18792e50e8 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -171,7 +171,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) } + updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) } ) } } @@ -270,7 +270,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) }, + updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) }, blocksCommunityMessageRequests: !checkForCommunityMessageRequests ) } @@ -337,7 +337,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.displayPictureLastUpdated.map { UInt64($0) } + updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) } ) } diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 30ff3f0dc3..06e4ba573c 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -34,7 +34,7 @@ public class DisplayPictureManager { case contactUpdateTo(url: String, key: Data, filePath: String, contactProProof: String?) case currentUserRemove - case currentUserUploadImageData(data: Data, sessionProProof: String?) + case currentUserUploadImageData(data: Data) case currentUserUpdateTo(url: String, key: Data, filePath: String, sessionProProof: String?) case groupRemove diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 64094e9bca..7adf3308d9 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -85,7 +85,6 @@ public extension Profile { displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) Log.info(.profile, "Successfully updated user profile.") @@ -93,7 +92,7 @@ public extension Profile { .mapError { _ in DisplayPictureError.databaseChangesFailed } .eraseToAnyPublisher() - case .currentUserUploadImageData(let data, let sessionProProof): + case .currentUserUploadImageData(let data): return dependencies[singleton: .displayPictureManager] .prepareAndUploadDisplayPicture(imageData: data) .mapError { $0 as Error } @@ -106,10 +105,9 @@ public extension Profile { url: result.downloadUrl, key: result.encryptionKey, filePath: result.filePath, - sessionProProof: sessionProProof + sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -134,21 +132,20 @@ public extension Profile { displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, profileUpdateTimestamp: TimeInterval, - sentTimestamp: TimeInterval, using dependencies: Dependencies ) throws { let isCurrentUser = (publicKey == dependencies[cache: .general].sessionId.hexString) let profile: Profile = Profile.fetchOrCreate(db, id: publicKey) var profileChanges: [ConfigColumnAssignment] = [] + guard profileUpdateTimestamp > profile.profileLastUpdated.defaulting(to: 0) else { return } + // Name - switch (displayNameUpdate, isCurrentUser, (profileUpdateTimestamp > profile.lastNameUpdate.defaulting(to: 0))) { - case (.none, _, _): break - case (.currentUserUpdate(let name), true, true), (.contactUpdate(let name), false, true): + switch (displayNameUpdate, isCurrentUser) { + case (.none, _): break + case (.currentUserUpdate(let name), true), (.contactUpdate(let name), false): guard let name: String = name, !name.isEmpty, name != profile.name else { break } - profileChanges.append(Profile.Columns.lastNameUpdate.set(to: profileUpdateTimestamp)) - if profile.name != name { profileChanges.append(Profile.Columns.name.set(to: name)) db.addProfileEvent(id: publicKey, change: .name(name)) @@ -159,20 +156,17 @@ public extension Profile { } // Blocks community message requests flag - if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests.defaulting(to: 0) { + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests { profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) - profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) } // Profile picture & profile key - switch (displayPictureUpdate, isCurrentUser, (profileUpdateTimestamp > profile.displayPictureLastUpdated.defaulting(to: 0))) { - case (.none, _, _): break - case (.currentUserUploadImageData, _, _), (.groupRemove, _, _), (.groupUpdateTo, _, _): + switch (displayPictureUpdate, isCurrentUser) { + case (.none, _): break + case (.currentUserUploadImageData, _), (.groupRemove, _), (.groupUpdateTo, _): preconditionFailure("Invalid options for this function") - case (.contactRemove, false, true), (.currentUserRemove, true, true): - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: profileUpdateTimestamp)) - + case (.contactRemove, false), (.currentUserRemove, true): if profile.displayPictureEncryptionKey != nil { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: nil)) } @@ -182,8 +176,8 @@ public extension Profile { db.addProfileEvent(id: publicKey, change: .displayPictureUrl(nil)) } - case (.contactUpdateTo(let url, let key, let filePath, let proProof), false, true), - (.currentUserUpdateTo(let url, let key, let filePath, let proProof), true, true): + case (.contactUpdateTo(let url, let key, let filePath, let proProof), false), + (.currentUserUpdateTo(let url, let key, let filePath, let proProof), true): /// If we have already downloaded the image then no need to download it again (the database records will be updated /// once the download completes) if !dependencies[singleton: .fileManager].fileExists(atPath: filePath) { @@ -210,7 +204,6 @@ public extension Profile { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: key)) } - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: profileUpdateTimestamp)) profileChanges.append(Profile.Columns.sessionProProof.set(to: proProof)) } @@ -220,6 +213,8 @@ public extension Profile { // Persist any changes if !profileChanges.isEmpty { + profileChanges.append(Profile.Columns.profileLastUpdated.set(to: profileUpdateTimestamp)) + try profile.upsert(db) try Profile diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index a6f52293a7..29d33402b9 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -497,6 +497,8 @@ public final class ProfilePictureView: UIView { // MARK: - Content private func prepareForReuse() { + currentUserProfileImage = .none + imageView.image = nil imageView.shouldAnimateImage = true imageView.contentMode = .scaleAspectFill From a65841ab31a7043147800077907ee19e79a09418 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 12 Aug 2025 16:16:07 +1000 Subject: [PATCH 059/244] wip: fix unit test --- .../Jobs/DisplayPictureDownloadJobSpec.swift | 18 +++++++++--------- SessionTests/Onboarding/OnboardingSpec.swift | 12 ++++-------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 0ca3ed5345..052cad6450 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -590,7 +590,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil + profileLastUpdated: nil ) mockStorage.write { db in try profile.insert(db) } job = Job( @@ -708,7 +708,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567890 + profileLastUpdated: 1234567890 ) mockStorage.write { db in _ = try Profile.deleteAll(db) @@ -757,7 +757,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { .updateAll( db, Profile.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])), - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -780,7 +780,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -794,7 +794,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { .updateAll( db, Profile.Columns.displayPictureUrl.set(to: "testUrl"), - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -817,7 +817,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -830,7 +830,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { try Profile .updateAll( db, - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -862,7 +862,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -877,7 +877,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e6a4e41559..2404a6cec0 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -583,13 +583,11 @@ class OnboardingSpec: AsyncSpec { Profile( id: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b", name: "TestCompleteName", - lastNameUpdate: 1234567890, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, - blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: 1234567890, + blocksCommunityMessageRequests: nil ) ])) } @@ -623,13 +621,11 @@ class OnboardingSpec: AsyncSpec { Profile( id: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b", name: "TestCompleteName", - lastNameUpdate: nil, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, - blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: nil, + blocksCommunityMessageRequests: nil ) )) } From d03c51eebae80abfa7394acf8f51daf09c24fa13 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 12 Aug 2025 16:54:27 +1000 Subject: [PATCH 060/244] fix unit test --- SessionMessagingKit/Database/Models/Profile.swift | 9 ++------- .../LibSession/Config Handling/LibSession+Pro.swift | 5 ++++- SessionMessagingKit/Utilities/Profile+CurrentUser.swift | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 43e378b725..87824331db 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -29,8 +29,6 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet case profileLastUpdated case blocksCommunityMessageRequests - - case sessionProProof } /// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant) @@ -105,8 +103,7 @@ extension Profile: CustomStringConvertible, CustomDebugStringConvertible { displayPictureUrl: \(displayPictureUrl.map { "\"\($0)\"" } ?? "null"), displayPictureEncryptionKey: \(displayPictureEncryptionKey?.toHexString() ?? "null"), profileLastUpdated: \(profileLastUpdated.map { "\($0)" } ?? "null"), - blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null"), - sessionProProof: \(sessionProProof.map { "\($0)" } ?? "null") + blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null") ) """ } @@ -137,8 +134,7 @@ public extension Profile { displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: displayPictureKey, profileLastUpdated: try? container.decode(TimeInterval?.self, forKey: .profileLastUpdated), - blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests), - sessionProProof: try? container.decode(String?.self, forKey: .sessionProProof) + blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests) ) } @@ -152,7 +148,6 @@ public extension Profile { try container.encodeIfPresent(displayPictureEncryptionKey, forKey: .displayPictureEncryptionKey) try container.encodeIfPresent(profileLastUpdated, forKey: .profileLastUpdated) try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) - try container.encodeIfPresent(sessionProProof, forKey: .sessionProProof) } } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift index afb8a8a0a9..a7841adee2 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift @@ -37,7 +37,10 @@ public extension LibSessionCacheType { func validateProProof(for profile: Profile?) -> Bool { guard let profile = profile else { return false } - return profile.sessionProProof != nil + if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { + return dependencies[feature: .treatAllIncomingMessagesAsProMessages] + } + return false } func getProProof() -> String? { diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 7adf3308d9..851c37bf37 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -203,8 +203,6 @@ public extension Profile { if key != profile.displayPictureEncryptionKey && key.count == DisplayPictureManager.aes256KeyByteLength { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: key)) } - - profileChanges.append(Profile.Columns.sessionProProof.set(to: proProof)) } /// Don't want profiles in messages to modify the current users profile info so ignore those cases From a14265d5046749fe4476c4f48ca1cd85e7d3b388 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 13 Aug 2025 09:58:00 +1000 Subject: [PATCH 061/244] fix an clean up update profile picture job --- .../Jobs/UpdateProfilePictureJob.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index 5d4915579e..6da65d872f 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -18,6 +18,7 @@ public enum UpdateProfilePictureJob: JobExecutor { public static let maxFailureCount: Int = -1 public static let requiresThreadId: Bool = false public static let requiresInteractionId: Bool = false + public static let maxTTL: TimeInterval = (14 * 24 * 60 * 60) public static func run( _ job: Job, @@ -34,11 +35,10 @@ public enum UpdateProfilePictureJob: JobExecutor { let expirationDate: Date? = dependencies[defaults: .standard, key: .profilePictureExpiresDate] let lastUploadDate: Date? = dependencies[defaults: .standard, key: .lastProfilePictureUpload] + let expired: Bool = (expirationDate.map({ dependencies.dateNow.timeIntervalSince($0) > 0 }) == true) + let exceededMaxTTL: Bool = (lastUploadDate.map({ dependencies.dateNow.timeIntervalSince($0) > Self.maxTTL }) == true) - if - expirationDate.map({ dependencies.dateNow.timeIntervalSince($0) > 0 }) == true, - lastUploadDate.map({ dependencies.dateNow.timeIntervalSince($0) > (14 * 24 * 60 * 60) }) == true - { + if (expired || exceededMaxTTL) { /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` let profile = dependencies.mutate(cache: .libSession) { $0.profile } let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl @@ -64,8 +64,7 @@ public enum UpdateProfilePictureJob: JobExecutor { } } ) - } - else { + } else { // Reset the `nextRunTimestamp` value just in case the last run failed so we don't get stuck // in a loop endlessly deferring the job if let jobId: Int64 = job.id { @@ -76,12 +75,11 @@ public enum UpdateProfilePictureJob: JobExecutor { } } - Log.info( - .cat, - expirationDate != nil ? - "Deferred as current picture hasn't expired" : - "Deferred as not enough time has passed since the last update" - ) + if expirationDate != nil { + Log.info(.cat, "Deferred as current picture hasn't expired") + } else { + Log.info(.cat, "Deferred as not enough time has passed since the last update") + } return deferred(job) } From cc613f986fefa7625dad4d37078141efca889c93 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 13 Aug 2025 10:01:23 +1000 Subject: [PATCH 062/244] clean up the mock logic in LibSession+Pro --- .../Config Handling/LibSession+Pro.swift | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift index a7841adee2..31ba81eb19 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift @@ -22,25 +22,18 @@ public extension LibSession { public extension LibSessionCacheType { var isSessionPro: Bool { - if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { - return dependencies[feature: .mockCurrentUserSessionPro] - } - return false + guard dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .mockCurrentUserSessionPro] } func validateProProof(for message: Message?) -> Bool { - if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { - return dependencies[feature: .treatAllIncomingMessagesAsProMessages] - } - return false + guard let message = message, dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .treatAllIncomingMessagesAsProMessages] } func validateProProof(for profile: Profile?) -> Bool { - guard let profile = profile else { return false } - if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { - return dependencies[feature: .treatAllIncomingMessagesAsProMessages] - } - return false + guard let profile = profile, dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .treatAllIncomingMessagesAsProMessages] } func getProProof() -> String? { From 4f1e53ddbf7c5c2071c2ca0fb9d7b0bbe19c630e Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 13 Aug 2025 09:09:35 +0800 Subject: [PATCH 063/244] Display truncated session id along with contact name --- .../SessionCallManager+CXCallController.swift | 6 ++++-- .../Call Management/SessionCallManager.swift | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 06b36c9b6f..07e47ac35a 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -16,8 +16,10 @@ extension SessionCallManager { reportOutgoingCall(call) if callController != nil { - // Show contact name opening outgoing call in apple watch - let handle = CXHandle(type: .generic, value: call.contactName) + // Show contact name + session id (truncated...) opening outgoing call in apple watch + let callDisplay = generateDisplayForCall(call) + + let handle = CXHandle(type: .generic, value: callDisplay) let startCallAction = CXStartCallAction(call: call.callId, handle: handle) startCallAction.isVideo = false diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index f484f6c066..2d7f8eb9dc 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -114,10 +114,13 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } + // Show contact name + session id (truncated...) opening outgoing call in apple watch + let callDisplay = generateDisplayForCall(call) + // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.sessionId) + update.remoteHandle = CXHandle(type: .generic, value: callDisplay) update.hasVideo = false disableUnsupportedFeatures(callUpdate: update) @@ -296,4 +299,20 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { Log.flush() } } + + func generateDisplayForCall(_ call: CurrentCallProtocol) -> String { + guard + let sessionCall = call as? SessionCall, + sessionCall.contactName.isEmpty == false + else { + /// When contact name is empty display + /// ex. 1234...7890 + return call.sessionId.truncated(prefix: 4, suffix: 4) + } + + /// Display contact name + truncated session id prefix 4 + /// ex. John 1234... + let truncatedSessionId = sessionCall.sessionId.truncated(prefix: 4, suffix: 0) + return "\(sessionCall.contactName) \(truncatedSessionId)" + } } From b346e8bfa67238c86f2d66481415ace990e55f7b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 13 Aug 2025 13:20:40 +1000 Subject: [PATCH 064/244] Fixed an issue where Desktop can't download iOS attachments Try to extract (and set) the file id for attachments and fallback to 0 if it fails to convert to a UInt64 (in the future these will be string values, but current/old versions of Desktop require this to be set so we need to try to set it where possible for maximum compatibility, but not fail if we can't get it for future compatibility) --- SessionMessagingKit/Database/Models/Attachment.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 3f1f5854d9..728daf375f 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -484,7 +484,13 @@ extension Attachment { } public func buildProto() -> SNProtoAttachmentPointer? { - let builder = SNProtoAttachmentPointer.builder(id: 0) /// `id` is deprecated, rely on `url` instead + /// The `id` value on the protobuf is deprecated, rely on `url` instead + /// + /// **Note:** We need to continue to send this because it seems that the Desktop client _does_ in fact still use this + /// id for downloading attachments. Desktop will be updated to remove it's use but in order to fix attachments for old + /// versions we set this value again + let legacyId: UInt64 = (Attachment.fileId(for: self.downloadUrl).map { UInt64($0) } ?? 0) + let builder = SNProtoAttachmentPointer.builder(id: legacyId) builder.setContentType(contentType) if let sourceFilename: String = sourceFilename, !sourceFilename.isEmpty { From 03f870e1eda846cd18c9b7d3b3877fd909969193 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 13 Aug 2025 13:40:54 +1000 Subject: [PATCH 065/244] clean up and refactor on profile picture view subscribing current user's pro state --- .../Views & Modals/IncomingCallBanner.swift | 3 +- Session/Conversations/ConversationVC.swift | 3 +- .../Input View/MentionSelectionView.swift | 4 +- .../Message Cells/VisibleMessageCell.swift | 4 +- Session/Home/HomeVC.swift | 3 +- .../MessageInfoScreen.swift | 3 +- Session/Shared/FullConversationCell.swift | 6 +- .../Views/SessionCell+AccessoryView.swift | 3 +- .../Config Handling/LibSession+Contacts.swift | 12 +- .../Config Handling/LibSession+Shared.swift | 4 +- .../Utilities/Profile+CurrentUser.swift | 5 +- .../ProfilePictureView+Convenience.swift | 18 +-- .../SimplifiedConversationCell.swift | 4 +- .../Modals & Toast/ConfirmationModal.swift | 5 +- .../Components/ProfilePictureView.swift | 110 +++++------------- .../Components/SwiftUI/ProCTAModal.swift | 1 + 16 files changed, 63 insertions(+), 125 deletions(-) diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 29374fd112..e40a97724d 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -25,8 +25,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, - dataManager: dependencies[singleton: .imageDataManager], - currentUserSessionProState: dependencies[singleton: .sessionProState] + dataManager: dependencies[singleton: .imageDataManager] ) private lazy var displayNameLabel: UILabel = { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index ee7ae68525..e3a7228511 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1419,8 +1419,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa let profilePictureView = ProfilePictureView( size: .navigation, - dataManager: viewModel.dependencies[singleton: .imageDataManager], - currentUserSessionProState: viewModel.dependencies[singleton: .sessionProState] + dataManager: viewModel.dependencies[singleton: .imageDataManager] ) profilePictureView.update( publicKey: threadData.threadId, // Contact thread uses the contactId diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 37425e562b..2f1703d3a8 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -124,8 +124,7 @@ private extension MentionSelectionView { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, - dataManager: nil, - currentUserSessionProState: nil + dataManager: nil ) private lazy var displayNameLabel: UILabel = { @@ -203,7 +202,6 @@ private extension MentionSelectionView { currentUserSessionIds: currentUserSessionIds ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: profile.id, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index a89c7a9343..58dbf649b5 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -67,8 +67,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, - dataManager: nil, - currentUserSessionProState: nil + dataManager: nil ) lazy var bubbleBackgroundView: UIView = { @@ -324,7 +323,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView.isHidden = !cellViewModel.canHaveProfile profilePictureView.alpha = (profileShouldBeVisible ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.authorId, threadVariant: .contact, // Always show the display picture in 'contact' mode diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index e208388918..a313630232 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -460,8 +460,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Profile picture view let profilePictureView = ProfilePictureView( size: .navigation, - dataManager: viewModel.dependencies[singleton: .imageDataManager], - currentUserSessionProState: viewModel.dependencies[singleton: .sessionProState] + dataManager: viewModel.dependencies[singleton: .imageDataManager] ) profilePictureView.accessibilityIdentifier = "User settings" profilePictureView.accessibilityLabel = "User settings" diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 7f43f4bd74..e614b81b5f 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -303,8 +303,7 @@ struct MessageInfoScreen: View { size: size, info: info, additionalInfo: additionalInfo, - dataManager: dependencies[singleton: .imageDataManager], - sessionProState: dependencies[singleton: .sessionProState] + dataManager: dependencies[singleton: .imageDataManager] ) .frame( width: size.viewSize, diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index 915657ac26..dc3aecf9d1 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -23,8 +23,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .list, - dataManager: nil, - currentUserSessionProState: nil + dataManager: nil ) private lazy var displayNameLabel: UILabel = { @@ -281,7 +280,6 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // MARK: --Search Results public func updateForDefaultContacts(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -358,7 +356,6 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC using dependencies: Dependencies ) { profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, @@ -434,7 +431,6 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC ) ) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 27e9dbf776..2cae2e3c31 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -636,7 +636,7 @@ extension SessionCell { // MARK: -- DisplayPicture private func createDisplayPictureView() -> ProfilePictureView { - return ProfilePictureView(size: .list, dataManager: nil, currentUserSessionProState: nil) + return ProfilePictureView(size: .list, dataManager: nil) } private func layoutDisplayPictureView(_ view: UIView?, size: ProfilePictureView.Size) { @@ -661,7 +661,6 @@ extension SessionCell { profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) profilePictureView.size = accessory.size profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: accessory.id, threadVariant: accessory.threadVariant, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 187f58a473..bda8611474 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -63,11 +63,9 @@ internal extension LibSessionCacheType { // observation system can't differ between update calls which do and don't change anything) let contact: Contact = Contact.fetchOrCreate(db, id: sessionId, using: dependencies) let profile: Profile = Profile.fetchOrCreate(db, id: sessionId) + let profileUpdated: Bool = ((profile.profileLastUpdated ?? 0) < (data.profile.profileLastUpdated ?? 0)) - if - (profile.profileLastUpdated ?? 0) < (data.profile.profileLastUpdated ?? 0) || - profile.nickname != data.profile.nickname - { + if (profileUpdated || (profile.nickname != data.profile.nickname)) { let profileNameShouldBeUpdated: Bool = ( !data.profile.name.isEmpty && profile.name != data.profile.name @@ -95,7 +93,7 @@ internal extension LibSessionCacheType { (profile.displayPictureEncryptionKey != data.profile.displayPictureEncryptionKey ? nil : Profile.Columns.displayPictureEncryptionKey.set(to: data.profile.displayPictureEncryptionKey) ), - (!profilePictureShouldBeUpdated ? nil : + (!profileUpdated ? nil : Profile.Columns.profileLastUpdated.set(to: data.profile.profileLastUpdated) ) ].compactMap { $0 }, @@ -337,7 +335,9 @@ public extension LibSession { contact.set(\.nickname, to: info.nickname) contact.set(\.profile_pic.url, to: info.displayPictureUrl) contact.set(\.profile_pic.key, to: info.displayPictureEncryptionKey) - contact.set(\.profile_updated, to: (info.profileLastUpdated ?? Int64(dependencies.dateNow.timeIntervalSince1970))) + if let profileLastUpdated = info.profileLastUpdated { + contact.set(\.profile_updated, to: profileLastUpdated) + } // Attempts retrieval of the profile picture (will schedule a download if // needed via a throttled subscription on another thread to prevent blocking) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 318c9b5522..9a6a75bbbc 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -814,7 +814,7 @@ public extension LibSession.Cache { nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : member.get(\.profile_pic.key)), - profileLastUpdated: nil + profileLastUpdated: TimeInterval(member.get(\.profile_updated)) ) } @@ -839,7 +839,7 @@ public extension LibSession.Cache { nickname: contact.get(\.nickname, nullIfEmpty: true), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - profileLastUpdated: nil + profileLastUpdated: TimeInterval(contact.get( \.profile_updated)) ) } diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 851c37bf37..92ab6b1b80 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -97,6 +97,7 @@ public extension Profile { .prepareAndUploadDisplayPicture(imageData: data) .mapError { $0 as Error } .flatMapStorageWritePublisher(using: dependencies, updates: { db, result in + let profileUpdateTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, @@ -107,7 +108,7 @@ public extension Profile { filePath: result.filePath, sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), - profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, + profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), using: dependencies ) @@ -205,6 +206,8 @@ public extension Profile { } } + // TODO: Handle Pro Proof update + /// Don't want profiles in messages to modify the current users profile info so ignore those cases default: break } diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index e73f328c74..6a0ddff983 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -194,13 +194,16 @@ public extension ProfilePictureView { public extension ProfilePictureView { static func animationBehaviour(from profile: Profile?, using dependencies: Dependencies) -> Info.AnimationBehaviour { guard dependencies[feature: .sessionProEnabled] else { return .generic(true) } - guard let profile: Profile = profile else { return .generic(false) } - - guard profile.id == dependencies[cache: .general].sessionId.hexString else { - return .contact(dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) })) + + switch profile { + case .none: return .generic(false) + + case .some(let profile) where profile.id == dependencies[cache: .general].sessionId.hexString: + return .currentUser(dependencies[singleton: .sessionProState]) + + case .some(let profile): + return .contact(dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) })) } - - return .currentUser(dependencies[cache: .libSession].isSessionPro) } } @@ -235,8 +238,7 @@ public extension ProfilePictureSwiftUI { size: size, info: info, additionalInfo: additionalInfo, - dataManager: dependencies[singleton: .imageDataManager], - sessionProState: dependencies[singleton: .sessionProState] + dataManager: dependencies[singleton: .imageDataManager] ) } } diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 8e2c657fc1..d4ced9d635 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -42,8 +42,7 @@ final class SimplifiedConversationCell: UITableViewCell { private lazy var profilePictureView: ProfilePictureView = { let view: ProfilePictureView = ProfilePictureView( size: .list, - dataManager: nil, - currentUserSessionProState: nil + dataManager: nil ) view.translatesAutoresizingMaskIntoConstraints = false @@ -92,7 +91,6 @@ final class SimplifiedConversationCell: UITableViewCell { public func update(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { accentLineView.alpha = (cellViewModel.threadIsBlocked == true ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) - profilePictureView.setCurrentUserSessionProState(dependencies[singleton: .sessionProState]) profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 05664600cf..8a53863bb1 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -215,8 +215,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { private lazy var profileView: ProfilePictureView = ProfilePictureView( size: .modal, - dataManager: nil, - currentUserSessionProState: nil + dataManager: nil ) private lazy var textToConfirmContainer: UIView = { @@ -550,7 +549,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.update( ProfilePictureView.Info( source: (source ?? placeholder), - animationBehaviour: .currentUser(true), + animationBehaviour: .generic(true), // Force the animate the avatar in modals icon: icon ) ) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index 29d33402b9..88d7afe668 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -9,14 +9,7 @@ public final class ProfilePictureView: UIView { public enum AnimationBehaviour { case generic(Bool) // For communities and when Pro is not enabled case contact(Bool) - case currentUser(Bool) - - public var enableAnimation: Bool { - switch self { - case .generic(let enableAnimation), .contact(let enableAnimation), .currentUser(let enableAnimation): - return enableAnimation - } - } + case currentUser(SessionProManagerType) } let source: ImageDataManager.DataSource? @@ -120,8 +113,7 @@ public final class ProfilePictureView: UIView { } private var dataManager: ImageDataManagerType? - private var currentUserSessionProState: SessionProManagerType? - public var disposables: Set = Set() + private var disposables: Set = Set() public var size: Size { didSet { widthConstraint.constant = (customWidth ?? size.viewSize) @@ -161,14 +153,6 @@ public final class ProfilePictureView: UIView { } } - public enum CurrentUserProfileImage: Equatable { - case none - case main - case additional - } - - public var currentUserProfileImage: CurrentUserProfileImage = .none - // MARK: - Constraints private var widthConstraint: NSLayoutConstraint! @@ -306,7 +290,7 @@ public final class ProfilePictureView: UIView { // MARK: - Lifecycle - public init(size: Size, dataManager: ImageDataManagerType?, currentUserSessionProState: SessionProManagerType?) { + public init(size: Size, dataManager: ImageDataManagerType?) { self.dataManager = dataManager self.size = size @@ -314,10 +298,6 @@ public final class ProfilePictureView: UIView { clipsToBounds = true setUpViewHierarchy() - - if let currentUserSessionProState: SessionProManagerType = currentUserSessionProState { - setCurrentUserSessionProState(currentUserSessionProState) - } } public required init?(coder: NSCoder) { @@ -413,26 +393,6 @@ public final class ProfilePictureView: UIView { self.additionalImageView.setDataManager(dataManager) } - public func setCurrentUserSessionProState(_ currentUserSessionProState: SessionProManagerType) { - self.currentUserSessionProState = currentUserSessionProState - - // TODO: Refactor this to use async/await instead of Combine - currentUserSessionProState.isSessionProPublisher - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink( - receiveValue: { [weak self] isPro in - if isPro { - self?.startAnimatingForCurrentUserIfNeeded() - } else { - self?.stopAnimatingForCurrentUserIfNeeded() - } - - } - ) - .store(in: &disposables) - } - // MARK: - Content private func updateIconView( @@ -497,16 +457,17 @@ public final class ProfilePictureView: UIView { // MARK: - Content private func prepareForReuse() { - currentUserProfileImage = .none + /// Reset the disposables in case this was called with different data/ + disposables = Set() imageView.image = nil - imageView.shouldAnimateImage = true + imageView.shouldAnimateImage = false imageView.contentMode = .scaleAspectFill imageContainerView.clipsToBounds = clipsToBounds imageContainerView.themeBackgroundColor = .backgroundSecondary additionalImageContainerView.isHidden = true additionalImageView.image = nil - additionalImageView.shouldAnimateImage = true + additionalImageView.shouldAnimateImage = false additionalImageContainerView.clipsToBounds = clipsToBounds imageViewTopConstraint.isActive = false @@ -551,16 +512,11 @@ public final class ProfilePictureView: UIView { imageView.image = source.directImage?.withRenderingMode(renderingMode) case (.some(let source), _): - imageView.shouldAnimateImage = info.animationBehaviour.enableAnimation imageView.loadImage(source) default: imageView.image = nil } - if case .currentUser(_) = info.animationBehaviour { - self.currentUserProfileImage = .main - } - imageView.themeTintColor = info.themeTintColor imageContainerView.themeBackgroundColor = info.backgroundColor imageContainerView.themeBackgroundColorForced = info.forcedBackgroundColor @@ -575,6 +531,8 @@ public final class ProfilePictureView: UIView { } } + startAnimationIfNeeded(for: info, with: imageView) + // Check if there is a second image (if not then set the size and finish) guard let additionalInfo: Info = additionalInfo else { imageViewWidthConstraint.constant = size.imageSize @@ -602,7 +560,6 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = false case (.some(let source), _): - additionalImageView.shouldAnimateImage = additionalInfo.animationBehaviour.enableAnimation additionalImageView.loadImage(source) additionalImageContainerView.isHidden = false @@ -611,10 +568,6 @@ public final class ProfilePictureView: UIView { additionalImageContainerView.isHidden = true } - if case .currentUser(_) = additionalInfo.animationBehaviour { - self.currentUserProfileImage = .additional - } - additionalImageView.themeTintColor = additionalInfo.themeTintColor switch (info.backgroundColor, info.forcedBackgroundColor) { @@ -633,6 +586,8 @@ public final class ProfilePictureView: UIView { } } + startAnimationIfNeeded(for: additionalInfo, with: additionalImageView) + imageViewTopConstraint.isActive = true imageViewLeadingConstraint.isActive = true imageViewCenterXConstraint.isActive = false @@ -650,25 +605,22 @@ public final class ProfilePictureView: UIView { additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2) } - public func startAnimatingForCurrentUserIfNeeded() { - switch currentUserProfileImage { - case .none: - break - case .main: - imageView.shouldAnimateImage = true - case .additional: - additionalImageView.shouldAnimateImage = true - } - } - - public func stopAnimatingForCurrentUserIfNeeded() { - switch currentUserProfileImage { - case .none: - break - case .main: - imageView.shouldAnimateImage = false - case .additional: - additionalImageView.shouldAnimateImage = false + private func startAnimationIfNeeded(for info: Info, with targetImageView: SessionImageView) { + switch info.animationBehaviour { + case .generic(let enableAnimation), .contact(let enableAnimation): + targetImageView.shouldAnimateImage = enableAnimation + + case .currentUser(let currentUserSessionProState): + targetImageView.shouldAnimateImage = currentUserSessionProState.isSessionProSubject.value + currentUserSessionProState.isSessionProPublisher + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink( + receiveValue: { [weak targetImageView] isPro in + targetImageView?.shouldAnimateImage = isPro + } + ) + .store(in: &disposables) } } } @@ -682,27 +634,23 @@ public struct ProfilePictureSwiftUI: UIViewRepresentable { var info: ProfilePictureView.Info var additionalInfo: ProfilePictureView.Info? let dataManager: ImageDataManagerType - let sessionProState: SessionProManagerType public init( size: ProfilePictureView.Size, info: ProfilePictureView.Info, additionalInfo: ProfilePictureView.Info? = nil, - dataManager: ImageDataManagerType, - sessionProState: SessionProManagerType + dataManager: ImageDataManagerType ) { self.size = size self.info = info self.additionalInfo = additionalInfo self.dataManager = dataManager - self.sessionProState = sessionProState } public func makeUIView(context: Context) -> ProfilePictureView { ProfilePictureView( size: size, - dataManager: dataManager, - currentUserSessionProState: sessionProState + dataManager: dataManager ) } diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index b547d200da..f4196cd942 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -366,6 +366,7 @@ public struct ProCTAModal: View { // MARK: - SessionProManagerType public protocol SessionProManagerType: AnyObject { + var isSessionProSubject: CurrentValueSubject { get } var isSessionProPublisher: AnyPublisher { get } func upgradeToPro(completion: ((_ result: Bool) -> Void)?) } From 464fbf490d981e7bac104bf18b78094b2ba55a5e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 13 Aug 2025 14:50:55 +1000 Subject: [PATCH 066/244] Fixed an issue where we were storing the 'softfork' version in the 'hardfork' value --- SessionSnodeKit/SnodeAPI/SnodeAPI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI/SnodeAPI.swift index af511bf056..71bbe5bd41 100644 --- a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI/SnodeAPI.swift @@ -685,7 +685,7 @@ public final class SnodeAPI { if snodeResponse.hardForkVersion[1] > $0.softfork { $0.softfork = snodeResponse.hardForkVersion[1] - dependencies[defaults: .standard, key: .hardfork] = $0.softfork + dependencies[defaults: .standard, key: .softfork] = $0.softfork } if snodeResponse.hardForkVersion[0] > $0.hardfork { From 083c7b85229a372de2389525625b84f11631fd49 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 13 Aug 2025 17:20:53 +1000 Subject: [PATCH 067/244] fix unit test --- SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 556a2adee0..fb7b9760f5 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -199,6 +199,7 @@ class OpenGroupManagerSpec: QuickSpec { @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.bool(forKey: .any) }.thenReturn(false) + defaults.when { $0.object(forKey: .any) }.thenReturn(nil) } ) @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( From ec9aebfb06322192d9bc8d004f6e18681f587fd1 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 13 Aug 2025 15:46:41 +0800 Subject: [PATCH 068/244] Added app store review prompt dialog Prepared dialog to show appstore review dialog --- Session.xcodeproj/project.pbxproj | 24 +++ .../App Review/AppReviewPromptModel.swift | 48 +++++ .../View/AppReviewPromptDialog.swift | 164 ++++++++++++++++++ Session/Home/HomeVC.swift | 56 ++++++ 4 files changed, 292 insertions(+) create mode 100644 Session/Home/App Review/AppReviewPromptModel.swift create mode 100644 Session/Home/App Review/View/AppReviewPromptDialog.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5ed0e0cdfc..5f7dc947f0 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1157,6 +1157,8 @@ FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; + FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; }; + FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2428,6 +2430,8 @@ FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; + FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = ""; }; + FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3429,6 +3433,7 @@ C360968E25AD16E8008B62B2 /* Home */ = { isa = PBXGroup; children = ( + FED288EF2E4C239800C31171 /* App Review */, 7B8C44C328B49DA900FBE25F /* New Conversation */, 7B93D06827CF173D00811CB6 /* Message Requests */, 7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */, @@ -5124,6 +5129,23 @@ path = Transitions; sourceTree = ""; }; + FED288EF2E4C239800C31171 /* App Review */ = { + isa = PBXGroup; + children = ( + FED288F42E4C3B5A00C31171 /* View */, + FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */, + ); + path = "App Review"; + sourceTree = ""; + }; + FED288F42E4C3B5A00C31171 /* View */ = { + isa = PBXGroup; + children = ( + FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -6718,6 +6740,7 @@ FDB3DA842E1CA22400148F8D /* UIActivityViewController+Utilities.swift in Sources */, FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */, FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */, + FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */, 7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */, C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */, 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */, @@ -6913,6 +6936,7 @@ B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */, 9409433E2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift in Sources */, + FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */, FDE754B12C9B96B4002A2623 /* TurnServerInfo.swift in Sources */, 7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */, B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */, diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift new file mode 100644 index 0000000000..3cada02ace --- /dev/null +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -0,0 +1,48 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct AppReviewPromptModel { + let title: String + let message: String + + let primaryButtonTitle: String + let secondaryButtonTitle: String +} + +enum AppReviewPromptState { + case enjoyingSession + case rateSession + case feedback + case none +} + +extension AppReviewPromptState { + var promptContent: AppReviewPromptModel { + switch self { + case .enjoyingSession: + return .init( + title: NSLocalizedString("Enjoying Session?", comment: "Title for the app review prompt dialog"), + message: "You've been using Session for a little while, how’s it going? We’d really appreciate hearing your thoughts.", + primaryButtonTitle: "It's Great ❤️", + secondaryButtonTitle: "Needs Work 😕" + ) + case .rateSession: + return .init( + title: "Rate Session?", + message: "We're glad you're enjoying Session, if you have a moment, rating us in the App Store helps others discover private, secure messaging!", + primaryButtonTitle: "Rate App", + secondaryButtonTitle: "Not now" + ) + case .feedback: + return .init( + title: "Give Feedback?", + message: "Sorry to hear your Session experience hasn’t been ideal. We'd be grateful if you could take a moment to share your thoughts in a brief survey", + primaryButtonTitle: "Open Survey", + secondaryButtonTitle: "Not now" + ) + case .none: + return .init(title: "", message: "", primaryButtonTitle: "", secondaryButtonTitle: "") + } + } +} diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift new file mode 100644 index 0000000000..9795698d4b --- /dev/null +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -0,0 +1,164 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit + +protocol AppReviewPromptDialogDelegate: AnyObject { + func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) + func didChangePromptState(_ state: AppReviewPromptState) +} + +class AppReviewPromptDialog: UIView { + weak var delegate: AppReviewPromptDialogDelegate? + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage( + UIImage(named: "X")? + .withRenderingMode(.alwaysTemplate), + for: .normal + ) + button.themeTintColor = .textPrimary + button.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside) + button.set(.width, to: Values.largeSpacing) + button.set(.height, to: Values.largeSpacing) + + return button + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + label.themeTextColor = .textPrimary + label.font = .systemFont(ofSize: Values.mediumFontSize, weight: .medium) + return label + }() + + private lazy var messageLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + label.themeTextColor = .textSecondary + label.font = .systemFont(ofSize: Values.smallFontSize, weight: .regular) + return label + }() + + private lazy var primaryButton: UIButton = { + let button = UIButton(type: .custom) + button.setThemeTitleColor(.sessionButton_text, for: .normal) + button.setThemeTitleColor(.sessionButton_highlight, for: .highlighted) + + button.titleLabel?.numberOfLines = 3 + button.titleLabel?.textAlignment = .center + + button.addTarget(self, action: #selector(primaryEvent), for: .touchUpInside) + + return button + }() + + private lazy var secondaryButton: UIButton = { + let button = UIButton(type: .custom) + button.setThemeTitleColor(.textPrimary, for: .normal) + button.setThemeTitleColor(.textSecondary, for: .highlighted) + + button.titleLabel?.numberOfLines = 3 + button.titleLabel?.textAlignment = .center + + button.addTarget(self, action: #selector(secondaryEvent), for: .touchUpInside) + + return button + }() + + private lazy var buttonStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [ + primaryButton, + secondaryButton + ]) + stack.axis = .horizontal + stack.distribution = .fillEqually + stack.alignment = .fill + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = .init(top: 16, left: 0, bottom: 16, right: 0) + + return stack + }() + + private lazy var contentStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [ + titleLabel, + messageLabel, + buttonStack + ]) + stack.axis = .vertical + stack.distribution = .fill + stack.spacing = 6 + return stack + }() + + private var prompt: AppReviewPromptState = .none + + override init(frame: CGRect) { + super.init(frame: frame) + + setupHierarchy() + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePrompt(_ prompt: AppReviewPromptState) { + self.prompt = prompt + + isHidden = prompt == .none + + titleLabel.text = prompt.promptContent.title + messageLabel.text = prompt.promptContent.message + + primaryButton.setTitle(prompt.promptContent.primaryButtonTitle, for: .normal) + secondaryButton.setTitle(prompt.promptContent.secondaryButtonTitle, for: .normal) + + delegate?.didChangePromptState(prompt) + } + + @objc + func close() { + updatePrompt(.none) + } + + @objc + func primaryEvent() { + switch prompt { + case .enjoyingSession: + updatePrompt(.rateSession) + default: delegate?.willHandlePromptState(prompt, isPrimary: true) + } + } + + @objc + func secondaryEvent() { + switch prompt { + case .enjoyingSession: updatePrompt(.feedback) + default: delegate?.willHandlePromptState(prompt, isPrimary: false) + } + } +} + +private extension AppReviewPromptDialog { + func setupHierarchy() { + addSubview(closeButton) + addSubview(contentStack) + } + + func setupLayout() { + closeButton.pin(.top, to: .top, of: self, withInset: Values.smallSpacing) + closeButton.pin(.right, to: .right, of: self, withInset: -Values.smallSpacing) + + contentStack.pin(.top, to: .bottom, of: closeButton) + contentStack.pin(.left, to: .left, of: self, withInset: Values.mediumSmallSpacing) + contentStack.pin(.right, to: .right, of: self, withInset: -Values.mediumSmallSpacing) + contentStack.pin(.bottom, to: .bottom, of: self, withInset: -Values.mediumSmallSpacing) + } +} diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index a313630232..7ffb59ecd0 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -93,6 +93,20 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi return result }() + private lazy var appReviewPrompt: AppReviewPromptDialog = { + let prompt = AppReviewPromptDialog() + prompt.delegate = self + prompt.translatesAutoresizingMaskIntoConstraints = false + + // Layers + prompt.themeBorderColor = .borderSeparator + prompt.layer.borderWidth = 1 + prompt.layer.cornerRadius = 12 + prompt.themeBackgroundColor = .backgroundSecondary + + return prompt + }() + private lazy var newConversationButton: UIView = { let result: UIView = UIView() result.set(.width, to: HomeVC.newConversationButtonSize) @@ -334,6 +348,15 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi newConversationButton.center(.horizontal, in: view) newConversationButton.pin(.bottom, to: .bottom, of: view.safeAreaLayoutGuide, withInset: -Values.smallSpacing) + appReviewPrompt.updatePrompt(.none) + + // Preview prompt + view.addSubview(appReviewPrompt) + + appReviewPrompt.pin(.left, to: .left, of: view, withInset: 12) + appReviewPrompt.pin(.right, to: .right, of: view, withInset: -12) + appReviewPrompt.pin(.bottom, to: .top, of: newConversationButton, withInset: -10) + // Start polling if needed (i.e. if the user just created or restored their Session ID) if viewModel.dependencies[cache: .general].userExists, @@ -790,3 +813,36 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi present(navigationController, animated: true, completion: nil) } } + +extension HomeVC: AppReviewPromptDialogDelegate { + func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) { + if !isPrimary { + appReviewPrompt.updatePrompt(.none) + return + } + + switch state { + case .feedback: + print("LAUNCH SUMMARY") + case .rateSession: + print("SHOW APP RATING") + default: + break + } + } + + func didChangePromptState(_ state: AppReviewPromptState) { + let originalBottomInsets = ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + ) + + if state == .none { + tableView.contentInset.bottom = originalBottomInsets + } else { + tableView.contentInset.bottom = originalBottomInsets + (appReviewPrompt.frame.size.height + 24) + } + } +} From 94595d93f3a044d880e7b4f17140fd84296617ac Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 14 Aug 2025 10:10:33 +0800 Subject: [PATCH 069/244] Updated localized strings --- .../App Review/AppReviewPromptModel.swift | 40 +++++++++++++------ .../View/AppReviewPromptDialog.swift | 7 +++- SessionUIKit/Style Guide/Constants.swift | 1 + 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 3cada02ace..be33ee2dcc 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -1,6 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionUIKit struct AppReviewPromptModel { let title: String @@ -22,24 +23,39 @@ extension AppReviewPromptState { switch self { case .enjoyingSession: return .init( - title: NSLocalizedString("Enjoying Session?", comment: "Title for the app review prompt dialog"), - message: "You've been using Session for a little while, how’s it going? We’d really appreciate hearing your thoughts.", - primaryButtonTitle: "It's Great ❤️", - secondaryButtonTitle: "Needs Work 😕" + title: "enjoyingSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "enjoyingSessionDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "enjoyingSessionButtonPositive" + .put(key: "emoji", value: "❤️") + .localized(), + secondaryButtonTitle: "enjoyingSessionButtonNegative" + .put(key: "emoji", value: "😕") + .localized() ) case .rateSession: return .init( - title: "Rate Session?", - message: "We're glad you're enjoying Session, if you have a moment, rating us in the App Store helps others discover private, secure messaging!", - primaryButtonTitle: "Rate App", - secondaryButtonTitle: "Not now" + title: "rateSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "rateSessionModalDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "storevariant", value: Constants.store_variant) + .localized(), + primaryButtonTitle: "rateSessionApp".localized(), + secondaryButtonTitle: "notNow".localized() ) case .feedback: return .init( - title: "Give Feedback?", - message: "Sorry to hear your Session experience hasn’t been ideal. We'd be grateful if you could take a moment to share your thoughts in a brief survey", - primaryButtonTitle: "Open Survey", - secondaryButtonTitle: "Not now" + title: "giveFeedback".localized(), + message: "giveFeedbackDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "openSurvey".localized(), + secondaryButtonTitle: "notNow".localized() ) case .none: return .init(title: "", message: "", primaryButtonTitle: "", secondaryButtonTitle: "") diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 9795698d4b..08ff020888 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -79,7 +79,12 @@ class AppReviewPromptDialog: UIView { stack.distribution = .fillEqually stack.alignment = .fill stack.isLayoutMarginsRelativeArrangement = true - stack.layoutMargins = .init(top: 16, left: 0, bottom: 16, right: 0) + stack.layoutMargins = .init( + top: Values.mediumSpacing, + left: 0, + bottom: Values.mediumSpacing, + right: 0 + ) return stack }() diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index 7ff7fa8564..ba9fe47fd6 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -15,4 +15,5 @@ public enum Constants { public static let usd_name_short: String = "USD" public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" + public static let store_variant: String = "App Store" } From 04cd5a5516f7fc2bf0fda108f1f7e390bfa6d28a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Aug 2025 12:53:46 +1000 Subject: [PATCH 070/244] Fixed and cleaned up some attachment handling issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added the ability to save audio and generic attachments • Cleaned up a bunch of duplicate and custom image processing • Removed a few cases where we would load image data when we only wanted metadata • Removed SignalAttachment thumbnail loading logic (now goes via ImageDataManager) • Removed the "GIF" badge from animated thumbnails (not needed anymore now that they animate) • Fixed an issue where activity indicators weren't getting themed correctly • Fixed an issue where WebP images may not load due to large resolutions --- Session.xcodeproj/project.pbxproj | 8 +- .../Context Menu/ContextMenuVC+Action.swift | 35 +- .../Conversations/ConversationSearch.swift | 2 +- .../ConversationVC+Interaction.swift | 271 ++++++++----- Session/Conversations/ConversationVC.swift | 3 +- .../Content Views/DocumentView.swift | 2 +- .../Content Views/MediaView.swift | 4 +- .../Content Views/QuoteView.swift | 4 +- Session/Home/HomeVC.swift | 2 +- .../GIFs/GifPickerCell.swift | 2 +- .../MediaTileViewController.swift | 13 +- .../PhotoGridViewCell.swift | 30 +- .../PhotoLibrary.swift | 13 +- Session/Meta/AppDelegate.swift | 1 + .../Contents.json | 23 -- .../icon_GIF@1x.png | Bin 380 -> 0 bytes .../icon_GIF@2x.png | Bin 759 -> 0 bytes .../icon_GIF@3x.png | Bin 1083 -> 0 bytes Session/Shared/Views/SessionHeaderView.swift | 2 +- .../Utilities/ImageLoading+Convenience.swift | 12 +- .../_022_GroupsRebuildChanges.swift | 5 +- .../Database/Models/Attachment.swift | 4 +- .../Database/Models/LinkPreview.swift | 12 +- .../Attachments/SignalAttachment.swift | 82 +--- .../Utilities/AttachmentManager.swift | 2 +- .../Utilities/DisplayPictureManager.swift | 2 +- .../NotificationServiceExtension.swift | 1 + .../ShareNavController.swift | 1 + .../Components/SessionImageView.swift | 11 +- .../Style Guide/Themes/UIKit+Theme.swift | 16 +- SessionUIKit/Types/ImageDataManager.swift | 10 +- SessionUtilitiesKit/Configuration.swift | 5 +- SessionUtilitiesKit/Media/Data+Image.swift | 369 ------------------ SessionUtilitiesKit/Media/DataSource.swift | 102 +++-- SessionUtilitiesKit/Media/MediaUtils.swift | 244 +++++++++++- .../Types/DocumentPickerHandler.swift | 28 ++ .../AttachmentApprovalViewController.swift | 36 +- .../AttachmentItemCollection.swift | 4 - .../AttachmentPrepViewController.swift | 4 +- .../Image Editing/ImageEditorCanvasView.swift | 18 +- .../Image Editing/ImageEditorModel.swift | 20 +- .../Image Editing/ImageEditorView.swift | 7 +- .../MediaMessageView.swift | 75 ++-- 43 files changed, 670 insertions(+), 815 deletions(-) delete mode 100644 Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json delete mode 100644 Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png delete mode 100644 Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@2x.png delete mode 100644 Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png delete mode 100644 SessionUtilitiesKit/Media/Data+Image.swift create mode 100644 SessionUtilitiesKit/Types/DocumentPickerHandler.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5ed0e0cdfc..abcb82d11d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -659,6 +659,7 @@ FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; @@ -818,7 +819,6 @@ FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; }; FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB11A622DD5BDDD00BEF49F /* ImageDataManager+Singleton.swift */; }; - FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754CB2C9BAF37002A2623 /* Data+Image.swift */; }; FD78E9F22DDA9EA200D55B50 /* MockImageDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */; }; FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */; }; FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F52DDD43AB00D55B50 /* Mutation.swift */; }; @@ -1983,6 +1983,7 @@ FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -2315,7 +2316,6 @@ FDE754C82C9BAF36002A2623 /* UTType+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UTType+Utilities.swift"; sourceTree = ""; }; FDE754C92C9BAF36002A2623 /* ImageFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; FDE754CA2C9BAF37002A2623 /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; - FDE754CB2C9BAF37002A2623 /* Data+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = ""; }; FDE754D12C9BAF53002A2623 /* JobDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = ""; }; FDE754D32C9BAF6B002A2623 /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = ""; }; FDE754D52C9BAF89002A2623 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; @@ -3009,7 +3009,6 @@ B8A582AF258C665E00AFD84C /* Media */ = { isa = PBXGroup; children = ( - FDE754CB2C9BAF37002A2623 /* Data+Image.swift */, FDE754CA2C9BAF37002A2623 /* DataSource.swift */, FDE754C92C9BAF36002A2623 /* ImageFormat.swift */, FDE754C72C9BAF36002A2623 /* MediaUtils.swift */, @@ -4174,6 +4173,7 @@ FD2272D22C34ECBB004D8A6C /* Types */ = { isa = PBXGroup; children = ( + FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */, FD0E353A2AB98773006A81F7 /* AppVersion.swift */, FDB3486D2BE8457F00B716C2 /* BackgroundTaskManager.swift */, FDE755042C9BB4ED002A2623 /* Bencode.swift */, @@ -6348,7 +6348,6 @@ FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */, FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, - FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, @@ -6394,6 +6393,7 @@ FD7728962849E7E90018502F /* String+Utilities.swift in Sources */, C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */, FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */, + FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, FD29598D2A43BC0B00888A17 /* Version.swift in Sources */, FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 15d0c0a8b9..53b44ea9b5 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -225,17 +225,30 @@ extension ContextMenuVC { ) ) ) - let canSave: Bool = ( - cellViewModel.cellType == .mediaMessage && - (cellViewModel.attachments ?? []) - .filter { attachment in - attachment.isValid && - attachment.isVisualMedia && ( - attachment.state == .downloaded || - attachment.state == .uploaded - ) - }.isEmpty == false - ) + let canSave: Bool = { + switch cellViewModel.cellType { + case .mediaMessage: + return (cellViewModel.attachments ?? []) + .filter { attachment in + attachment.isValid && + attachment.isVisualMedia && ( + attachment.state == .downloaded || + attachment.state == .uploaded + ) + }.isEmpty == false + + case .audio, .genericAttachment: + return (cellViewModel.attachments ?? []) + .filter { attachment in + attachment.isValid && ( + attachment.state == .downloaded || + attachment.state == .uploaded + ) + }.isEmpty == false + + default: return false + } + }() let canCopySessionId: Bool = ( cellViewModel.variant == .standardIncoming && cellViewModel.threadVariant != .community diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index f8dd4c2a5b..69e80a7b90 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -170,7 +170,7 @@ public final class SearchResultsBar: UIView { private lazy var loadingIndicator: UIActivityIndicatorView = { let result = UIActivityIndicatorView(style: .medium) - result.themeTintColor = .textPrimary + result.themeColor = .textPrimary result.alpha = 0.5 result.hidesWhenStopped = true diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 62baf8fb45..ebd9760325 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -21,7 +21,6 @@ extension ConversationVC: MessageCellDelegate, ContextMenuActionDelegate, SendMediaNavDelegate, - UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, GifPickerViewControllerDelegate { @@ -362,9 +361,74 @@ extension ConversationVC: // UIDocumentPickerModeImport copies to a temp file within our container. // It uses more memory than "open" but lets us avoid working with security scoped URLs. let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) - documentPickerVC.delegate = self documentPickerVC.modalPresentationStyle = .fullScreen + self.documentHandler = DocumentPickerHandler( + didPickDocumentsAt: { [weak self, dependencies = viewModel.dependencies] _, urls in + defer { + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + } + + guard let url: URL = urls.first else { return } + + let urlResourceValues: URLResourceValues + do { + urlResourceValues = try url.resourceValues(forKeys: [ .typeIdentifierKey, .isDirectoryKey, .nameKey ]) + } + catch { + DispatchQueue.main.async { [weak self] in + self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) + } + return + } + + let type: UTType = (urlResourceValues.typeIdentifier.map({ UTType($0) }) ?? .data) + guard urlResourceValues.isDirectory != true else { + DispatchQueue.main.async { [weak self] in + let modal: ConfirmationModal = ConfirmationModal( + targetView: self?.view, + info: ConfirmationModal.Info( + title: "attachmentsErrorLoad".localized(), + body: .text("attachmentsErrorNotSupported".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ) + self?.present(modal, animated: true) + } + return + } + + let fileName: String = (urlResourceValues.name ?? "attachment".localized()) + guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: urlResourceValues.name, shouldDeleteOnDeinit: false, using: dependencies) else { + DispatchQueue.main.async { [weak self] in + self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) + } + return + } + dataSource.sourceFilename = fileName + + // Although we want to be able to send higher quality attachments through the document picker + // it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov) + guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, type: type) else { + self?.showAttachmentApprovalDialogAfterProcessingVideo(at: url, with: fileName) + return + } + + // "Document picker" attachments _SHOULD NOT_ be resized + let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original, using: dependencies) + self?.showAttachmentApprovalDialog(for: [ attachment ]) + }, + wasCancelled: { [weak self] _ in + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + } + ) + documentPickerVC.delegate = self.documentHandler + present(documentPickerVC, animated: true, completion: nil) } @@ -412,59 +476,6 @@ extension ConversationVC: showAttachmentApprovalDialog(for: [ attachment ]) } - // MARK: - UIDocumentPickerDelegate - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url = urls.first else { return } // TODO: Handle multiple? - - let urlResourceValues: URLResourceValues - do { - urlResourceValues = try url.resourceValues(forKeys: [ .typeIdentifierKey, .isDirectoryKey, .nameKey ]) - } - catch { - DispatchQueue.main.async { [weak self] in - self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) - } - return - } - - let type: UTType = (urlResourceValues.typeIdentifier.map({ UTType($0) }) ?? .data) - guard urlResourceValues.isDirectory != true else { - DispatchQueue.main.async { [weak self] in - let modal: ConfirmationModal = ConfirmationModal( - targetView: self?.view, - info: ConfirmationModal.Info( - title: "attachmentsErrorLoad".localized(), - body: .text("attachmentsErrorNotSupported".localized()), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ) - self?.present(modal, animated: true) - } - return - } - - let fileName: String = (urlResourceValues.name ?? "attachment".localized()) - guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: urlResourceValues.name, shouldDeleteOnDeinit: false, using: viewModel.dependencies) else { - DispatchQueue.main.async { [weak self] in - self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) - } - return - } - dataSource.sourceFilename = fileName - - // Although we want to be able to send higher quality attachments through the document picker - // it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov) - guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, type: type) else { - return showAttachmentApprovalDialogAfterProcessingVideo(at: url, with: fileName) - } - - // "Document picker" attachments _SHOULD NOT_ be resized - let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original, using: viewModel.dependencies) - showAttachmentApprovalDialog(for: [ attachment ]) - } - func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { guard let navController = AttachmentApprovalViewController.wrappedInNavController( threadId: self.viewModel.threadData.threadId, @@ -2375,12 +2386,12 @@ extension ConversationVC: } func save(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { - guard cellViewModel.cellType == .mediaMessage else { return } - - let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) + let validAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) .filter { attachment in - attachment.isValid && - attachment.isVisualMedia && ( + attachment.isValid && ( + cellViewModel.cellType != .mediaMessage || + attachment.isVisualMedia + ) && ( attachment.state == .downloaded || attachment.state == .uploaded ) @@ -2399,63 +2410,112 @@ extension ConversationVC: return (attachment, path) } - guard !mediaAttachments.isEmpty else { return } - - Permissions.requestLibraryPermissionIfNeeded( - isSavingMedia: true, - presentingViewController: self, - using: viewModel.dependencies - ) { [weak self, dependencies = viewModel.dependencies] in - PHPhotoLibrary.shared().performChanges( - { - mediaAttachments.forEach { attachment, path in - if attachment.isImage || attachment.isAnimated { - PHAssetChangeRequest.creationRequestForAssetFromImage( - atFileURL: URL(fileURLWithPath: path) - ) + guard !validAttachments.isEmpty else { return } + + switch cellViewModel.cellType { + case .audio, .genericAttachment: + let documentPicker = UIDocumentPickerViewController( + forExporting: validAttachments.map { _, path in URL(fileURLWithPath: path) }, + asCopy: true + ) + + self.documentHandler = DocumentPickerHandler( + didPickDocumentsAt: { [weak self, dependencies = viewModel.dependencies] _, _ in + validAttachments.forEach { attachment, path in + /// Sanity check to make sure we don't unintentionally remove a proper attachment file + guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { + return + } + + try? dependencies[singleton: .fileManager].removeItem(atPath: path) } - else if attachment.isVideo { - PHAssetChangeRequest.creationRequestForAssetFromVideo( - atFileURL: URL(fileURLWithPath: path) + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in + self?.viewModel.showToast( + text: "saved".localized(), + backgroundColor: .toast_background, + inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) ) - } - } - }, - completionHandler: { [dependencies] _, _ in - mediaAttachments.forEach { attachment, path in - /// Sanity check to make sure we don't unintentionally remove a proper attachment file - guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { - return + + // Send a 'media saved' notification if needed + guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { + return + } + + self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) } - try? dependencies[singleton: .fileManager].removeItem(atPath: path) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in - self?.viewModel.showToast( - text: "saved".localized(), - backgroundColor: .toast_background, - inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) - ) + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + }, + wasCancelled: { [weak self] _ in + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil } + ) + documentPicker.delegate = documentHandler + present(documentPicker, animated: true) + + case .mediaMessage: + Permissions.requestLibraryPermissionIfNeeded( + isSavingMedia: true, + presentingViewController: self, + using: viewModel.dependencies + ) { [weak self, dependencies = viewModel.dependencies] in + PHPhotoLibrary.shared().performChanges( + { + validAttachments.forEach { attachment, path in + if attachment.isImage || attachment.isAnimated { + PHAssetChangeRequest.creationRequestForAssetFromImage( + atFileURL: URL(fileURLWithPath: path) + ) + } + else if attachment.isVideo { + PHAssetChangeRequest.creationRequestForAssetFromVideo( + atFileURL: URL(fileURLWithPath: path) + ) + } + } + }, + completionHandler: { [weak self, dependencies] _, _ in + validAttachments.forEach { attachment, path in + /// Sanity check to make sure we don't unintentionally remove a proper attachment file + guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { + return + } + + try? dependencies[singleton: .fileManager].removeItem(atPath: path) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in + self?.viewModel.showToast( + text: "saved".localized(), + backgroundColor: .toast_background, + inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) + ) + } + + // Send a 'media saved' notification if needed + guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { + return + } + + self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) + } + ) } - ) - - // Send a 'media saved' notification if needed - guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { - return - } - - self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) + + completion?() + + default: break } - - completion?() } func ban(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { guard cellViewModel.threadVariant == .community else { return } - let threadId: String = self.viewModel.threadData.threadId let modal: ConfirmationModal = ConfirmationModal( targetView: self.view, info: ConfirmationModal.Info( @@ -2534,7 +2594,6 @@ extension ConversationVC: func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { guard cellViewModel.threadVariant == .community else { return } - let threadId: String = self.viewModel.threadData.threadId let modal: ConfirmationModal = ConfirmationModal( targetView: self.view, info: ConfirmationModal.Info( diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e3a7228511..54c0e7f9dc 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -43,6 +43,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Context menu var contextMenuWindow: ContextMenuWindow? var contextMenuVC: ContextMenuVC? + var documentHandler: DocumentPickerHandler? // Mentions var currentMentionStartIndex: String.Index? @@ -1708,7 +1709,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa switch section.model { case .loadOlder, .loadNewer: let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary + loadingIndicator.themeColor = .textPrimary loadingIndicator.alpha = 0.5 loadingIndicator.startAnimating() diff --git a/Session/Conversations/Message Cells/Content Views/DocumentView.swift b/Session/Conversations/Message Cells/Content Views/DocumentView.swift index de017bffbf..aef1f49e9b 100644 --- a/Session/Conversations/Message Cells/Content Views/DocumentView.swift +++ b/Session/Conversations/Message Cells/Content Views/DocumentView.swift @@ -74,7 +74,7 @@ final class DocumentView: UIView { rightContainerView.set(.height, to: 24) let activityIndicator = UIActivityIndicatorView(style: .medium) - activityIndicator.themeTintColor = .textPrimary + activityIndicator.themeColor = textColor activityIndicator.startAnimating() activityIndicator.hidesWhenStopped = true activityIndicator.isHidden = (attachment.state != .uploading && attachment.state != .downloading) diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 370c14ac1f..01a44703a9 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -194,8 +194,8 @@ public class MediaView: UIView { case (_, false, _), (_, _, false): return configure(forError: .invalid) case (_, true, true): - imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak self] success in - guard !success else { return } + imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak self] processedData in + guard processedData == nil else { return } Log.error("[MediaView] Could not load thumbnail") Task { @MainActor [weak self] in self?.configure(forError: .invalid) } diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index 590395a6c8..1c7d67b06c 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -131,8 +131,8 @@ final class QuoteView: UIView { } // Generate the thumbnail if needed - imageView.loadThumbnail(size: .small, attachment: attachment, using: dependencies) { [weak imageView] success in - guard success else { return } + imageView.loadThumbnail(size: .small, attachment: attachment, using: dependencies) { [weak imageView] processedData in + guard processedData != nil else { return } imageView?.contentMode = .scaleAspectFill } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index a313630232..dc3db601d7 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -549,7 +549,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi switch section.model { case .loadMore: let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary + loadingIndicator.themeColor = .textPrimary loadingIndicator.alpha = 0.5 loadingIndicator.startAnimating() diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index bf01d591f7..e3bf699035 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -201,7 +201,7 @@ class GifPickerCell: UICollectionViewCell { clearViewState() return } - guard let dependencies: Dependencies = dependencies, Data.isValidImage(at: asset.filePath, type: .gif, using: dependencies) else { + guard let dependencies: Dependencies = dependencies, MediaUtils.isValidImage(at: asset.filePath, type: .gif, using: dependencies) else { Log.error(.giphy, "Cell received invalid asset.") clearViewState() return diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index 5b0eb922d2..5518050f80 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -878,18 +878,7 @@ class GalleryGridCellItem: PhotoGridItem { self.galleryItem = galleryItem } - var type: PhotoGridItemType { - if galleryItem.isVideo { - return .video - } - - if galleryItem.isAnimated { - return .animated - } - - return .photo - } - + var isVideo: Bool { galleryItem.isVideo } var source: ImageDataManager.DataSource { ImageDataManager.DataSource.thumbnailFrom( attachment: galleryItem.attachment, diff --git a/Session/Media Viewing & Editing/PhotoGridViewCell.swift b/Session/Media Viewing & Editing/PhotoGridViewCell.swift index f32643c210..39f12ded9f 100644 --- a/Session/Media Viewing & Editing/PhotoGridViewCell.swift +++ b/Session/Media Viewing & Editing/PhotoGridViewCell.swift @@ -6,12 +6,8 @@ import UIKit import SessionUIKit import SessionUtilitiesKit -public enum PhotoGridItemType { - case photo, animated, video -} - public protocol PhotoGridItem: AnyObject { - var type: PhotoGridItemType { get } + var isVideo: Bool { get } var source: ImageDataManager.DataSource { get } } @@ -26,8 +22,6 @@ public class PhotoGridViewCell: UICollectionViewCell { var item: PhotoGridItem? - private static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video") - private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif") private static let selectedBadgeImage = UIImage(systemName: "checkmark.circle.fill") override public var isSelected: Bool { @@ -52,7 +46,7 @@ public class PhotoGridViewCell: UICollectionViewCell { let kSelectedBadgeSize = CGSize(width: 32, height: 32) self.selectedBadgeView = UIImageView() - selectedBadgeView.image = PhotoGridViewCell.selectedBadgeImage?.withRenderingMode(.alwaysTemplate) + selectedBadgeView.image = UIImage(named: "ic_gallery_badge_video")?.withRenderingMode(.alwaysTemplate) selectedBadgeView.themeTintColor = .primary selectedBadgeView.themeBorderColor = .textPrimary selectedBadgeView.themeBackgroundColor = .textPrimary @@ -105,23 +99,11 @@ public class PhotoGridViewCell: UICollectionViewCell { self.item = item imageView.setDataManager(dependencies[singleton: .imageDataManager]) imageView.themeBackgroundColor = .textSecondary - imageView.loadImage(item.source) { [weak imageView] success in - imageView?.themeBackgroundColor = (success ? .clear : .textSecondary) - } - - switch item.type { - case .video: - contentTypeBadgeView.image = PhotoGridViewCell.videoBadgeImage - contentTypeBadgeView.isHidden = false - - case .animated: - contentTypeBadgeView.image = PhotoGridViewCell.animatedBadgeImage - contentTypeBadgeView.isHidden = false - - case .photo: - contentTypeBadgeView.image = nil - contentTypeBadgeView.isHidden = true + imageView.loadImage(item.source) { [weak imageView] processedData in + imageView?.themeBackgroundColor = (processedData != nil ? .clear : .textSecondary) } + + contentTypeBadgeView.isHidden = !item.isVideo } override public func prepareForReuse() { diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index 6aee5cfb2a..16f3c4e715 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -47,18 +47,7 @@ class PhotoPickerAssetItem: PhotoGridItem { // MARK: PhotoGridItem - var type: PhotoGridItemType { - if asset.mediaType == .video { - return .video - } - - if asset.utType?.isAnimated == true { - return .animated - } - - return .photo - } - + var isVideo: Bool { asset.mediaType == .video } var source: ImageDataManager.DataSource { return .asyncSource(self.asset.localIdentifier) { [photoCollectionContents, asset, size, pixelDimension] in await photoCollectionContents.requestThumbnail( diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 46cafadd53..2fbc6a648d 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -86,6 +86,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json deleted file mode 100644 index be7b71198d..0000000000 --- a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_GIF@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_GIF@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_GIF@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png deleted file mode 100644 index 58592c84638b334dca086a764b7a514058741373..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 380 zcmV-?0fYXDP)Px$Hc3Q5R45g#Q^Bf&KomXC9wiDPN|55(owVp51Q-2+_&I$>^Z~8>gO&aEJd zE(=1j@_C1pXS6Wr!e#E6LbO^5V-A7L2c@pwRzBv_V(+wF$K;Q-flvD@uBD0rTS<2e2Px2uvQ zK@bGkY&K971;^tNr_%|m)vANAmBg3J<)BBKUMNjd$g&JoRdKmoV45a4jw25Tq@pNL z*EK{@gr;fztQVrrY&K(qIF1oT5w6!OY}@X^ROES%^ZAU$V)0&x)XASL%U~D=BajcT zv0kqS0^Phnk*@2^56|=1b>{OqGn-y0000Px%u}MThR7efYS5He~Q4~K~shB1%TqL6Tr;Un8L_r{dxNse~@B_?zsKLH~A3$8p z2!e_Ox{ZQLkQP#+W-6wobAPvam*;zG&0yGp<2(P}J->U;dFMTe{?c?h)iD0XxXk#8 z@n;iqAQy6KBpm$847-efFCu)oQu@^74`jg#wq8$s`A!N~Od-O2J~WP$H2~nz_C?*UF5!T#l}M_|m-*%bs@LlRjNNXhNF+ko6-t-S=jrb5 zj;^k*$nAEM)9IAwQptY5p8|ovj2)X>L~{XEt5qJ4^2FnD+T45#Lk~rxQOaa8^z!mT z_xJbj0>TKp?Oj1{VRRQYtdc0SVX=i6gBt&E1 z+}sEqf=;KS{r&wJn^Hj^;w0r13WdnwaL7$URsj+oCdePa!C*keVo~nqfZqdzaRHW- z*Xxz5GqMVh$z&qwiF$l|6v0NLk&cdzxL&K(=;7gk^m@JUFA~uBN&q&SZBaO>0Ac8% z37gF(f6@Q~rIF?jR=T~ti`GJz1f5J}xcgb^<>`Q&Z p@|`eHU*;oU94$_Y@!J0e{Q(`(<$_uccUAxZ002ovPDHLkV1oB3X9EBL diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png deleted file mode 100644 index df5f374ad4ea6bf08b32dafc46104a0621c9e5cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1083 zcmV-B1jPG^P)Px&^+`lQR9Fe!SxZYJK@hHJeBx^bpAi*A7DYuwP?2O8%ti5W@Q|ZFZ1LjF;~o^z zEI!tQ3Zkf}(UXt$jZqWCcecK9dzhXilVlAslY*kBs%x71x~8jZx-Arf0)apet6j_v zW*c)Jb25>io%zGOz&viTSbiBg$AIdY=a|h#@VQ$8!R@ChBGC_S%?=7sfB{&@!qGyaa6?ETaw1~LeyK(-L~NH) zeN8cB9su((sF?Pto0}W*dcE}U@IW~^IaF9!NCgE2l%AfREc4?Bus}54yuH2A-rgRq zudmbH-CfWt0M*#oNUg1{3Ru0t;^HECJRVg&JUmQAMMa7{Iy$QABR!Orl~I3xe7^Ho}Q+YlasLKudlDPxw%O@J3HibI;pz4TC&B&z($aEKDBz$AFhqoR;2rI z;}9I0nwkm;DkCF<($eH1US3|v?RL}E)m7Lr6VVYdVjCM9bar+o`O?zTLJbWK%G$!r zpP!%8%F2pr2XHepGvso)f`Cf{g%FC1i>bT2J4DaB3X>TP83IGt0ZdDFyIlb#biB(` zd3ibY_4U!j#DoG8eLkO$4i68hwzgK7M%89!W>Q5(MO3y%MuxzSkB^niCkDW5B2uBq zYPC{lXD2N$qcVG^`}=$GnDEvR*y-u1R|I!ltMTQZBQ>fm1dwU60g`V&`6ABbctGc>6y?H|AsHQYKJDUau29zkFDE9kt5F1R$ znCLo#n^?_%9SvMxUlSOm4Z5MV^KZ>}0Ss3#`1LjW`MrE8!3g&m)DSZ|%u4Vb*GRxZ>ITgU z+$(i3&!fX+1fOwz1Plr(pP~>%%VkmzKb!ih_AeH+7J7eAhim`<002ovPDHLkV1fZj B{DA-f diff --git a/Session/Shared/Views/SessionHeaderView.swift b/Session/Shared/Views/SessionHeaderView.swift index 899f6b53ee..ebe88b1b95 100644 --- a/Session/Shared/Views/SessionHeaderView.swift +++ b/Session/Shared/Views/SessionHeaderView.swift @@ -34,7 +34,7 @@ class SessionHeaderView: UITableViewHeaderFooterView { private let loadingIndicator: UIActivityIndicatorView = { let result: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - result.themeTintColor = .textPrimary + result.themeColor = .textPrimary result.alpha = 0.5 result.startAnimating() result.hidesWhenStopped = true diff --git a/Session/Utilities/ImageLoading+Convenience.swift b/Session/Utilities/ImageLoading+Convenience.swift index 08e4ffd061..54a69d2524 100644 --- a/Session/Utilities/ImageLoading+Convenience.swift +++ b/Session/Utilities/ImageLoading+Convenience.swift @@ -102,7 +102,7 @@ public extension ImageDataManagerType { public extension SessionImageView { @MainActor - func loadImage(from path: String, onComplete: ((Bool) -> Void)? = nil) { + func loadImage(from path: String, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.url(URL(fileURLWithPath: path)), onComplete: onComplete) } @@ -110,13 +110,13 @@ public extension SessionImageView { func loadImage( attachment: Attachment, using dependencies: Dependencies, - onComplete: ((Bool) -> Void)? = nil + onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( attachment: attachment, using: dependencies ) else { - onComplete?(false) + onComplete?(nil) return } @@ -128,14 +128,14 @@ public extension SessionImageView { size: ImageDataManager.ThumbnailSize, attachment: Attachment, using dependencies: Dependencies, - onComplete: ((Bool) -> Void)? = nil + onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.thumbnailFrom( attachment: attachment, size: size, using: dependencies ) else { - onComplete?(false) + onComplete?(nil) return } @@ -143,7 +143,7 @@ public extension SessionImageView { } @MainActor - func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: ((Bool) -> Void)? = nil) { + func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.placeholderIcon(seed: seed, text: text, size: size), onComplete: onComplete) } } diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift index 3f60a24d4a..11894d2d79 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift @@ -181,7 +181,10 @@ enum _022_GroupsRebuildChanges: Migration { return } - let filename: String = generateFilename(format: imageData.guessedImageFormat, using: dependencies) + let filename: String = generateFilename( + format: MediaUtils.guessedImageFormat(data: imageData), + using: dependencies + ) let filePath: String = URL(fileURLWithPath: dependencies[singleton: .displayPictureManager].sharedDataDisplayPictureDirPath()) .appendingPathComponent(filename) .path diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 3f1f5854d9..544650a358 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -178,7 +178,7 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR case .success = Result(try dataSource.write(to: uploadInfo.path)) else { return nil } - let imageSize: CGSize? = Data.mediaSize( + let imageSize: CGSize? = MediaUtils.unrotatedSize( for: uploadInfo.path, type: UTType(sessionMimeType: contentType), mimeType: contentType, @@ -406,7 +406,7 @@ extension Attachment { .path(for: finalDownloadUrl) else { return nil } - return Data.mediaSize( + return MediaUtils.unrotatedSize( for: path, type: UTType(sessionMimeType: contentType), mimeType: contentType, diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index faa84197cc..51bfb22b48 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -510,7 +510,7 @@ public extension LinkPreview { ) .tryMap { asset, _ -> Data in let type: UTType? = UTType(sessionMimeType: imageMimeType) - let imageSize = Data.mediaSize( + let imageSize = MediaUtils.unrotatedSize( for: asset.filePath, type: type, mimeType: imageMimeType, @@ -522,17 +522,17 @@ public extension LinkPreview { throw LinkPreviewError.invalidContent } + // Loki: If it's a GIF then ensure its validity and don't download it as a JPG + if type == .gif && MediaUtils.isValidImage(at: asset.filePath, type: .gif, using: dependencies) { + return try Data(contentsOf: URL(fileURLWithPath: asset.filePath)) + } + guard let data: Data = try? Data(contentsOf: URL(fileURLWithPath: asset.filePath)) else { throw LinkPreviewError.assertionFailure } guard let srcImage = UIImage(data: data) else { throw LinkPreviewError.invalidContent } - // Loki: If it's a GIF then ensure its validity and don't download it as a JPG - if type == .gif && data.isValidImage(type: .gif) { - return data - } - let maxImageSize: CGFloat = 1024 let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index 152819c82f..adbc379e5c 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -111,6 +111,7 @@ public class SignalAttachment: Equatable { public var sourceFilename: String? { return dataSource.sourceFilename?.filteredFilename } public var isValidImage: Bool { return dataSource.isValidImage } public var isValidVideo: Bool { return dataSource.isValidVideo } + public var imageSize: CGSize? { return dataSource.imageSize } // This flag should be set for text attachments that can be sent as text messages. public var isConvertibleToTextMessage = false @@ -180,71 +181,6 @@ public class SignalAttachment: Equatable { return errorDescription } - - public func staticThumbnail(using dependencies: Dependencies) -> UIImage? { - if isAnimatedImage { - return image() - } - else if isImage { - return image() - } - else if isVideo { - return videoPreview(using: dependencies) - } - else if isAudio { - return nil - } - - return nil - } - - public func image() -> UIImage? { - if let cachedImage = cachedImage { - return cachedImage - } - guard let image = UIImage(data: dataSource.data) else { - return nil - } - - cachedImage = image - return image - } - - public func videoPreview(using dependencies: Dependencies) -> UIImage? { - if let cachedVideoPreview = cachedVideoPreview { - return cachedVideoPreview - } - - guard let mediaUrl = dataUrl else { - return nil - } - - do { - let filePath = mediaUrl.path - guard - dependencies[singleton: .fileManager].fileExists(atPath: filePath), - let mimeType: String = dataType.sessionMimeType, - let assetInfo: (asset: AVURLAsset, cleanup: () -> Void) = AVURLAsset.asset( - for: filePath, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - else { return nil } - - let generator = AVAssetImageGenerator(asset: assetInfo.asset) - generator.appliesPreferredTrackTransform = true - let cgImage = try generator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) - let image = UIImage(cgImage: cgImage) - assetInfo.cleanup() - - cachedVideoPreview = image - return image - - } catch { - return nil - } - } public func text() -> String? { guard let text = String(data: dataSource.data, encoding: .utf8) else { @@ -462,7 +398,7 @@ public class SignalAttachment: Equatable { } if UTType.supportedAnimatedImageTypes.contains(type) { - guard dataSource.dataLength <= MediaUtils.maxFileSizeAnimatedImage else { + guard dataSource.dataLength <= SNUtilitiesKit.maxFileSize else { attachment.error = .fileSizeTooLarge return attachment } @@ -515,7 +451,7 @@ public class SignalAttachment: Equatable { return ( doesImageHaveAcceptableFileSize(dataSource: dataSource, imageQuality: imageQuality) && - dataSource.dataLength <= MediaUtils.maxFileSizeImage + dataSource.dataLength <= SNUtilitiesKit.maxFileSize ) } @@ -545,7 +481,7 @@ public class SignalAttachment: Equatable { assert(attachment.error == nil) if imageQuality == .original && - attachment.dataLength < MediaUtils.maxFileSizeGeneric && + attachment.dataLength < SNUtilitiesKit.maxFileSize && UTType.supportedOutputImageTypes.contains(attachment.dataType) { // We should avoid resizing images attached "as documents" if possible. return attachment @@ -575,7 +511,7 @@ public class SignalAttachment: Equatable { dataSource.sourceFilename = jpgFilename if doesImageHaveAcceptableFileSize(dataSource: dataSource, imageQuality: imageQuality) && - dataSource.dataLength <= MediaUtils.maxFileSizeImage { + dataSource.dataLength <= SNUtilitiesKit.maxFileSize { let recompressedAttachment = SignalAttachment(dataSource: dataSource, dataType: .jpeg) recompressedAttachment.cachedImage = dstImage return recompressedAttachment @@ -759,7 +695,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: UTType.supportedVideoTypes, - maxFileSize: MediaUtils.maxFileSizeVideo, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } @@ -878,7 +814,7 @@ public class SignalAttachment: Equatable { guard let dataSource = dataSource, UTType.supportedOutputVideoTypes.contains(type), - dataSource.dataLength <= MediaUtils.maxFileSizeVideo + dataSource.dataLength <= SNUtilitiesKit.maxFileSize else { return false } return false @@ -895,7 +831,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: UTType.supportedAudioTypes, - maxFileSize: MediaUtils.maxFileSizeAudio, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } @@ -911,7 +847,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: nil, - maxFileSize: MediaUtils.maxFileSizeGeneric, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } diff --git a/SessionMessagingKit/Utilities/AttachmentManager.swift b/SessionMessagingKit/Utilities/AttachmentManager.swift index 33f95cdb4c..5746315ac1 100644 --- a/SessionMessagingKit/Utilities/AttachmentManager.swift +++ b/SessionMessagingKit/Utilities/AttachmentManager.swift @@ -210,7 +210,7 @@ public final class AttachmentManager: Sendable, ThumbnailManager { // Process image attachments if UTType.isImage(contentType) || UTType.isAnimated(contentType) { return ( - Data.isValidImage(at: path, type: UTType(sessionMimeType: contentType), using: dependencies), + MediaUtils.isValidImage(at: path, type: UTType(sessionMimeType: contentType), using: dependencies), nil ) } diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index f544f6eb3e..ae3b2adf93 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -191,7 +191,7 @@ public class DisplayPictureManager { let newEncryptionKey: Data let finalImageData: Data let fileExtension: String - let guessedFormat: ImageFormat = imageData.guessedImageFormat + let guessedFormat: ImageFormat = MediaUtils.guessedImageFormat(data: imageData) finalImageData = try { switch guessedFormat { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 70d2273560..00417e7b82 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -104,6 +104,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension /// Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 475489dd60..13b036416a 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -65,6 +65,7 @@ final class ShareNavController: UINavigationController { // Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index 70c8fc0d9b..0eec4aab2b 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -138,7 +138,7 @@ public class SessionImageView: UIImageView { } @MainActor - public func loadImage(_ source: ImageDataManager.DataSource, onComplete: ((Bool) -> Void)? = nil) { + public func loadImage(_ source: ImageDataManager.DataSource, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it @@ -154,9 +154,12 @@ public class SessionImageView: UIImageView { /// No need to kick of an async task if we were given an image directly switch source { case .image(_, .some(let image)): + let processedData: ImageDataManager.ProcessedImageData = ImageDataManager.ProcessedImageData( + type: .staticImage(image) + ) imageSizeMetadata = image.size - handleLoadedImageData(ImageDataManager.ProcessedImageData(type: .staticImage(image))) - onComplete?(true) + handleLoadedImageData(processedData) + onComplete?(processedData) return default: break @@ -181,7 +184,7 @@ public class SessionImageView: UIImageView { guard !Task.isCancelled && self?.currentLoadIdentifier == source.identifier else { return } self?.handleLoadedImageData(processedData) - onComplete?(processedData != nil) + onComplete?(processedData) } } } diff --git a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift index 9f79609489..d956e82826 100644 --- a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift +++ b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift @@ -35,7 +35,14 @@ public extension UIView { } var themeTintColor: ThemeValue? { - set { ThemeManager.set(self, keyPath: \.tintColor, to: newValue) } + set { + /// The `UIActivityIndicatorView` uses a `color` value instead of `tintColor` so redirect it in case this + /// is mistakenly used + switch self { + case let indicator as UIActivityIndicatorView: indicator.themeColor = newValue + default: ThemeManager.set(self, keyPath: \.tintColor, to: newValue) + } + } get { return nil } } @@ -337,6 +344,13 @@ public extension UIPageControl { } } +public extension UIActivityIndicatorView { + var themeColor: ThemeValue? { + set { ThemeManager.set(self, keyPath: \.color, to: newValue) } + get { return nil } + } +} + public extension GradientView { var themeBackgroundGradient: [ThemeValue]? { set { diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index eda34211bc..4cfaa8083f 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -243,9 +243,9 @@ public actor ImageDataManager: ImageDataManagerType { let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, sourceWidth > 0, - sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceWidth < ImageDataManager.DataSource.maxValidDimension, sourceHeight > 0, - sourceHeight < ImageDataManager.DataSource.maxValidSize + sourceHeight < ImageDataManager.DataSource.maxValidDimension else { return nil } /// Get the number of frames in the image @@ -798,7 +798,7 @@ extension AVAsset { public extension ImageDataManager.DataSource { /// We need to ensure that the image size is "reasonable", otherwise trying to load it could cause out-of-memory crashes - fileprivate static let maxValidSize: Int = 1 << 18 // 262,144 pixels + static let maxValidDimension: Int = 1 << 18 // 262,144 pixels @MainActor var sizeFromMetadata: CGSize? { @@ -826,9 +826,9 @@ public extension ImageDataManager.DataSource { let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, sourceWidth > 0, - sourceWidth < ImageDataManager.DataSource.maxValidSize, + sourceWidth < ImageDataManager.DataSource.maxValidDimension, sourceHeight > 0, - sourceHeight < ImageDataManager.DataSource.maxValidSize + sourceHeight < ImageDataManager.DataSource.maxValidDimension else { return nil } return CGSize(width: sourceWidth, height: sourceHeight) diff --git a/SessionUtilitiesKit/Configuration.swift b/SessionUtilitiesKit/Configuration.swift index ce7ef571ee..16485d4f3f 100644 --- a/SessionUtilitiesKit/Configuration.swift +++ b/SessionUtilitiesKit/Configuration.swift @@ -5,7 +5,8 @@ import UIKit.UIFont import GRDB public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API nice - public static var maxFileSize: UInt = 0 + public private(set) static var maxFileSize: UInt = 0 + public private(set) static var maxValidImageDimension: Int = 0 public static var isRunningTests: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil // stringlint:ignore } @@ -41,9 +42,11 @@ public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API public static func configure( networkMaxFileSize: UInt, + maxValidImageDimention: Int, using dependencies: Dependencies ) { self.maxFileSize = networkMaxFileSize + self.maxValidImageDimension = maxValidImageDimention // Register any recurring jobs to ensure they are actually scheduled dependencies[singleton: .jobRunner].registerRecurringJobs( diff --git a/SessionUtilitiesKit/Media/Data+Image.swift b/SessionUtilitiesKit/Media/Data+Image.swift deleted file mode 100644 index 028a20930b..0000000000 --- a/SessionUtilitiesKit/Media/Data+Image.swift +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import AVKit -import ImageIO -import UniformTypeIdentifiers - -public extension Data { - private struct ImageDimensions { - let pixelSize: CGSize - let depthBytes: CGFloat - } - - var isValidImage: Bool { - let imageFormat: ImageFormat = self.guessedImageFormat - let isAnimated: Bool = (imageFormat == .gif) - let maxFileSize: UInt = (isAnimated ? - MediaUtils.maxFileSizeAnimatedImage : - MediaUtils.maxFileSizeImage - ) - - return ( - count < maxFileSize && - isValidImage(type: nil, format: imageFormat) && - hasValidImageDimensions(isAnimated: isAnimated) - ) - } - - var guessedImageFormat: ImageFormat { - let twoBytesLength: Int = 2 - - guard count > twoBytesLength else { return .unknown } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. bufferLength else { return false } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: bufferLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. 0 && width < maxValidSize && height > 0 && height < maxValidSize) - } - - var sizeForWebpData: CGSize { - guard - guessedImageFormat == .webp, - let source: CGImageSource = CGImageSourceCreateWithData(self as CFData, nil) - else { return .zero } - - // Check if there's at least one image - let count: Int = CGImageSourceGetCount(source) - guard count > 0 else { - return .zero - } - - // Get properties of the first frame - guard let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else { - return .zero - } - - // Try to get dimensions from properties - if - let width: Int = properties[kCGImagePropertyPixelWidth] as? Int, - let height: Int = properties[kCGImagePropertyPixelHeight] as? Int, - width > 0, - height > 0 - { - return CGSize(width: width, height: height) - } - - // If we can't get dimensions from properties, try creating an image - if let image: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) { - return CGSize(width: image.width, height: image.height) - } - - return .zero - } - - // MARK: - Initialization - - init?(validImageDataAt path: String, type: UTType? = nil, using dependencies: Dependencies) throws { - let fileUrl: URL = URL(fileURLWithPath: path) - - guard - let type: UTType = type, - let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path), - fileSize <= SNUtilitiesKit.maxFileSize, - (type.isImage || type.isAnimated) - else { return nil } - - self = try Data(contentsOf: fileUrl, options: [.dataReadingMapped]) - } - - // MARK: - Functions - - func hasValidImageDimensions(isAnimated: Bool) -> Bool { - guard - let dataPtr: CFData = CFDataCreate(kCFAllocatorDefault, self.bytes, self.count), - let imageSource = CGImageSourceCreateWithData(dataPtr, nil) - else { return false } - - return Data.hasValidImageDimension(source: imageSource, isAnimated: isAnimated) - } - - func isValidImage(type: UTType?) -> Bool { - return isValidImage(type: type, format: self.guessedImageFormat) - } - - func isValidImage(type: UTType?, format: ImageFormat) -> Bool { - // Don't trust the file extension; iOS (e.g. UIKit, Core Graphics) will happily - // load a .gif with a .png file extension - // - // Instead, use the "magic numbers" in the file data to determine the image format - // - // If the image has a declared MIME type, ensure that agrees with the - // deduced image format - switch format { - case .unknown: return false - case .png: return (type == nil || type == .png) - case .jpeg: return (type == nil || type == .jpeg) - - case .gif: - guard hasValidGifSize else { return false } - - return (type == nil || type == .gif) - - case .tiff: return (type == nil || type == .tiff || type == .xTiff) - case .bmp: return (type == nil || type == .bmp || type == .xWinBpm) - case .webp: return (type == nil || type == .webP) - } - } - - static func isValidImage(at path: String, type: UTType? = nil, using dependencies: Dependencies) -> Bool { - guard let data: Data = try? Data(validImageDataAt: path, type: type, using: dependencies) else { - return false - } - - return data.hasValidImageDimensions(isAnimated: type?.isAnimated == true) - } - - static func hasValidImageDimension(source: CGImageSource, isAnimated: Bool) -> Bool { - guard let dimensions: ImageDimensions = imageDimensions(source: source) else { return false } - - // We only support (A)RGB and (A)Grayscale, so worst case is 4. - let worseCastComponentsPerPixel: CGFloat = 4 - let bytesPerPixel: CGFloat = (worseCastComponentsPerPixel * dimensions.depthBytes) - let expectedBytePerPixel: CGFloat = 4 - let maxValidImageDimension: CGFloat = CGFloat(isAnimated ? - MediaUtils.maxAnimatedImageDimensions : - MediaUtils.maxStillImageDimensions - ) - let maxBytes: CGFloat = (maxValidImageDimension * maxValidImageDimension * expectedBytePerPixel) - let actualBytes: CGFloat = (dimensions.pixelSize.width * dimensions.pixelSize.height * bytesPerPixel) - - return (actualBytes <= maxBytes) - } - - static func hasAlpha(forValidImageFilePath filePath: String) -> Bool { - let fileUrl: URL = URL(fileURLWithPath: filePath) - let options: [String: Any] = [kCGImageSourceShouldCache as String: NSNumber(booleanLiteral: false)] - - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) as? [CFString: Any], - let hasAlpha: Bool = properties[kCGImagePropertyHasAlpha] as? Bool - else { return false } - - return hasAlpha - } - - private static func imageDimensions(source: CGImageSource) -> ImageDimensions? { - guard - let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let width: Double = properties[kCGImagePropertyPixelWidth] as? Double, - let height: Double = properties[kCGImagePropertyPixelHeight] as? Double, - // The number of bits in each color sample of each pixel. The value of this key is a CFNumberRef - let depthBits: UInt = properties[kCGImagePropertyDepth] as? UInt - else { return nil } - - // This should usually be 1. - let depthBytes: CGFloat = ceil(CGFloat(depthBits) / 8.0) - - // The color model of the image such as "RGB", "CMYK", "Gray", or "Lab" - // The value of this key is CFStringRef - guard - let colorModel = properties[kCGImagePropertyColorModel] as? String, - ( - colorModel != (kCGImagePropertyColorModelRGB as String) || - colorModel != (kCGImagePropertyColorModelGray as String) - ) - else { return nil } - - return ImageDimensions(pixelSize: CGSize(width: width, height: height), depthBytes: depthBytes) - } - - static func mediaSize( - for path: String, - type: UTType?, - mimeType: String?, - sourceFilename: String?, - using dependencies: Dependencies - ) -> CGSize { - let fileUrl: URL = URL(fileURLWithPath: path) - let maybePixelSize: CGSize? = extractSize( - from: path, - type: type, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - - guard let pixelSize: CGSize = maybePixelSize else { return .zero } - - // WebP and videos shouldn't have orientations so no need for any logic to rotate the size - switch (type, type?.isVideo, type?.isAnimated) { - case (.webP, _, _), (_, true, _), (_, _, true): return pixelSize - default: break - } - - // With CGImageSource we avoid loading the whole image into memory. - let options: [String: Any] = [kCGImageSourceShouldCache as String: NSNumber(booleanLiteral: false)] - - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) as? [AnyHashable: Any], - let width: CGFloat = properties[kCGImagePropertyPixelWidth as String] as? CGFloat, - let height: CGFloat = properties[kCGImagePropertyPixelHeight as String] as? CGFloat - else { return .zero } - - guard - let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation] as? UInt32, - let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) - else { - return CGSize(width: width, height: height) - } - - return apply( - orientation: UIImage.Orientation(cgOrientation), - to: CGSize(width: width, height: height) - ) - } - - private static func apply(orientation: UIImage.Orientation, to imageSize: CGSize) -> CGSize { - switch orientation { - case .up, .upMirrored, .down, .downMirrored: return imageSize - case .leftMirrored, .left, .rightMirrored, .right: - return CGSize(width: imageSize.height, height: imageSize.width) - - @unknown default: return imageSize - } - } - - private static func extractSize( - from path: String, - type: UTType?, - mimeType: String?, - sourceFilename: String?, - using dependencies: Dependencies - ) -> CGSize? { - let fileUrl: URL = URL(fileURLWithPath: path) - - switch (type, type?.isVideo) { - case (.webP, _): - // Need to custom handle WebP images - guard let targetData: Data = try? Data(contentsOf: fileUrl, options: [.dataReadingMapped]) else { - return nil - } - - let imageSize: CGSize = targetData.sizeForWebpData - - guard imageSize.width > 0, imageSize.height > 0 else { return nil } - - return imageSize - - case (_, true): - // Videos don't have the same metadata as images so also need custom handling - let assetInfo: (asset: AVURLAsset, cleanup: () -> Void)? = AVURLAsset.asset( - for: path, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - - guard - let asset: AVURLAsset = assetInfo?.asset, - let track: AVAssetTrack = asset.tracks(withMediaType: .video).first - else { return nil } - - let size: CGSize = track.naturalSize - let transformedSize: CGSize = size.applying(track.preferredTransform) - let videoSize: CGSize = CGSize( - width: abs(transformedSize.width), - height: abs(transformedSize.height) - ) - - guard videoSize.width > 0, videoSize.height > 0 else { return nil } - - return videoSize - - default: - // Otherwise use our custom code - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let dimensions: ImageDimensions = imageDimensions(source: imageSource), - dimensions.pixelSize.width > 0, - dimensions.pixelSize.height > 0, - dimensions.depthBytes > 0 - else { return nil } - - return dimensions.pixelSize - } - } -} - -private extension UIImage.Orientation { - init(_ cgOrientation: CGImagePropertyOrientation) { - switch cgOrientation { - case .up: self = .up - case .upMirrored: self = .upMirrored - case .down: self = .down - case .downMirrored: self = .downMirrored - case .left: self = .left - case .leftMirrored: self = .leftMirrored - case .right: self = .right - case .rightMirrored: self = .rightMirrored - } - } -} diff --git a/SessionUtilitiesKit/Media/DataSource.swift b/SessionUtilitiesKit/Media/DataSource.swift index 31730867dc..84fcddb595 100644 --- a/SessionUtilitiesKit/Media/DataSource.swift +++ b/SessionUtilitiesKit/Media/DataSource.swift @@ -1,11 +1,14 @@ // Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. import Foundation +import CoreGraphics +import ImageIO import UniformTypeIdentifiers // MARK: - DataSource public protocol DataSource: Equatable { + var dependencies: Dependencies { get } var data: Data { get } var dataUrl: URL? { get } @@ -20,17 +23,55 @@ public protocol DataSource: Equatable { var dataLength: Int { get } var sourceFilename: String? { get set } + var fileExtension: String { get } var mimeType: String? { get } var shouldDeleteOnDeinit: Bool { get } - var isValidImage: Bool { get } - var isValidVideo: Bool { get } - // MARK: - Functions func write(to path: String) throws } +public extension DataSource { + var imageSize: CGSize? { + let type: UTType? = UTType(sessionFileExtension: fileExtension) + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + let maybeSource: CGImageSource? = { + switch self.dataPathIfOnDisk { + case .some(let path): return CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, options) + case .none: return CGImageSourceCreateWithData(data as CFData, options) + } + }() + + guard let source: CGImageSource = maybeSource else { return nil } + + return MediaUtils.MediaMetadata(source: source)?.pixelSize + } + + var isValidImage: Bool { + let type: UTType? = UTType(sessionFileExtension: fileExtension) + + switch self.dataPathIfOnDisk { + case .some(let path): return MediaUtils.isValidImage(at: path, type: type, using: dependencies) + case .none: return MediaUtils.isValidImage(data: data, type: type) + } + } + + var isValidVideo: Bool { + guard let dataUrl: URL = self.dataUrl else { return false } + + return MediaUtils.isValidVideo( + path: dataUrl.path, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + } +} + // MARK: - DataSourceValue public class DataSourceValue: DataSource { @@ -38,10 +79,10 @@ public class DataSourceValue: DataSource { return DataSourceValue(data: Data(), fileExtension: UTType.fileExtensionText, using: dependencies) } - private let dependencies: Dependencies + public let dependencies: Dependencies public var data: Data public var sourceFilename: String? - var fileExtension: String + public var fileExtension: String var cachedFilePath: String? public var shouldDeleteOnDeinit: Bool @@ -68,28 +109,6 @@ public class DataSourceValue: DataSource { } } - public var isValidImage: Bool { - guard let dataPath: String = self.dataPathIfOnDisk else { - return self.data.isValidImage - } - - // if ows_isValidImage is given a file path, it will - // avoid loading most of the data into memory, which - // is considerably more performant, so try to do that. - return Data.isValidImage(at: dataPath, type: UTType(sessionFileExtension: fileExtension), using: dependencies) - } - - public var isValidVideo: Bool { - guard let dataUrl: URL = self.dataUrl else { return false } - - return MediaUtils.isValidVideo( - path: dataUrl.path, - mimeType: UTType.sessionMimeType(for: fileExtension), - sourceFilename: sourceFilename, - using: dependencies - ) - } - // MARK: - Initialization public init(data: Data, fileExtension: String, using dependencies: Dependencies) { @@ -157,9 +176,10 @@ public class DataSourceValue: DataSource { // MARK: - DataSourcePath public class DataSourcePath: DataSource { - private let dependencies: Dependencies + public let dependencies: Dependencies public var filePath: String public var sourceFilename: String? + public var fileExtension: String { URL(fileURLWithPath: filePath).pathExtension } var cachedData: Data? var cachedDataLength: Int? public var shouldDeleteOnDeinit: Bool @@ -197,32 +217,6 @@ public class DataSourcePath: DataSource { } public var mimeType: String? { UTType.sessionMimeType(for: URL(fileURLWithPath: filePath).pathExtension) } - - public var isValidImage: Bool { - guard let dataPath: String = self.dataPathIfOnDisk else { - return self.data.isValidImage - } - - // if ows_isValidImage is given a file path, it will - // avoid loading most of the data into memory, which - // is considerably more performant, so try to do that. - return Data.isValidImage( - at: dataPath, - type: UTType(sessionFileExtension: URL(fileURLWithPath: filePath).pathExtension), - using: dependencies - ) - } - - public var isValidVideo: Bool { - guard let dataUrl: URL = self.dataUrl else { return false } - - return MediaUtils.isValidVideo( - path: dataUrl.path, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - } // MARK: - Initialization diff --git a/SessionUtilitiesKit/Media/MediaUtils.swift b/SessionUtilitiesKit/Media/MediaUtils.swift index dee568b0ed..ae3dec2938 100644 --- a/SessionUtilitiesKit/Media/MediaUtils.swift +++ b/SessionUtilitiesKit/Media/MediaUtils.swift @@ -18,16 +18,130 @@ public enum MediaError: Error { // MARK: - MediaUtils public enum MediaUtils { - public static var maxFileSizeAnimatedImage: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeImage: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeVideo: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeAudio: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeGeneric: UInt { SNUtilitiesKit.maxFileSize } - - public static let maxAnimatedImageDimensions: UInt = 1 * 1024 - public static let maxStillImageDimensions: UInt = 8 * 1024 - public static let maxVideoDimensions: CGFloat = 3 * 1024 - + public struct MediaMetadata { + public let pixelSize: CGSize + public let frameCount: Int + public let depthBytes: CGFloat? + public let hasAlpha: Bool? + public let colorModel: String? + public let orientation: UIImage.Orientation? + + public var hasValidPixelSize: Bool { + pixelSize.width > 0 && + pixelSize.width < CGFloat(SNUtilitiesKit.maxValidImageDimension) && + pixelSize.height > 0 && + pixelSize.height < CGFloat(SNUtilitiesKit.maxValidImageDimension) + } + + // MARK: - Initialization + + public init?(source: CGImageSource) { + let count: Int = CGImageSourceGetCount(source) + + guard + count > 0, + let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], + let width: Double = properties[kCGImagePropertyPixelWidth] as? Double, + let height: Double = properties[kCGImagePropertyPixelHeight] as? Double + else { return nil } + + self.pixelSize = CGSize(width: width, height: height) + self.frameCount = count + self.depthBytes = { + /// The number of bits in each color sample of each pixel. The value of this key is a CFNumberRef + guard let depthBits: UInt = properties[kCGImagePropertyDepth] as? UInt else { return nil } + + /// This should usually be 1 + return ceil(CGFloat(depthBits) / 8.0) + }() + self.hasAlpha = (properties[kCGImagePropertyHasAlpha] as? Bool) + /// The color model of the image such as "RGB", "CMYK", "Gray", or "Lab", the value of this key is CFStringRef + self.colorModel = (properties[kCGImagePropertyColorModel] as? String) + self.orientation = { + guard + let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation] as? UInt32, + let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) + else { return nil } + + return UIImage.Orientation(cgOrientation) + }() + } + + public init( + pixelSize: CGSize, + depthBytes: CGFloat? = nil, + hasAlpha: Bool? = nil, + colorModel: String? = nil, + orientation: UIImage.Orientation? = nil + ) { + self.pixelSize = pixelSize + self.frameCount = 1 + self.depthBytes = depthBytes + self.hasAlpha = hasAlpha + self.colorModel = colorModel + self.orientation = orientation + } + + public init?( + from path: String, + type: UTType?, + mimeType: String?, + sourceFilename: String?, + using dependencies: Dependencies + ) { + /// Videos don't have the same metadata as images so need custom handling + guard type?.isVideo != true else { + let assetInfo: (asset: AVURLAsset, cleanup: () -> Void)? = AVURLAsset.asset( + for: path, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + + guard + let asset: AVURLAsset = assetInfo?.asset, + let track: AVAssetTrack = asset.tracks(withMediaType: .video).first + else { return nil } + + let size: CGSize = track.naturalSize + let transformedSize: CGSize = size.applying(track.preferredTransform) + let videoSize: CGSize = CGSize( + width: abs(transformedSize.width), + height: abs(transformedSize.height) + ) + + guard videoSize.width > 0, videoSize.height > 0 else { return nil } + + self.pixelSize = videoSize + self.frameCount = -1 /// Rather than try to extract the frames, or give it an "incorrect" value, make it explicitly invalid + self.depthBytes = nil + self.hasAlpha = false + self.colorModel = nil + self.orientation = nil + return + } + + guard + let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil), + let metadata: MediaMetadata = MediaMetadata(source: imageSource) + else { return nil } + + self = metadata + } + + // MARK: - Functions + + public func apply(orientation: UIImage.Orientation) -> CGSize { + switch orientation { + case .up, .upMirrored, .down, .downMirrored: return pixelSize + case .leftMirrored, .left, .rightMirrored, .right: + return CGSize(width: pixelSize.height, height: pixelSize.width) + + @unknown default: return pixelSize + } + } + } + public static func isVideoOfValidContentTypeAndSize(path: String, type: String?, using dependencies: Dependencies) -> Bool { guard dependencies[singleton: .fileManager].fileExists(atPath: path) else { Log.error(.media, "Media file missing.") @@ -37,30 +151,24 @@ public enum MediaUtils { Log.error(.media, "Media file has invalid content type.") return false } - + guard let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path) else { Log.error(.media, "Media file has unknown length.") return false } return UInt(fileSize) <= SNUtilitiesKit.maxFileSize } - + public static func isValidVideo(asset: AVURLAsset) -> Bool { var maxTrackSize = CGSize.zero + for track: AVAssetTrack in asset.tracks(withMediaType: .video) { let trackSize: CGSize = track.naturalSize maxTrackSize.width = max(maxTrackSize.width, trackSize.width) maxTrackSize.height = max(maxTrackSize.height, trackSize.height) } - if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 { - Log.error(.media, "Invalid video size: \(maxTrackSize)") - return false - } - if maxTrackSize.width > maxVideoDimensions || maxTrackSize.height > maxVideoDimensions { - Log.error(.media, "Invalid video dimensions: \(maxTrackSize)") - return false - } - return true + + return MediaMetadata(pixelSize: maxTrackSize).hasValidPixelSize } /// Use `isValidVideo(asset: AVURLAsset)` if the `AVURLAsset` needs to be generated elsewhere in the code, @@ -80,4 +188,98 @@ public enum MediaUtils { return result } + + public static func isValidImage(data: Data, type: UTType? = nil) -> Bool { + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + + guard + data.count < SNUtilitiesKit.maxFileSize, + let type: UTType = type, + (type.isImage || type.isAnimated), + let source: CGImageSource = CGImageSourceCreateWithData(data as CFData, options), + let metadata: MediaMetadata = MediaMetadata(source: source) + else { return false } + + return metadata.hasValidPixelSize + } + + public static func isValidImage(at path: String, type: UTType? = nil, using dependencies: Dependencies) -> Bool { + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + + guard + let type: UTType = type, + let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path), + fileSize <= SNUtilitiesKit.maxFileSize, + (type.isImage || type.isAnimated), + let source: CGImageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, options), + let metadata: MediaMetadata = MediaMetadata(source: source) + else { return false } + + return metadata.hasValidPixelSize + } + + public static func unrotatedSize( + for path: String, + type: UTType?, + mimeType: String?, + sourceFilename: String?, + using dependencies: Dependencies + ) -> CGSize { + guard + let metadata: MediaMetadata = MediaMetadata( + from: path, + type: type, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + else { return .zero } + + /// If the metadata doesn't ahve an orientation then don't rotate the size (WebP and videos shouldn't have orientations) + guard let orientation: UIImage.Orientation = metadata.orientation else { return metadata.pixelSize } + + return metadata.apply(orientation: orientation) + } + + public static func guessedImageFormat(data: Data) -> ImageFormat { + let twoBytesLength: Int = 2 + + guard data.count > twoBytesLength else { return .unknown } + + var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) + data.copyBytes(to: &bytes, from: (data.startIndex.. Void)? + private let wasCancelled: ((UIDocumentPickerViewController) -> Void)? + + // MARK: - Initialization + + public init( + didPickDocumentsAt: ((UIDocumentPickerViewController, [URL]) -> Void)? = nil, + wasCancelled: ((UIDocumentPickerViewController) -> Void)? = nil + ) { + self.didPickDocumentsAt = didPickDocumentsAt + self.wasCancelled = wasCancelled + } + + // MARK: - UIDocumentPickerDelegate + + public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + didPickDocumentsAt?(controller, urls) + } + + public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + wasCancelled?(controller) + } +} diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 2d09e83f81..0332840e9c 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -524,7 +524,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // Image editor has no changes. return attachmentItem.attachment } - guard let dstImage = ImageEditorCanvasView.renderForOutput(model: imageEditorModel, transform: imageEditorModel.currentTransform()) else { + guard let dstImage = ImageEditorCanvasView.renderForOutput(model: imageEditorModel, transform: imageEditorModel.currentTransform(), using: dependencies) else { Log.error(.cat, "Could not render for output.") return attachmentItem.attachment } @@ -758,11 +758,39 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate extension SignalAttachmentItem: GalleryRailItem { func buildRailItemView(using dependencies: Dependencies) -> UIView { - let imageView = UIImageView() - imageView.image = getThumbnailImage(using: dependencies) - imageView.themeBackgroundColor = .backgroundSecondary + let imageView: SessionImageView = SessionImageView(dataManager: dependencies[singleton: .imageDataManager]) imageView.contentMode = .scaleAspectFill + imageView.themeBackgroundColor = .backgroundSecondary + if let path: String = (attachment.dataSource.dataPathIfOnDisk ?? attachment.dataUrl?.absoluteString) { + let source: ImageDataManager.DataSource = { + /// Can't thumbnail animated images so just load the full file in this case + if attachment.isAnimatedImage { + return .url(URL(fileURLWithPath: path)) + } + + /// Videos have a custom method for generating their thumbnails so use that instead + if attachment.isVideo { + return .videoUrl( + URL(fileURLWithPath: path), + attachment.mimeType, + attachment.sourceFilename, + dependencies[singleton: .attachmentManager] + ) + } + + return .urlThumbnail( + URL(fileURLWithPath: path), + .small, + dependencies[singleton: .attachmentManager] + ) + }() + + Task(priority: .userInitiated) { + await imageView.loadImage(source) + } + } + return imageView } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift index dc8c384ae3..b636a474fc 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift @@ -60,10 +60,6 @@ class SignalAttachmentItem: Equatable { return attachment.captionText } - func getThumbnailImage(using dependencies: Dependencies) -> UIImage? { - return attachment.staticThumbnail(using: dependencies) - } - // MARK: Equatable static func == (lhs: SignalAttachmentItem, rhs: SignalAttachmentItem) -> Bool { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index 40ee9e0643..0844c1ee20 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -74,7 +74,7 @@ public class AttachmentPrepViewController: OWSViewController { private lazy var imageEditorView: ImageEditorView? = { guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil } - let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) + let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self, using: dependencies) view.translatesAutoresizingMaskIntoConstraints = false guard view.configureSubviews() else { return nil } @@ -201,7 +201,7 @@ public class AttachmentPrepViewController: OWSViewController { ]) if attachment.isImage, let editorView: ImageEditorView = imageEditorView { - let size: CGSize = (attachment.image()?.size ?? CGSize.zero) + let size: CGSize = (attachment.imageSize ?? CGSize.zero) let isPortrait: Bool = (size.height > size.width) NSLayoutConstraint.activate([ diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift index 9a48afaeb0..f323b7adb9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift @@ -595,16 +595,22 @@ public class ImageEditorCanvasView: UIView { // We render using the transform parameter, not the transform from the model. // This allows this same method to be used for rendering "previews" for the // crop tool and the final output. - public class func renderForOutput(model: ImageEditorModel, transform: ImageEditorTransform) -> UIImage? { - // TODO: Do we want to render off the main thread? - Log.assertOnMainThread() - + @MainActor public class func renderForOutput( + model: ImageEditorModel, + transform: ImageEditorTransform, + using dependencies: Dependencies + ) -> UIImage? { // Render output at same size as source image. let dstSizePixels = transform.outputSizePixels let dstScale: CGFloat = 1.0 // The size is specified in pixels, not in points. let viewSize = dstSizePixels - - let hasAlpha = Data.hasAlpha(forValidImageFilePath: model.srcImagePath) + let hasAlpha: Bool = (MediaUtils.MediaMetadata( + from: model.srcImagePath, + type: nil, + mimeType: nil, + sourceFilename: nil, + using: dependencies + )?.hasAlpha == true) // We use an UIImageView + UIView.renderAsImage() instead of a CGGraphicsContext // Because CALayer.renderInContext() doesn't honor CALayer properties like frame, diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift index 8f0478b432..1e6e22dfc1 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift @@ -70,7 +70,7 @@ public class ImageEditorModel { throw ImageEditorError.invalidInput } - let srcImageSizePixels = Data.mediaSize( + let srcImageSizePixels = MediaUtils.unrotatedSize( for: srcImagePath, type: type, mimeType: nil, @@ -278,10 +278,11 @@ public class ImageEditorModel { // MARK: - Utilities // Returns nil on error. - private class func crop(imagePath: String, unitCropRect: CGRect) -> UIImage? { - // TODO: Do we want to render off the main thread? - Log.assertOnMainThread() - + @MainActor private class func crop( + imagePath: String, + unitCropRect: CGRect, + using dependencies: Dependencies + ) -> UIImage? { guard let srcImage = UIImage(contentsOfFile: imagePath) else { Log.error("[ImageEditorModel] Could not load image") return nil @@ -307,8 +308,13 @@ public class ImageEditorModel { Log.warn("[ImageEditorModel] Empty crop rectangle.") return nil } - - let hasAlpha = Data.hasAlpha(forValidImageFilePath: imagePath) + let hasAlpha: Bool = (MediaUtils.MediaMetadata( + from: imagePath, + type: nil, + mimeType: nil, + sourceFilename: nil, + using: dependencies + )?.hasAlpha == true) UIGraphicsBeginImageContextWithOptions(cropRect.size, !hasAlpha, srcImage.scale) defer { UIGraphicsEndImageContext() } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift index 09a508e811..be0b023167 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift @@ -20,15 +20,16 @@ public class ImageEditorView: UIView { weak var delegate: ImageEditorViewDelegate? + private let dependencies: Dependencies private let model: ImageEditorModel - private let canvasView: ImageEditorCanvasView // TODO: We could hang this on the model or make this static // if we wanted more color continuity. private var currentColor = ImageEditorColor.defaultColor() - public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) { + public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate, using dependencies: Dependencies) { + self.dependencies = dependencies self.model = model self.delegate = delegate self.canvasView = ImageEditorCanvasView(model: model) @@ -463,7 +464,7 @@ public class ImageEditorView: UIView { // into the background image without applying the transform (e.g. rotating, etc.), so we // use a default transform. let previewTransform = ImageEditorTransform.defaultTransform(srcImageSizePixels: model.srcImageSizePixels) - guard let previewImage = ImageEditorCanvasView.renderForOutput(model: model, transform: previewTransform) else { + guard let previewImage = ImageEditorCanvasView.renderForOutput(model: model, transform: previewTransform, using: dependencies) else { Log.error("[ImageEditorView] Couldn't generate preview image.") return } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index c2e0ab5444..30dea77793 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -22,42 +22,6 @@ public class MediaMessageView: UIView { public let mode: Mode public let attachment: SignalAttachment private let disableLinkPreviewImageDownload: Bool - - private lazy var validImageData: Data? = { - guard - attachment.isValidImage, - let dataUrl: URL = attachment.dataUrl, - let imageData: Data = try? Data(contentsOf: dataUrl), ( - ( - attachment.dataType == .gif && - attachment.isAnimatedImage && - imageData.hasValidGifSize - ) || ( - attachment.dataType == .webP && - attachment.isAnimatedImage && - imageData.sizeForWebpData != .zero - ) || ( - imageData.hasValidImageDimensions(isAnimated: false) - ) - ) - else { return nil } - - return imageData - }() - private lazy var validVideoImage: UIImage? = { - if attachment.isVideo { - guard - attachment.isValidVideo, - let image: UIImage = attachment.videoPreview(using: dependencies), - image.size.width > 0, - image.size.height > 0 - else { return nil } - - return image - } - - return nil - }() private lazy var duration: TimeInterval? = attachment.duration(using: dependencies) private var linkPreviewInfo: (url: String, draft: LinkPreviewDraft?)? @@ -146,10 +110,19 @@ public class MediaMessageView: UIView { // Override the image to the correct one if attachment.isImage || attachment.isAnimatedImage { - if let imageData: Data = validImageData, let dataUrl: URL = attachment.dataUrl { + let maybeSource: ImageDataManager.DataSource? = { + guard attachment.isValidImage else { return nil } + + return ( + attachment.dataSource.dataPathIfOnDisk.map { .url(URL(fileURLWithPath: $0)) } ?? + attachment.dataSource.dataUrl.map { .url($0) } + ) + }() + + if let source: ImageDataManager.DataSource = maybeSource { view.layer.minificationFilter = .trilinear view.layer.magnificationFilter = .trilinear - view.loadImage(.data(dataUrl.absoluteString, imageData)) + view.loadImage(source) } else { view.contentMode = .scaleAspectFit @@ -158,10 +131,28 @@ public class MediaMessageView: UIView { } } else if attachment.isVideo { - if let validImage: UIImage = validVideoImage { + let maybeSource: ImageDataManager.DataSource? = { + guard attachment.isValidVideo else { return nil } + + return attachment.dataSource.dataUrl.map { url in + .videoUrl( + url, + attachment.mimeType, + attachment.sourceFilename, + dependencies[singleton: .attachmentManager] + ) + } + }() + + if let source: ImageDataManager.DataSource = maybeSource { view.layer.minificationFilter = .trilinear view.layer.magnificationFilter = .trilinear - view.image = validImage + view.loadImage(source) + } + else { + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.themeTintColor = .textPrimary } } else if attachment.isUrl { @@ -378,12 +369,12 @@ public class MediaMessageView: UIView { let maybeImageSize: CGFloat? = { if attachment.isImage || attachment.isAnimatedImage { - if validImageData != nil { return nil } + guard attachment.isValidImage else { return nil } // If we don't have a valid image then use the 'generic' case } else if attachment.isValidVideo { - if validVideoImage != nil { return nil } + guard attachment.isValidVideo else { return nil } // If we don't have a valid image then use the 'generic' case } From f0f47b07710e96055f89f1f453ad1ac115793cea Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 3 Jul 2025 15:28:47 +1000 Subject: [PATCH 071/244] fix an issue where emoji reactions to a contact's own message should not display a notification --- .../MessageReceiver+VisibleMessages.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index e096a96a9d..c67f091f60 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -516,7 +516,7 @@ extension MessageReceiver { using dependencies: Dependencies ) throws -> Int64? { guard - let reaction: VisibleMessage.VMReaction = message.reaction, + let vmReaction: VisibleMessage.VMReaction = message.reaction, proto.dataMessage?.reaction != nil else { return nil } @@ -525,8 +525,8 @@ extension MessageReceiver { let maybeInteractionId: Int64? = try? Interaction .select(.id) .filter(Interaction.Columns.threadId == thread.id) - .filter(Interaction.Columns.timestampMs == reaction.timestamp) - .filter(Interaction.Columns.authorId == reaction.publicKey) + .filter(Interaction.Columns.timestampMs == vmReaction.timestamp) + .filter(Interaction.Columns.authorId == vmReaction.publicKey) .filter(Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted) .filter(Interaction.Columns.state != Interaction.State.deleted) .asRequest(of: Int64.self) @@ -539,10 +539,10 @@ extension MessageReceiver { let sortId = Reaction.getSortId( db, interactionId: interactionId, - emoji: reaction.emoji + emoji: vmReaction.emoji ) - switch reaction.kind { + switch vmReaction.kind { case .react: // Determine whether the app is active based on the prefs rather than the UIApplication state to avoid // requiring main-thread execution @@ -554,7 +554,7 @@ extension MessageReceiver { serverHash: message.serverHash, timestampMs: timestampMs, authorId: sender, - emoji: reaction.emoji, + emoji: vmReaction.emoji, count: 1, sortId: sortId ).inserted(db) @@ -568,11 +568,12 @@ extension MessageReceiver { } // Don't notify if the reaction was added before the lastest read timestamp for - // the conversation + // the conversation or the reaction is for the sender's own message if !suppressNotifications && sender != userSessionId.hexString && - !timestampAlreadyRead + !timestampAlreadyRead && + vmReaction.publicKey != sender { try? dependencies[singleton: .notificationsManager].notifyUser( cat: .messageReceiver, @@ -619,7 +620,7 @@ extension MessageReceiver { try Reaction .filter(Reaction.Columns.interactionId == interactionId) .filter(Reaction.Columns.authorId == sender) - .filter(Reaction.Columns.emoji == reaction.emoji) + .filter(Reaction.Columns.emoji == vmReaction.emoji) .deleteAll(db) } From 6a79a2d636a7b8cc87df2fd789dda10f101a7680 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Aug 2025 13:16:35 +1000 Subject: [PATCH 072/244] Updated the 'ensureWeShouldShowNotification' with the same logic --- .../Notifications/NotificationsManagerType.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift index aa0cdcefa0..87667ae234 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift @@ -95,10 +95,16 @@ public extension NotificationsManagerType { ) else { throw MessageReceiverError.ignorableMessage } - /// If the message is a reaction then we only want to show notifications for `contact` conversations + /// If the message is a reaction then we only want to show notifications for `contact` conversations, any only if the + /// reaction isn't added to a message sent by the reactor if visibleMessage.reaction != nil { switch threadVariant { - case .contact: break + case .contact: + guard visibleMessage.reaction?.publicKey != sender else { + throw MessageReceiverError.ignorableMessage + } + break + case .legacyGroup, .group, .community: throw MessageReceiverError.ignorableMessage } } From 7c1d7c357bd949d765c2d692982bb42ae854df51 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Aug 2025 13:22:28 +1000 Subject: [PATCH 073/244] Bumped build and version numbers --- Session.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index abcb82d11d..7311847f2a 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8152,7 +8152,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 623; + CURRENT_PROJECT_VERSION = 624; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8192,7 +8192,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.14.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8233,7 +8233,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 623; + CURRENT_PROJECT_VERSION = 624; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8268,7 +8268,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.14.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8714,7 +8714,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 623; + CURRENT_PROJECT_VERSION = 624; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8753,7 +8753,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.14.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9301,7 +9301,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 623; + CURRENT_PROJECT_VERSION = 624; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9334,7 +9334,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.0; + MARKETING_VERSION = 2.14.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", From 36323ccdfcfd4d788390270b23d536b4ac18b2aa Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 14 Aug 2025 13:38:38 +1000 Subject: [PATCH 074/244] Fixed a couple of issues with the CI config --- .drone.jsonnet | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 4a98a62f64..04afc476a2 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,7 +77,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ - 'echo "Explicitly running unit tests on `App_Store_Release` configuration to ensure optimisation behaviour is consistent"', + 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', 'echo "If tests fail inconsistently from local builds this is likely the difference"', 'echo ""', 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', @@ -89,9 +89,19 @@ local clean_up_old_test_sims_on_commit_trigger = { ], }, { - name: 'Unit Test Summary', + name: 'Stop Simulator Keep-Alive', commands: [ + 'echo "Signaling simulator keep-alive to stop and clean up..."', sim_delete_cmd, + ], + depends_on: ['Build and Run Tests'], + when: { + status: ['success', 'failure'], + }, + }, + { + name: 'Unit Test Summary', + commands: [ 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' ], depends_on: ['Build and Run Tests'] From 467df3d01d762bbda0380d43a8ad149f56af01cf Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 14 Aug 2025 13:32:51 +0800 Subject: [PATCH 075/244] Added app review triggers and dialgos --- Session.xcodeproj/project.pbxproj | 4 + .../Home/App Review/AppReviewManager.swift | 95 +++++ .../View/AppReviewPromptDialog.swift | 8 + Session/Home/HomeVC.swift | 399 ++++++++++-------- Session/Path/PathVC.swift | 2 + Session/Settings/AppearanceViewModel.swift | 10 +- Session/Settings/SettingsViewModel.swift | 3 + SessionUIKit/Style Guide/Constants.swift | 1 + .../Types/UserDefaultsType.swift | 12 + 9 files changed, 366 insertions(+), 168 deletions(-) create mode 100644 Session/Home/App Review/AppReviewManager.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5f7dc947f0..49e81512f7 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1157,6 +1157,7 @@ FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; + FEAB06002E4D7D5D0006237B /* AppReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */; }; FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; }; FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; }; /* End PBXBuildFile section */ @@ -2430,6 +2431,7 @@ FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; + FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewManager.swift; sourceTree = ""; }; FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = ""; }; FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -5134,6 +5136,7 @@ children = ( FED288F42E4C3B5A00C31171 /* View */, FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */, + FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */, ); path = "App Review"; sourceTree = ""; @@ -6872,6 +6875,7 @@ 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */, 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, + FEAB06002E4D7D5D0006237B /* AppReviewManager.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, FDEF57242C3CF04700131302 /* (null) in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, diff --git a/Session/Home/App Review/AppReviewManager.swift b/Session/Home/App Review/AppReviewManager.swift new file mode 100644 index 0000000000..d5c6593f75 --- /dev/null +++ b/Session/Home/App Review/AppReviewManager.swift @@ -0,0 +1,95 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUtilitiesKit + +public extension Singleton { + static let appReviewManager: SingletonConfig = Dependencies.create( + identifier: "appReviewManager", + createInstance: { dependencies in AppReviewManager(using: dependencies) } + ) +} + +public enum AppReviewTrigger { + case pathScreenVisit + case donateButtonPress + case themeChange +} + +// MARK: - AppReviewManager +public class AppReviewManager: NSObject, ObservableObject { + private let dependencies: Dependencies + + @Published var currentPrompToShow: AppReviewPromptState = .none + + private var shouldTriggerReview: Bool = false + + // MARK: - Initialization + fileprivate init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init() + } + + func triggerReview(for trigger: AppReviewTrigger) { + currentPrompToShow = .none + shouldTriggerReview = false + + let didShowAppReviewPrompt = dependencies[defaults: .standard, key: .didShowAppReviewPrompt] + + guard didShowAppReviewPrompt == false else { + // Skip triggers since it was already shown + return + } + + switch trigger { + case .pathScreenVisit: + let hasVisitedPathScreen = dependencies[defaults: .standard, key: .hasVisitedPathScreen] + + if !hasVisitedPathScreen { + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = true + + shouldTriggerReview = true + } + case .donateButtonPress: + let hasPressedDonate = dependencies[defaults: .standard, key: .hasDonated] + + if !hasPressedDonate { + dependencies[defaults: .standard, key: .hasDonated] = true + + shouldTriggerReview = true + } + case .themeChange: + let hasChangedTheme = dependencies[defaults: .standard, key: .hasChangedTheme] + + if !hasChangedTheme { + dependencies[defaults: .standard, key: .hasChangedTheme] = true + + shouldTriggerReview = true + } + } + } + + func shouldShowReviewModalNextTime() { + guard shouldTriggerReview else { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self, dependencies] in + self?.currentPrompToShow = .enjoyingSession + self?.shouldTriggerReview = false + + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + } + } + + func didExitAppReviewWithoutRating() { + + } + + // For testing purposes + func clearFlags() { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false + dependencies[defaults: .standard, key: .hasDonated] = false + dependencies[defaults: .standard, key: .hasChangedTheme] = false + } +} diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 08ff020888..49df5c938d 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -6,6 +6,7 @@ import SessionUIKit protocol AppReviewPromptDialogDelegate: AnyObject { func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) func didChangePromptState(_ state: AppReviewPromptState) + func didCloseBeforeReview() } class AppReviewPromptDialog: UIView { @@ -130,7 +131,14 @@ class AppReviewPromptDialog: UIView { @objc func close() { + let prevState = prompt + updatePrompt(.none) + + switch prevState { + case .rateSession: delegate?.didCloseBeforeReview() + default : break + } } @objc diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 7ffb59ecd0..325e35df0a 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -33,7 +33,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { preconditionFailure("Use init() instead.") } @@ -67,7 +67,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi return result }() - + private lazy var tableView: UITableView = { let result = UITableView() result.separatorStyle = .none @@ -89,7 +89,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi result.dataSource = self result.delegate = self result.sectionHeaderTopPadding = 0 - + return result }() @@ -103,7 +103,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi prompt.layer.borderWidth = 1 prompt.layer.cornerRadius = 12 prompt.themeBackgroundColor = .backgroundSecondary - + return prompt }() @@ -174,7 +174,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi innerShadowLayer.shadowOffset = .zero innerShadowLayer.shadowOpacity = 0.4 innerShadowLayer.shadowRadius = 2 - + let cutout: UIBezierPath = UIBezierPath( roundedRect: innerShadowLayer.bounds .insetBy(dx: innerShadowLayer.shadowRadius, dy: innerShadowLayer.shadowRadius), @@ -187,7 +187,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi path.append(cutout) innerShadowLayer.shadowPath = path.cgPath result.layer.addSublayer(innerShadowLayer) - + return result }() @@ -206,7 +206,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi instructionLabel.lineBreakMode = .byWordWrapping instructionLabel.numberOfLines = 0 - let result = UIStackView(arrangedSubviews: [ + let result = UIStackView(arrangedSubviews: [ emptyConvoLabel, UIView.vSpacer(Values.smallSpacing), instructionLabel @@ -264,7 +264,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi .localized() welcomeLabel.themeTextColor = .sessionButton_text welcomeLabel.textAlignment = .center - + let result = UIStackView(arrangedSubviews: [ image, accountCreatedLabel, @@ -377,6 +377,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi super.viewDidAppear(animated) viewModel.dependencies[singleton: .notificationsManager].scheduleSessionNetworkPageLocalNotifcation(force: false) + + // Show app review dialog when all flags are valid + viewModel.dependencies[singleton: .appReviewManager].shouldShowReviewModalNextTime() } // MARK: - Updating @@ -397,6 +400,14 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi .removeDuplicates() .sink { [weak self] state in self?.render(state: state) } .store(in: &disposables) + + viewModel + .dependencies[singleton: .appReviewManager] + .$currentPrompToShow + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink { [weak self] state in self?.appReviewPrompt.updatePrompt(state) } + .store(in: &disposables) } @MainActor private func render(state: HomeViewModel.State) { @@ -414,7 +425,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi tableViewTopConstraint?.isActive = false loadingConversationsLabelTopConstraint?.isActive = false seedReminderView.isHidden = !state.showViewedSeedBanner - + if state.showViewedSeedBanner { loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .bottom, of: seedReminderView, withInset: Values.mediumSpacing) tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView) @@ -429,19 +440,19 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Update the overall view state (loading, empty, or loaded) switch state.viewState { - case .loading: - loadingConversationsLabel.isHidden = false - emptyStateStackView.isHidden = true - - case .empty(let isNewUser): - loadingConversationsLabel.isHidden = true - emptyStateStackView.isHidden = false - accountCreatedView.isHidden = !isNewUser - emptyStateLogoView.isHidden = isNewUser - - case .loaded: - loadingConversationsLabel.isHidden = true - emptyStateStackView.isHidden = true + case .loading: + loadingConversationsLabel.isHidden = false + emptyStateStackView.isHidden = true + + case .empty(let isNewUser): + loadingConversationsLabel.isHidden = true + emptyStateStackView.isHidden = false + accountCreatedView.isHidden = !isNewUser + emptyStateLogoView.isHidden = isNewUser + + case .loaded: + loadingConversationsLabel.isHidden = true + emptyStateStackView.isHidden = true } // If we are still loading then don't try to load the table content (it'll be empty and we @@ -495,9 +506,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi profile: userProfile, profileIcon: { switch (serviceNetwork, forceOffline) { - case (.testnet, false): return .letter("T", false) // stringlint:ignore - case (.testnet, true): return .letter("T", true) // stringlint:ignore - default: return .none + case (.testnet, false): return .letter("T", false) // stringlint:ignore + case (.testnet, true): return .letter("T", true) // stringlint:ignore + default: return .none } }(), additionalProfile: nil, @@ -546,23 +557,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[indexPath.section] switch section.model { - case .messageRequests: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath) - cell.accessibilityIdentifier = "Message requests banner" - cell.isAccessibilityElement = true - cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0)) - return cell - - case .threads: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.update(with: threadViewModel, using: viewModel.dependencies) - cell.accessibilityIdentifier = "Conversation list item" - cell.accessibilityLabel = threadViewModel.displayName - return cell - - default: preconditionFailure("Other sections should have no content") + case .messageRequests: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath) + cell.accessibilityIdentifier = "Message requests banner" + cell.isAccessibilityElement = true + cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0)) + return cell + + case .threads: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) + cell.update(with: threadViewModel, using: viewModel.dependencies) + cell.accessibilityIdentifier = "Conversation list item" + cell.accessibilityLabel = threadViewModel.displayName + return cell + + default: preconditionFailure("Other sections should have no content") } } @@ -570,19 +581,19 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[section] switch section.model { - case .loadMore: - let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary - loadingIndicator.alpha = 0.5 - loadingIndicator.startAnimating() - - let view: UIView = UIView() - view.addSubview(loadingIndicator) - loadingIndicator.center(in: view) - - return view + case .loadMore: + let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) + loadingIndicator.themeTintColor = .textPrimary + loadingIndicator.alpha = 0.5 + loadingIndicator.startAnimating() + + let view: UIView = UIView() + view.addSubview(loadingIndicator) + loadingIndicator.center(in: view) - default: return nil + return view + + default: return nil } } @@ -592,41 +603,41 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[section] switch section.model { - case .loadMore: return HomeVC.loadingHeaderHeight - default: return 0 + case .loadMore: return HomeVC.loadingHeaderHeight + default: return 0 } } public func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { switch sections[section].model { - case .loadMore: self.viewModel.loadNextPage() - default: break + case .loadMore: self.viewModel.loadNextPage() + default: break } } - + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let section: HomeViewModel.SectionModel = sections[indexPath.section] switch section.model { - case .messageRequests: - let viewController: SessionTableViewController = SessionTableViewController( - viewModel: MessageRequestsViewModel(using: viewModel.dependencies) - ) - self.navigationController?.pushViewController(viewController, animated: true) - - case .threads: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let viewController: ConversationVC = ConversationVC( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - focusedInteractionInfo: nil, - using: viewModel.dependencies - ) - self.navigationController?.pushViewController(viewController, animated: true) - - default: break + case .messageRequests: + let viewController: SessionTableViewController = SessionTableViewController( + viewModel: MessageRequestsViewModel(using: viewModel.dependencies) + ) + self.navigationController?.pushViewController(viewController, animated: true) + + case .threads: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let viewController: ConversationVC = ConversationVC( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + focusedInteractionInfo: nil, + using: viewModel.dependencies + ) + self.navigationController?.pushViewController(viewController, animated: true) + + default: break } } @@ -647,32 +658,32 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] switch section.model { - case .threads: - // Cannot properly sync outgoing blinded message requests so don't provide the option, - // the 'Note to Self' conversation also doesn't support 'mark as unread' so don't - // provide it there either - guard - threadViewModel.threadVariant != .legacyGroup && + case .threads: + // Cannot properly sync outgoing blinded message requests so don't provide the option, + // the 'Note to Self' conversation also doesn't support 'mark as unread' so don't + // provide it there either + guard + threadViewModel.threadVariant != .legacyGroup && threadViewModel.threadId != threadViewModel.currentUserSessionId && ( threadViewModel.threadVariant != .contact || (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard ) - else { return nil } - - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [.toggleReadStatus], - for: .leading, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies - ) + else { return nil } + + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [.toggleReadStatus], + for: .leading, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) + ) - default: return nil + default: return nil } } @@ -681,76 +692,76 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] switch section.model { - case .messageRequests: - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [.hide], - for: .trailing, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies - ) + case .messageRequests: + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [.hide], + for: .trailing, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) - - case .threads: - let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) - - // Cannot properly sync outgoing blinded message requests so only provide valid options - let shouldHavePinAction: Bool = { - switch threadViewModel.threadVariant { - // Only allow unpin for legacy groups - case .legacyGroup: return threadViewModel.threadPinnedPriority > 0 - - default: - return ( - sessionIdPrefix != .blinded15 && - sessionIdPrefix != .blinded25 - ) - } - }() - let shouldHaveMuteAction: Bool = { - switch threadViewModel.threadVariant { - case .contact: return ( - !threadViewModel.threadIsNoteToSelf && - sessionIdPrefix != .blinded15 && - sessionIdPrefix != .blinded25 - ) - - case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) - - case .legacyGroup: return false - case .community: return true - } - }() - let destructiveAction: UIContextualAction.SwipeAction = { - switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember, threadViewModel.currentUserIsClosedGroupAdmin) { - case (.contact, true, _, _): return .hide - case (.group, _, true, false), (.community, _, _, _): return .leave - default: return .delete - } - }() - - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [ - (!shouldHavePinAction ? nil : .pin), - (!shouldHaveMuteAction ? nil : .mute), - destructiveAction - ].compactMap { $0 }, - for: .trailing, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies + ) + + case .threads: + let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) + + // Cannot properly sync outgoing blinded message requests so only provide valid options + let shouldHavePinAction: Bool = { + switch threadViewModel.threadVariant { + // Only allow unpin for legacy groups + case .legacyGroup: return threadViewModel.threadPinnedPriority > 0 + + default: + return ( + sessionIdPrefix != .blinded15 && + sessionIdPrefix != .blinded25 ) + } + }() + let shouldHaveMuteAction: Bool = { + switch threadViewModel.threadVariant { + case .contact: return ( + !threadViewModel.threadIsNoteToSelf && + sessionIdPrefix != .blinded15 && + sessionIdPrefix != .blinded25 ) - - default: return nil + + case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) + + case .legacyGroup: return false + case .community: return true + } + }() + let destructiveAction: UIContextualAction.SwipeAction = { + switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember, threadViewModel.currentUserIsClosedGroupAdmin) { + case (.contact, true, _, _): return .hide + case (.group, _, true, false), (.community, _, _, _): return .leave + default: return .delete + } + }() + + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [ + (!shouldHavePinAction ? nil : .pin), + (!shouldHaveMuteAction ? nil : .mute), + destructiveAction + ].compactMap { $0 }, + for: .trailing, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies + ) + ) + + default: return nil } } @@ -769,7 +780,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi present(targetViewController, animated: true, completion: nil) return } - + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) viewController.setNavBarTitle("sessionRecoveryPassword".localized()) self.navigationController?.pushViewController(viewController, animated: true) @@ -823,15 +834,22 @@ extension HomeVC: AppReviewPromptDialogDelegate { switch state { case .feedback: - print("LAUNCH SUMMARY") + appReviewPrompt.updatePrompt(.none) + + showSurveyAlert() case .rateSession: - print("SHOW APP RATING") + // TODO: Add review kit default: break } } + func didCloseBeforeReview() { + // TODO: - Add 2 weeks timer + } + func didChangePromptState(_ state: AppReviewPromptState) { + // Adjust insents so tableview can still be scrolled to bottom let originalBottomInsets = ( Values.largeSpacing + HomeVC.newConversationButtonSize + @@ -846,3 +864,52 @@ extension HomeVC: AppReviewPromptDialogDelegate { } } } + +// MARK: - Alert for survey +private extension HomeVC { + func showSurveyAlert() { + guard let url: URL = URL(string: Constants.feedback_url) else { return } + + var surverUrl: URL { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return url + } + + // stringlint:ignore_contents + components.queryItems = [ + .init(name: "platform", value: "iOS"), + .init(name: "version", value: viewModel.dependencies[cache: .appVersion].appVersion) + ] + + guard let finalURL = components.url else { return url } + + return finalURL + } + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "urlOpen".localized(), + body: .attributedText( + "urlOpenDescription" + .put(key: "url", value: url.absoluteString) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + ), + confirmTitle: "open".localized(), + confirmStyle: .danger, + cancelTitle: "urlCopy".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + onConfirm: { modal in + UIApplication.shared.open(surverUrl, options: [:], completionHandler: nil) + modal.dismiss(animated: true) + }, + onCancel: { modal in + UIPasteboard.general.string = surverUrl.absoluteString + + modal.dismiss(animated: true) + } + ) + ) + + present(modal, animated: true) + } +} diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index ab37d115f3..0bf717d3cf 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -72,6 +72,8 @@ final class PathVC: BaseVC { setUpNavBar() setUpViewHierarchy() + + dependencies[singleton: .appReviewManager].triggerReview(for: .pathScreenVisit) } private func setUpNavBar() { diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index ab4d1dda1f..aa378e2f0c 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -149,6 +149,12 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ ) } + @MainActor private func didUpdateTheme(theme: Theme?) { + ThemeManager.updateThemeState(theme: theme) + + dependencies[singleton: .appReviewManager].triggerReview(for: .themeChange) + } + private static func sections( state: State, previousState: State, @@ -167,8 +173,8 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ trailingAccessory: .radio( isSelected: (state.theme == theme) ), - onTap: { - ThemeManager.updateThemeState(theme: theme) + onTap: { [weak viewModel] in + viewModel?.didUpdateTheme(theme: theme) } ) } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index d5bd3b0d26..d080457b0b 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -849,6 +849,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) self.transitionToScreen(modal, transitionType: .present) + + // Mark app review flag that donate button was tapped + dependencies[singleton: .appReviewManager].triggerReview(for: .donateButtonPress) } private func openTokenUrl() { diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index ba9fe47fd6..ce6b9f1022 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -16,4 +16,5 @@ public enum Constants { public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" public static let store_variant: String = "App Store" + public static let feedback_url: String = "https://getsession.org/feedback" } diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 34e12df7b7..1ffc3e34cb 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -165,6 +165,18 @@ public extension UserDefaults.BoolKey { /// Indicates whether the local notification for token bonus is scheduled static let isSessionNetworkPageNotificationScheduled: UserDefaults.BoolKey = "isSessionNetworkPageNotificationScheduled" + + /// Indicates whether the user visited the Path screen + static let hasVisitedPathScreen: UserDefaults.BoolKey = "hasVisitedPathScreen" + + /// Indicates whether the user changed the app theme + static let hasChangedTheme: UserDefaults.BoolKey = "hasChangedTheme" + + /// Indicates whether the user pressed the donate button + static let hasDonated: UserDefaults.BoolKey = "hasDonated" + + /// Indicates wheter app has already presented the user the app review prompt dialog + static let didShowAppReviewPrompt: UserDefaults.BoolKey = "didShowAppReviewPrompt" } public extension UserDefaults.DateKey { From acd96c50c20a9d0bc4948e9670f61c9e27c57547 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 14 Aug 2025 14:12:58 +0800 Subject: [PATCH 076/244] Fix wrong components tagged with accessibility --- Session/Settings/AppIconViewModel.swift | 3 --- Session/Settings/Views/AppIconGridView.swift | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 122cfe1af9..ff46be2ef0 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -171,9 +171,6 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl onChange: { icon in self?.updateAppIcon(icon) } ) ), - accessibility: Accessibility( - identifier: AppIcon(name: current).accessibilityIdentifier - ) ) ] ) diff --git a/Session/Settings/Views/AppIconGridView.swift b/Session/Settings/Views/AppIconGridView.swift index b135e5b13c..d5b9da3d89 100644 --- a/Session/Settings/Views/AppIconGridView.swift +++ b/Session/Settings/Views/AppIconGridView.swift @@ -24,7 +24,11 @@ final class AppIconGridView: UIView { private let contentView: UIView = UIView() private lazy var iconViews: [IconView] = icons.map { icon in - IconView(icon: icon) { [weak self] in self?.onChange?(icon) } + let view = IconView(icon: icon) { [weak self] in self?.onChange?(icon) } + view.accessibilityIdentifier = icon.accessibilityIdentifier + view.isAccessibilityElement = true + + return view } // MARK: - Initializtion From 0ad96e4c51ace4ecbc6c996ee5b8b5f3d6b96daf Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 14 Aug 2025 15:25:57 +0800 Subject: [PATCH 077/244] Added retry schedule and store kit review call --- .../Home/App Review/AppReviewManager.swift | 36 +++++++++++++++++-- .../View/AppReviewPromptDialog.swift | 16 +++------ Session/Home/HomeVC.swift | 29 ++++++++------- .../Types/UserDefaultsType.swift | 6 ++++ 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/Session/Home/App Review/AppReviewManager.swift b/Session/Home/App Review/AppReviewManager.swift index d5c6593f75..fb378101dd 100644 --- a/Session/Home/App Review/AppReviewManager.swift +++ b/Session/Home/App Review/AppReviewManager.swift @@ -2,6 +2,7 @@ import UIKit import SessionUtilitiesKit +import StoreKit public extension Singleton { static let appReviewManager: SingletonConfig = Dependencies.create( @@ -22,6 +23,7 @@ public class AppReviewManager: NSObject, ObservableObject { @Published var currentPrompToShow: AppReviewPromptState = .none + private var pendingPrompt: AppReviewPromptState? = nil private var shouldTriggerReview: Bool = false // MARK: - Initialization @@ -31,6 +33,18 @@ public class AppReviewManager: NSObject, ObservableObject { super.init() } + func checkIfCanRetryAppReview() { + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], Date() >= retryDate { + pendingPrompt = .rateSession + shouldTriggerReview = true + + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + } + } + func triggerReview(for trigger: AppReviewTrigger) { currentPrompToShow = .none shouldTriggerReview = false @@ -74,15 +88,29 @@ public class AppReviewManager: NSObject, ObservableObject { guard shouldTriggerReview else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self, dependencies] in - self?.currentPrompToShow = .enjoyingSession + self?.currentPrompToShow = self?.pendingPrompt ?? .enjoyingSession self?.shouldTriggerReview = false dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + self?.pendingPrompt = nil } } - - func didExitAppReviewWithoutRating() { + func willSubmitAppReview() { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + + if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + SKStoreReviewController.requestReview(in: scene) + } + } + + func scheduleAppReviewRetry() { + guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: Date()) else { + return + } + + dependencies[defaults: .standard, key: .rateAppRetryDate] = retryDate } // For testing purposes @@ -91,5 +119,7 @@ public class AppReviewManager: NSObject, ObservableObject { dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false dependencies[defaults: .standard, key: .hasDonated] = false dependencies[defaults: .standard, key: .hasChangedTheme] = false + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 } } diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 49df5c938d..d92140779d 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -6,7 +6,6 @@ import SessionUIKit protocol AppReviewPromptDialogDelegate: AnyObject { func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) func didChangePromptState(_ state: AppReviewPromptState) - func didCloseBeforeReview() } class AppReviewPromptDialog: UIView { @@ -131,23 +130,18 @@ class AppReviewPromptDialog: UIView { @objc func close() { - let prevState = prompt - updatePrompt(.none) - - switch prevState { - case .rateSession: delegate?.didCloseBeforeReview() - default : break - } } @objc func primaryEvent() { - switch prompt { - case .enjoyingSession: + let current = prompt + + if current == .enjoyingSession { updatePrompt(.rateSession) - default: delegate?.willHandlePromptState(prompt, isPrimary: true) } + + delegate?.willHandlePromptState(current, isPrimary: true) } @objc diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 325e35df0a..440774ef7e 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -369,6 +369,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Onion request path countries cache viewModel.dependencies.warmCache(cache: .ip2Country) + // Check app review for rating retry + viewModel.dependencies[singleton: .appReviewManager].checkIfCanRetryAppReview() + // Bind the UI to the view model bindViewModel() } @@ -827,27 +830,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi extension HomeVC: AppReviewPromptDialogDelegate { func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) { - if !isPrimary { - appReviewPrompt.updatePrompt(.none) + guard state != .enjoyingSession else { + if isPrimary { viewModel.dependencies[singleton: .appReviewManager].scheduleAppReviewRetry() } + return } - switch state { - case .feedback: - appReviewPrompt.updatePrompt(.none) - - showSurveyAlert() - case .rateSession: - // TODO: Add review kit - default: - break + appReviewPrompt.updatePrompt(.none) + + if isPrimary { + switch state { + case .feedback: showSurveyAlert() + case .rateSession: viewModel.dependencies[singleton: .appReviewManager].willSubmitAppReview() + default: break + } } } - func didCloseBeforeReview() { - // TODO: - Add 2 weeks timer - } - func didChangePromptState(_ state: AppReviewPromptState) { // Adjust insents so tableview can still be scrolled to bottom let originalBottomInsets = ( diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 1ffc3e34cb..f1ce627d3f 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -192,6 +192,9 @@ public extension UserDefaults.DateKey { /// The date/time when we received a call pre-offer (used to suppress call notifications which are too old) static let lastCallPreOffer: UserDefaults.DateKey = "lastCallPreOffer" + + /// The date/time when app review prompt will appear again + static let rateAppRetryDate: UserDefaults.DateKey = "rateAppRetryDate" } public extension UserDefaults.DoubleKey { @@ -208,6 +211,9 @@ public extension UserDefaults.IntKey { /// The id of the message that was just shared to static let lastSharedMessageId: UserDefaults.IntKey = "lastSharedMessageId" + + /// The number of attempts made to retry showing of app rating prompt + static let rateAppRetryAttemptCount: UserDefaults.IntKey = "rateAppRetryAttemptCount" } public extension UserDefaults.StringKey { From aaf5d08b67e874723299da1d127f27d44eb7f355 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 15 Aug 2025 11:52:25 +1000 Subject: [PATCH 078/244] Fix build error on old Xcode --- Session/Settings/AppIconViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index ff46be2ef0..c0cf0f2da8 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -170,7 +170,7 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl selectedIcon: AppIcon(name: current), onChange: { icon in self?.updateAppIcon(icon) } ) - ), + ) ) ] ) From 4f9d87d4f3edf0df09ab95dd4a57e6c6e3a1b092 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 15 Aug 2025 10:34:05 +0800 Subject: [PATCH 079/244] Fix incorrectly selecting image when scrolling thru collection --- .../ImagePickerController.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index a7ea443621..7661dacac8 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -558,6 +558,20 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate { return true } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let pan = gestureRecognizer as? UIPanGestureRecognizer { + let velocity = pan.velocity(in: collectionView) + + // Check the horizontal movement is greater than vertical then + // treat it as a "selection" pan rather than a "scroll". + // Vertical velocity == scrolling the list. + // Horizontal velocity == less common for scrolling (do panning multi select) + return abs(velocity.x) > abs(velocity.y) + } + + return true + } } protocol TitleViewDelegate: AnyObject { From bacd6a494b8b0658a5bb861703deb7e5f5fa9f69 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 15 Aug 2025 13:19:54 +1000 Subject: [PATCH 080/244] Fixed an issue where the 'video' badge was incorrectly removed --- .../PhotoGridViewCell.swift | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/Session/Media Viewing & Editing/PhotoGridViewCell.swift b/Session/Media Viewing & Editing/PhotoGridViewCell.swift index 39f12ded9f..b687238049 100644 --- a/Session/Media Viewing & Editing/PhotoGridViewCell.swift +++ b/Session/Media Viewing & Editing/PhotoGridViewCell.swift @@ -12,13 +12,51 @@ public protocol PhotoGridItem: AnyObject { } public class PhotoGridViewCell: UICollectionViewCell { - public let imageView: SessionImageView - - private let contentTypeBadgeView: UIImageView - private let selectedBadgeView: UIImageView + private static let badgeSize: CGSize = CGSize(width: 32, height: 32) + public let imageView: SessionImageView = { + let result: SessionImageView = SessionImageView() + result.contentMode = .scaleAspectFill + + return result + }() - private let highlightedView: UIView - private let selectedView: UIView + private let contentTypeBadgeView: UIImageView = { + let result: UIImageView = UIImageView() + result.image = UIImage(named: "ic_gallery_badge_video") + result.isHidden = true + + return result + }() + + private let selectedBadgeView: UIImageView = { + let result: UIImageView = UIImageView() + result.image = UIImage(systemName: "checkmark.circle.fill")?.withRenderingMode(.alwaysTemplate) + result.themeTintColor = .primary + result.themeBorderColor = .textPrimary + result.themeBackgroundColor = .textPrimary + result.isHidden = true + result.layer.cornerRadius = (PhotoGridViewCell.badgeSize.width / 2) + + return result + }() + + private let highlightedView: UIView = { + let result: UIView = UIView() + result.alpha = 0.2 + result.themeBackgroundColor = .black + result.isHidden = true + + return result + }() + + private let selectedView: UIView = { + let result: UIView = UIView() + result.alpha = 0.3 + result.themeBackgroundColor = .black + result.isHidden = true + + return result + }() var item: PhotoGridItem? @@ -38,31 +76,6 @@ public class PhotoGridViewCell: UICollectionViewCell { } override init(frame: CGRect) { - self.imageView = SessionImageView() - imageView.contentMode = .scaleAspectFill - - self.contentTypeBadgeView = UIImageView() - contentTypeBadgeView.isHidden = true - - let kSelectedBadgeSize = CGSize(width: 32, height: 32) - self.selectedBadgeView = UIImageView() - selectedBadgeView.image = UIImage(named: "ic_gallery_badge_video")?.withRenderingMode(.alwaysTemplate) - selectedBadgeView.themeTintColor = .primary - selectedBadgeView.themeBorderColor = .textPrimary - selectedBadgeView.themeBackgroundColor = .textPrimary - selectedBadgeView.isHidden = true - selectedBadgeView.layer.cornerRadius = (kSelectedBadgeSize.width / 2) - - self.highlightedView = UIView() - highlightedView.alpha = 0.2 - highlightedView.themeBackgroundColor = .black - highlightedView.isHidden = true - - self.selectedView = UIView() - selectedView.alpha = 0.3 - selectedView.themeBackgroundColor = .black - selectedView.isHidden = true - super.init(frame: frame) self.clipsToBounds = true @@ -86,8 +99,8 @@ public class PhotoGridViewCell: UICollectionViewCell { selectedBadgeView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.verySmallSpacing) selectedBadgeView.pin(.bottom, to: .bottom, of: contentView, withInset: -Values.verySmallSpacing) - selectedBadgeView.set(.width, to: kSelectedBadgeSize.width) - selectedBadgeView.set(.height, to: kSelectedBadgeSize.height) + selectedBadgeView.set(.width, to: PhotoGridViewCell.badgeSize.width) + selectedBadgeView.set(.height, to: PhotoGridViewCell.badgeSize.height) } @available(*, unavailable, message: "Unimplemented") @@ -110,7 +123,6 @@ public class PhotoGridViewCell: UICollectionViewCell { super.prepareForReuse() self.item = nil - self.imageView.image = nil self.contentTypeBadgeView.isHidden = true self.highlightedView.isHidden = true self.selectedView.isHidden = true From 08dda6be716439307444d21e10f62bddb35dc908 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 15 Aug 2025 11:50:51 +0800 Subject: [PATCH 081/244] Fix accessory labels expansion causing squashed content --- Session/Shared/Views/SessionCell+AccessoryView.swift | 1 + Session/Shared/Views/SessionCell.swift | 3 +++ Session/Shared/Views/SessionHighlightingBackgroundLabel.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 2cae2e3c31..8d7c9ee551 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -397,6 +397,7 @@ extension SessionCell { label.themeTextColor = .textPrimary label.setContentHugging(to: .required) label.setCompressionResistance(to: .required) + label.numberOfLines = 0 result.addArrangedSubview(imageView) result.addArrangedSubview(label) diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 4bcc1408f3..47bdebb394 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -221,6 +221,9 @@ public class SessionCell: UITableViewCell { botSeparatorTrailingConstraint = botSeparator.pin(.trailing, to: .trailing, of: cellBackgroundView) botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView) + // Limit accessory views horizontal expansion to 40% of the container + trailingAccessoryView.set(.width, lessThanOrEqualTo: .width, of: contentView, multiplier: 0.40) + // Explicitly call this to ensure we have initialised the constraints before we initially // layout (if we don't do this then some constraints get created for the first time when // updating the cell before the `isActive` value gets set, resulting in breaking constriants) diff --git a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift index a4e1972111..7e6d6ead22 100644 --- a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift +++ b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift @@ -24,6 +24,7 @@ public class SessionHighlightingBackgroundLabel: UIView { result.themeTextColor = .textPrimary result.setContentHugging(to: .required) result.setCompressionResistance(to: .required) + result.numberOfLines = 0 return result }() From ee7f8db266cec75ca709ebcaac5c1d5d4dce4085 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 15 Aug 2025 15:21:45 +0800 Subject: [PATCH 082/244] Fix unresponsive textfield due to small hit area --- SessionUIKit/Components/SwiftUI/SessionTextField.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SessionUIKit/Components/SwiftUI/SessionTextField.swift b/SessionUIKit/Components/SwiftUI/SessionTextField.swift index b83b95b417..e4e802304c 100644 --- a/SessionUIKit/Components/SwiftUI/SessionTextField.swift +++ b/SessionUIKit/Components/SwiftUI/SessionTextField.swift @@ -11,6 +11,8 @@ public struct SessionTextField: View where ExplanationView: Vie @State var textThemeColor: ThemeValue = .textPrimary @State fileprivate var textChanged: ((String) -> Void)? + @FocusState private var isFirstResponder: Bool + public enum SessionTextFieldType { case thin case normal @@ -83,6 +85,8 @@ public struct SessionTextField: View where ExplanationView: Vie .font(font) .foregroundColor(themeColor: textThemeColor) .accessibility(self.accessibility) + .focused($isFirstResponder) + } else { ZStack { TextEditor(text: $text) @@ -92,6 +96,7 @@ public struct SessionTextField: View where ExplanationView: Vie .accessibility(self.accessibility) .frame(maxHeight: self.height) .padding(.all, -4) + .focused($isFirstResponder) // FIXME: This is a workaround for dynamic height of the TextEditor. Text(text.isEmpty ? placeholder : text) @@ -117,6 +122,8 @@ public struct SessionTextField: View where ExplanationView: Vie RoundedRectangle(cornerRadius: self.cornerRadius) .stroke(themeColor: isErrorMode ? .danger : .borderSeparator) ) + .contentShape(RoundedRectangle(cornerRadius: self.cornerRadius)) + .onTapGesture { isFirstResponder = !isFirstResponder } // Added hit test to launch keyboard, currently textfield's hit area is too small .onChange(of: text) { newText in error = inputChecker?(newText) textThemeColor = ((newText == lastErroredText || error?.isEmpty == false) ? .danger : .textPrimary) @@ -127,7 +134,7 @@ public struct SessionTextField: View where ExplanationView: Vie textThemeColor = .danger } } - + // Error message switch self.type { case .thin: From fabad55305169315037c6d8004a79edb2192baf6 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 15 Aug 2025 15:26:00 +0800 Subject: [PATCH 083/244] Disable hit test on placeholder label --- SessionUIKit/Components/SwiftUI/SessionTextField.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SessionUIKit/Components/SwiftUI/SessionTextField.swift b/SessionUIKit/Components/SwiftUI/SessionTextField.swift index e4e802304c..44288b0b4b 100644 --- a/SessionUIKit/Components/SwiftUI/SessionTextField.swift +++ b/SessionUIKit/Components/SwiftUI/SessionTextField.swift @@ -74,6 +74,7 @@ public struct SessionTextField: View where ExplanationView: Vie Text(placeholder) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .textSecondary) + .allowsHitTesting(false) } if #available(iOS 16.0, *) { From 2c3afa9693b1c8d148cd66152c426726b3269733 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 18 Aug 2025 15:10:56 +0800 Subject: [PATCH 084/244] Updated expiresStartedAtMs when message has been sent successfully Disable countdown timer when message is not yet sent --- .../Context Menu/ContextMenuVC+Action.swift | 12 +++++++++- .../ContextMenuVC+ActionView.swift | 22 ++++++++++++++----- .../MessageSender+Convenience.swift | 12 +++++++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 15d0c0a8b9..a76e50b43c 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -10,6 +10,7 @@ extension ContextMenuVC { struct ExpirationInfo { let expiresStartedAtMs: Double? let expiresInSeconds: TimeInterval? + let canCountdown: Bool } struct Action { @@ -103,12 +104,21 @@ extension ContextMenuVC { } static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { + + let canCountdown: Bool = { + guard cellViewModel.variant.isOutgoing else { return true } + + let state = cellViewModel.state + return state != .sending && state != .failed + }() + return Action( icon: Lucide.image(icon: .trash2, size: 24), title: "delete".localized(), expirationInfo: ExpirationInfo( expiresStartedAtMs: cellViewModel.expiresStartedAtMs, - expiresInSeconds: cellViewModel.expiresInSeconds + expiresInSeconds: cellViewModel.expiresInSeconds, + canCountdown: canCountdown ), themeColor: .danger, shouldDismissInfoScreen: true, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 54ae63cbc3..638c3752a2 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -119,9 +119,10 @@ extension ContextMenuVC { } private func setUpSubtitle() { - guard - let expiresInSeconds = self.action.expirationInfo?.expiresInSeconds, - let expiresStartedAtMs = self.action.expirationInfo?.expiresStartedAtMs + guard + let expirationInfo = self.action.expirationInfo, + let expiresInSeconds = expirationInfo.expiresInSeconds, + let expiresStartedAtMs = expirationInfo.expiresStartedAtMs else { subtitleLabel.isHidden = true subtitleWidthConstraint.isActive = false @@ -130,12 +131,23 @@ extension ContextMenuVC { subtitleLabel.isHidden = false subtitleWidthConstraint.isActive = true + // To prevent a negative timer - let timeToExpireInSeconds: TimeInterval = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) + var timeToExpireInSeconds: TimeInterval { + // If canCountdown = false, use base expiration timer value + guard expirationInfo.canCountdown else { + return expiresInSeconds + } + + return max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) + } + subtitleLabel.text = "disappearingMessagesCountdownBigMobile" - .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits)) + .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits, minimumUnit: expirationInfo.canCountdown ? .second : .minute)) .localized() + guard expirationInfo.canCountdown else { return } + timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, using: dependencies, block: { [weak self, dependencies] _ in let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000 if timeToExpireInSeconds <= 0 { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index bb681dd1b5..fd3e8e8c03 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -209,6 +209,15 @@ extension MessageSender { try interaction.with(state: .sent).update(db) case (true, .syncMessage), (_, .contact), (_, .closedGroup), (_, .openGroup), (_, .openGroupInbox): + // The timestamp to use for scheduling message deletion. This is generated + // when the message is successfully sent to ensure the deletion timer starts + // from the correct time. + var scheduledTimestampForDeletion: Double? { + guard interaction.isExpiringMessage else { return nil } + let sentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return Double(sentTimestampMs) + } + try interaction.with( serverHash: message.serverHash, // Track the open group server message ID and update server timestamp (use server @@ -218,6 +227,7 @@ extension MessageSender { nil : serverTimestampMs.map { Int64($0) } ), + expiresStartedAtMs: scheduledTimestampForDeletion, // Updates the expiresStartedAtMs value when message is marked as sent openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }, state: .sent ).update(db) @@ -240,7 +250,7 @@ extension MessageSender { if case .syncMessage = destination, - let startedAtMs: Double = interaction.expiresStartedAtMs, + let startedAtMs: Double = scheduledTimestampForDeletion, let expiresInSeconds: TimeInterval = interaction.expiresInSeconds, let serverHash: String = message.serverHash { From 340d15e3582b4c665f3a54106ad2157c9b4aca21 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 19 Aug 2025 14:30:06 +1000 Subject: [PATCH 085/244] Fixed an issue where gif images would be scaled down --- .../Media Viewing & Editing/MediaMessageView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 30dea77793..1e732e5f1b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -322,6 +322,7 @@ public class MediaMessageView: UIView { titleStackView.addArrangedSubview(subtitleLabel) imageView.alpha = 1 + imageView.set(.width, to: .width, of: stackView) imageView.addSubview(fileTypeImageView) // Type-specific configurations From 9fc2ab122f5e043ae2459ebbd418ce72c475f7a8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 20 Aug 2025 09:15:59 +1000 Subject: [PATCH 086/244] Bumped build number --- Session.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a90617efad..693291d1cf 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -659,8 +659,8 @@ FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; - FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */; }; FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3937072E4AD3F800571F17 /* NoopDependency.swift */; }; + FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; @@ -1984,8 +1984,8 @@ FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; - FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; FD3937072E4AD3F800571F17 /* NoopDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopDependency.swift; sourceTree = ""; }; + FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -8156,7 +8156,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 624; + CURRENT_PROJECT_VERSION = 625; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8237,7 +8237,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 624; + CURRENT_PROJECT_VERSION = 625; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8718,7 +8718,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 624; + CURRENT_PROJECT_VERSION = 625; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9305,7 +9305,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 624; + CURRENT_PROJECT_VERSION = 625; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From 93e37d61f15d147c6288edd1ca1ce3bbc1dd9fbb Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 20 Aug 2025 10:48:39 +0800 Subject: [PATCH 087/244] Reworked app review prompt presentation and handlers logic --- Session.xcodeproj/project.pbxproj | 4 - .../Home/App Review/AppReviewManager.swift | 125 --------------- .../App Review/AppReviewPromptModel.swift | 3 - .../View/AppReviewPromptDialog.swift | 46 +++--- Session/Home/HomeVC.swift | 149 +++++------------ Session/Home/HomeViewModel.swift | 151 +++++++++++++++++- Session/Path/PathVC.swift | 4 +- Session/Settings/AppearanceViewModel.swift | 11 +- Session/Settings/SettingsViewModel.swift | 4 +- .../Types/UserDefaultsType.swift | 2 +- 10 files changed, 226 insertions(+), 273 deletions(-) delete mode 100644 Session/Home/App Review/AppReviewManager.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 49e81512f7..5f7dc947f0 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1157,7 +1157,6 @@ FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; - FEAB06002E4D7D5D0006237B /* AppReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */; }; FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; }; FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; }; /* End PBXBuildFile section */ @@ -2431,7 +2430,6 @@ FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; - FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewManager.swift; sourceTree = ""; }; FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = ""; }; FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -5136,7 +5134,6 @@ children = ( FED288F42E4C3B5A00C31171 /* View */, FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */, - FEAB05FF2E4D7D5D0006237B /* AppReviewManager.swift */, ); path = "App Review"; sourceTree = ""; @@ -6875,7 +6872,6 @@ 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */, 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, - FEAB06002E4D7D5D0006237B /* AppReviewManager.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, FDEF57242C3CF04700131302 /* (null) in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, diff --git a/Session/Home/App Review/AppReviewManager.swift b/Session/Home/App Review/AppReviewManager.swift deleted file mode 100644 index fb378101dd..0000000000 --- a/Session/Home/App Review/AppReviewManager.swift +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SessionUtilitiesKit -import StoreKit - -public extension Singleton { - static let appReviewManager: SingletonConfig = Dependencies.create( - identifier: "appReviewManager", - createInstance: { dependencies in AppReviewManager(using: dependencies) } - ) -} - -public enum AppReviewTrigger { - case pathScreenVisit - case donateButtonPress - case themeChange -} - -// MARK: - AppReviewManager -public class AppReviewManager: NSObject, ObservableObject { - private let dependencies: Dependencies - - @Published var currentPrompToShow: AppReviewPromptState = .none - - private var pendingPrompt: AppReviewPromptState? = nil - private var shouldTriggerReview: Bool = false - - // MARK: - Initialization - fileprivate init(using dependencies: Dependencies) { - self.dependencies = dependencies - - super.init() - } - - func checkIfCanRetryAppReview() { - let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - - if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], Date() >= retryDate { - pendingPrompt = .rateSession - shouldTriggerReview = true - - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 - } - } - - func triggerReview(for trigger: AppReviewTrigger) { - currentPrompToShow = .none - shouldTriggerReview = false - - let didShowAppReviewPrompt = dependencies[defaults: .standard, key: .didShowAppReviewPrompt] - - guard didShowAppReviewPrompt == false else { - // Skip triggers since it was already shown - return - } - - switch trigger { - case .pathScreenVisit: - let hasVisitedPathScreen = dependencies[defaults: .standard, key: .hasVisitedPathScreen] - - if !hasVisitedPathScreen { - dependencies[defaults: .standard, key: .hasVisitedPathScreen] = true - - shouldTriggerReview = true - } - case .donateButtonPress: - let hasPressedDonate = dependencies[defaults: .standard, key: .hasDonated] - - if !hasPressedDonate { - dependencies[defaults: .standard, key: .hasDonated] = true - - shouldTriggerReview = true - } - case .themeChange: - let hasChangedTheme = dependencies[defaults: .standard, key: .hasChangedTheme] - - if !hasChangedTheme { - dependencies[defaults: .standard, key: .hasChangedTheme] = true - - shouldTriggerReview = true - } - } - } - - func shouldShowReviewModalNextTime() { - guard shouldTriggerReview else { return } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self, dependencies] in - self?.currentPrompToShow = self?.pendingPrompt ?? .enjoyingSession - self?.shouldTriggerReview = false - - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - self?.pendingPrompt = nil - } - } - - func willSubmitAppReview() { - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 - - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - SKStoreReviewController.requestReview(in: scene) - } - } - - func scheduleAppReviewRetry() { - guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: Date()) else { - return - } - - dependencies[defaults: .standard, key: .rateAppRetryDate] = retryDate - } - - // For testing purposes - func clearFlags() { - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false - dependencies[defaults: .standard, key: .hasDonated] = false - dependencies[defaults: .standard, key: .hasChangedTheme] = false - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 - } -} diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index be33ee2dcc..4fb183e058 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -15,7 +15,6 @@ enum AppReviewPromptState { case enjoyingSession case rateSession case feedback - case none } extension AppReviewPromptState { @@ -57,8 +56,6 @@ extension AppReviewPromptState { primaryButtonTitle: "openSurvey".localized(), secondaryButtonTitle: "notNow".localized() ) - case .none: - return .init(title: "", message: "", primaryButtonTitle: "", secondaryButtonTitle: "") } } } diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index d92140779d..9574e6749c 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -3,13 +3,10 @@ import UIKit import SessionUIKit -protocol AppReviewPromptDialogDelegate: AnyObject { - func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) - func didChangePromptState(_ state: AppReviewPromptState) -} - class AppReviewPromptDialog: UIView { - weak var delegate: AppReviewPromptDialogDelegate? + var onCloseTapped: (() -> Void)? + var onPrimaryTapped: ((AppReviewPromptState) -> Void)? + var onSecondaryTapped: ((AppReviewPromptState) -> Void)? private lazy var closeButton: UIButton = { let button = UIButton(type: .custom) @@ -101,55 +98,58 @@ class AppReviewPromptDialog: UIView { return stack }() - private var prompt: AppReviewPromptState = .none + private var prompt: AppReviewPromptState? override init(frame: CGRect) { super.init(frame: frame) setupHierarchy() setupLayout() + + setReviewPrompt(nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func updatePrompt(_ prompt: AppReviewPromptState) { + func setReviewPrompt(_ prompt: AppReviewPromptState?) { self.prompt = prompt - isHidden = prompt == .none + isHidden = prompt == nil - titleLabel.text = prompt.promptContent.title - messageLabel.text = prompt.promptContent.message + titleLabel.text = prompt?.promptContent.title ?? "" + messageLabel.text = prompt?.promptContent.message ?? "" - primaryButton.setTitle(prompt.promptContent.primaryButtonTitle, for: .normal) - secondaryButton.setTitle(prompt.promptContent.secondaryButtonTitle, for: .normal) - - delegate?.didChangePromptState(prompt) + primaryButton.setTitle(prompt?.promptContent.primaryButtonTitle, for: .normal) + secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) } @objc func close() { - updatePrompt(.none) + setReviewPrompt(nil) } @objc func primaryEvent() { - let current = prompt + let current = prompt ?? .enjoyingSession if current == .enjoyingSession { - updatePrompt(.rateSession) + setReviewPrompt(.rateSession) } - - delegate?.willHandlePromptState(current, isPrimary: true) + + onPrimaryTapped?(current) } @objc func secondaryEvent() { - switch prompt { - case .enjoyingSession: updatePrompt(.feedback) - default: delegate?.willHandlePromptState(prompt, isPrimary: false) + let current = prompt ?? .enjoyingSession + + if current == .enjoyingSession { + setReviewPrompt(.feedback) } + + onSecondaryTapped?(current) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 440774ef7e..d5ea25178d 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -95,8 +95,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi private lazy var appReviewPrompt: AppReviewPromptDialog = { let prompt = AppReviewPromptDialog() - prompt.delegate = self - prompt.translatesAutoresizingMaskIntoConstraints = false // Layers prompt.themeBorderColor = .borderSeparator @@ -348,7 +346,8 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi newConversationButton.center(.horizontal, in: view) newConversationButton.pin(.bottom, to: .bottom, of: view.safeAreaLayoutGuide, withInset: -Values.smallSpacing) - appReviewPrompt.updatePrompt(.none) + appReviewPrompt.onPrimaryTapped = { [weak self] state in self?.onHandlePrimaryTappedForState(state) } + appReviewPrompt.onSecondaryTapped = { [weak self] in self?.onHandleSecondayTappedForState($0) } // Preview prompt view.addSubview(appReviewPrompt) @@ -369,11 +368,10 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Onion request path countries cache viewModel.dependencies.warmCache(cache: .ip2Country) - // Check app review for rating retry - viewModel.dependencies[singleton: .appReviewManager].checkIfCanRetryAppReview() - // Bind the UI to the view model bindViewModel() + + viewModel.navigatableState.setupBindings(viewController: self, disposables: &disposables) } public override func viewDidAppear(_ animated: Bool) { @@ -381,8 +379,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi viewModel.dependencies[singleton: .notificationsManager].scheduleSessionNetworkPageLocalNotifcation(force: false) - // Show app review dialog when all flags are valid - viewModel.dependencies[singleton: .appReviewManager].shouldShowReviewModalNextTime() + viewModel.viewDidAppear() } // MARK: - Updating @@ -403,14 +400,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi .removeDuplicates() .sink { [weak self] state in self?.render(state: state) } .store(in: &disposables) - - viewModel - .dependencies[singleton: .appReviewManager] - .$currentPrompToShow - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [weak self] state in self?.appReviewPrompt.updatePrompt(state) } - .store(in: &disposables) } @MainActor private func render(state: HomeViewModel.State) { @@ -443,19 +432,19 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Update the overall view state (loading, empty, or loaded) switch state.viewState { - case .loading: - loadingConversationsLabel.isHidden = false - emptyStateStackView.isHidden = true - - case .empty(let isNewUser): - loadingConversationsLabel.isHidden = true - emptyStateStackView.isHidden = false - accountCreatedView.isHidden = !isNewUser - emptyStateLogoView.isHidden = isNewUser - - case .loaded: - loadingConversationsLabel.isHidden = true - emptyStateStackView.isHidden = true + case .loading: + loadingConversationsLabel.isHidden = false + emptyStateStackView.isHidden = true + + case .empty(let isNewUser): + loadingConversationsLabel.isHidden = true + emptyStateStackView.isHidden = false + accountCreatedView.isHidden = !isNewUser + emptyStateLogoView.isHidden = isNewUser + + case .loaded: + loadingConversationsLabel.isHidden = true + emptyStateStackView.isHidden = true } // If we are still loading then don't try to load the table content (it'll be empty and we @@ -487,6 +476,12 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi ) { [weak self] updatedData in self?.sections = updatedData } + + // App reivew + if let promptState = state.appReviewPromptState, state.appReviewPromptTimestamp != nil { + appReviewPrompt.setReviewPrompt(promptState) + viewModel.dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + } } private func updateNavBarButtons( @@ -828,87 +823,27 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi } } -extension HomeVC: AppReviewPromptDialogDelegate { - func willHandlePromptState(_ state: AppReviewPromptState, isPrimary: Bool) { - guard state != .enjoyingSession else { - if isPrimary { viewModel.dependencies[singleton: .appReviewManager].scheduleAppReviewRetry() } - - return - } - - appReviewPrompt.updatePrompt(.none) - - if isPrimary { - switch state { - case .feedback: showSurveyAlert() - case .rateSession: viewModel.dependencies[singleton: .appReviewManager].willSubmitAppReview() - default: break - } +// MARK: - Alert for survey +private extension HomeVC { + func onHandlePrimaryTappedForState(_ state: AppReviewPromptState) { + switch state { + case .enjoyingSession: + viewModel.scheduleAppReviewRetry() + case .feedback: + // Close prompt before showing survery + appReviewPrompt.setReviewPrompt(nil) + viewModel.submitFeedbackSurvery() + case .rateSession: + // Close prompt before showing app review + appReviewPrompt.setReviewPrompt(nil) + viewModel.submitAppStoreReview() } } - func didChangePromptState(_ state: AppReviewPromptState) { - // Adjust insents so tableview can still be scrolled to bottom - let originalBottomInsets = ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - ) - - if state == .none { - tableView.contentInset.bottom = originalBottomInsets - } else { - tableView.contentInset.bottom = originalBottomInsets + (appReviewPrompt.frame.size.height + 24) - } - } -} - -// MARK: - Alert for survey -private extension HomeVC { - func showSurveyAlert() { - guard let url: URL = URL(string: Constants.feedback_url) else { return } - - var surverUrl: URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return url - } - - // stringlint:ignore_contents - components.queryItems = [ - .init(name: "platform", value: "iOS"), - .init(name: "version", value: viewModel.dependencies[cache: .appVersion].appVersion) - ] - - guard let finalURL = components.url else { return url } - - return finalURL + func onHandleSecondayTappedForState(_ state: AppReviewPromptState) { + switch state { + case .feedback, .rateSession: appReviewPrompt.setReviewPrompt(nil) + default: break } - let modal: ConfirmationModal = ConfirmationModal( - info: ConfirmationModal.Info( - title: "urlOpen".localized(), - body: .attributedText( - "urlOpenDescription" - .put(key: "url", value: url.absoluteString) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) - ), - confirmTitle: "open".localized(), - confirmStyle: .danger, - cancelTitle: "urlCopy".localized(), - cancelStyle: .alert_text, - hasCloseButton: true, - onConfirm: { modal in - UIApplication.shared.open(surverUrl, options: [:], completionHandler: nil) - modal.dismiss(animated: true) - }, - onCancel: { modal in - UIPasteboard.general.string = surverUrl.absoluteString - - modal.dismiss(animated: true) - } - ) - ) - - present(modal, animated: true) } } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index ade9c3f15e..e630602278 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -7,6 +7,8 @@ import DifferenceKit import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit +import StoreKit +import SessionUIKit // MARK: - Log.Category @@ -56,6 +58,10 @@ public class HomeViewModel: NavigatableStateHolder { .assign { [weak self] updatedState in self?.state = updatedState } } + public struct HomeViewModelEvent: Hashable { + let appReviewPromptTimestamp: TimeInterval? + } + // MARK: - State public struct State: ObservableKeyProvider { @@ -76,6 +82,8 @@ public class HomeViewModel: NavigatableStateHolder { let unreadMessageRequestThreadCount: Int let loadedPageInfo: PagedData.LoadedInfo let itemCache: [String: SessionThreadViewModel] + let appReviewPromptState: AppReviewPromptState? + let appReviewPromptTimestamp: TimeInterval? @MainActor public func sections(viewModel: HomeViewModel) -> [SectionModel] { HomeViewModel.sections(state: self, viewModel: viewModel) @@ -97,7 +105,11 @@ public class HomeViewModel: NavigatableStateHolder { .setting(.hasHiddenMessageRequests), .conversationCreated, .anyMessageCreatedInAnyConversation, - .anyContactBlockedStatusChanged + .anyContactBlockedStatusChanged, + .userDefault(.hasVisitedPathScreen), + .userDefault(.hasPressedDonateButton), + .userDefault(.hasChangedTheme), + .updateScreen(HomeViewModel.self) ] itemCache.values.forEach { threadViewModel in @@ -148,7 +160,9 @@ public class HomeViewModel: NavigatableStateHolder { groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL ), - itemCache: [:] + itemCache: [:], + appReviewPromptState: nil, + appReviewPromptTimestamp: nil ) } } @@ -170,6 +184,8 @@ public class HomeViewModel: NavigatableStateHolder { var unreadMessageRequestThreadCount: Int = previousState.unreadMessageRequestThreadCount var loadResult: PagedData.LoadResult = previousState.loadedPageInfo.asResult var itemCache: [String: SessionThreadViewModel] = previousState.itemCache + var appReviewPromptState: AppReviewPromptState? = previousState.appReviewPromptState + var appReviewPromptTimestamp: TimeInterval? = nil /// Store a local copy of the events so we can manipulate it based on the state changes var eventsToProcess: [ObservedEvent] = events @@ -196,6 +212,17 @@ public class HomeViewModel: NavigatableStateHolder { value: nil )) } + + /// Check if incomplete app review can be shown again to user + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + appReviewPromptState = .rateSession + } } /// If there are no events we want to process then just return the current state @@ -361,6 +388,36 @@ public class HomeViewModel: NavigatableStateHolder { } } + groupedOtherEvents?[.userDefault]?.forEach { event in + if let updatedValue = event.value as? Bool { + + switch event.key { + case .userDefault(.hasVisitedPathScreen): + if updatedValue == true { + appReviewPromptState = .enjoyingSession + } + case .userDefault(.hasPressedDonateButton): + if updatedValue == true { + appReviewPromptState = .enjoyingSession + } + case .userDefault(.hasChangedTheme): + if updatedValue == true { + appReviewPromptState = .enjoyingSession + } + default: break + } + } + } + + /// Next trigger should be ignored if `didShowAppReviewPrompt` is true + if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] == true { + appReviewPromptState = nil + } + + if let event: HomeViewModelEvent = events.first?.value as? HomeViewModelEvent { + appReviewPromptTimestamp = event.appReviewPromptTimestamp + } + /// Generate the new state return State( viewState: (loadResult.info.totalCount == 0 ? @@ -376,7 +433,9 @@ public class HomeViewModel: NavigatableStateHolder { hasHiddenMessageRequests: hasHiddenMessageRequests, unreadMessageRequestThreadCount: unreadMessageRequestThreadCount, loadedPageInfo: loadResult.info, - itemCache: itemCache + itemCache: itemCache, + appReviewPromptState: appReviewPromptState, + appReviewPromptTimestamp: appReviewPromptTimestamp ) } @@ -475,6 +534,92 @@ public class HomeViewModel: NavigatableStateHolder { ].flatMap { $0 } } + // MARK: - Handle App review + private static func handleAppReviewTriggerFlag(_ flag: Bool) -> AppReviewPromptState? { + guard flag == true else { return nil } + + return .enjoyingSession + } + + func viewDidAppear() { + let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [dependencies] in + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(HomeViewModel.self), + value: HomeViewModelEvent( + appReviewPromptTimestamp: timestamp, + ) + ) + } + } + + func scheduleAppReviewRetry() { + let now = dependencies.dateNow + + guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: now) else { + return + } + + dependencies[defaults: .standard, key: .rateAppRetryDate] = retryDate + } + + @MainActor func submitAppStoreReview() { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + + if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + SKStoreReviewController.requestReview(in: scene) + } + } + + @MainActor func submitFeedbackSurvery() { + guard let url: URL = URL(string: Constants.feedback_url) else { return } + + var surverUrl: URL { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return url + } + + // stringlint:ignore_contents + components.queryItems = [ + .init(name: "platform", value: "iOS"), + .init(name: "version", value: dependencies[cache: .appVersion].appVersion) + ] + + guard let finalURL = components.url else { return url } + + return finalURL + } + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "urlOpen".localized(), + body: .attributedText( + "urlOpenDescription" + .put(key: "url", value: url.absoluteString) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + ), + confirmTitle: "open".localized(), + confirmStyle: .danger, + cancelTitle: "urlCopy".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + onConfirm: { modal in + UIApplication.shared.open(surverUrl, options: [:], completionHandler: nil) + modal.dismiss(animated: true) + }, + onCancel: { modal in + UIPasteboard.general.string = surverUrl.absoluteString + + modal.dismiss(animated: true) + } + ) + ) + + self.transitionToScreen(modal, transitionType: .present) + } + // MARK: - Functions @MainActor func loadPageBefore() { diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 0bf717d3cf..6dec383517 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -73,7 +73,9 @@ final class PathVC: BaseVC { setUpNavBar() setUpViewHierarchy() - dependencies[singleton: .appReviewManager].triggerReview(for: .pathScreenVisit) + if dependencies[defaults: .standard, key: .hasVisitedPathScreen] == false { + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = true + } } private func setUpNavBar() { diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index aa378e2f0c..44c397c1e3 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -150,9 +150,6 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ } @MainActor private func didUpdateTheme(theme: Theme?) { - ThemeManager.updateThemeState(theme: theme) - - dependencies[singleton: .appReviewManager].triggerReview(for: .themeChange) } private static func sections( @@ -173,8 +170,12 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ trailingAccessory: .radio( isSelected: (state.theme == theme) ), - onTap: { [weak viewModel] in - viewModel?.didUpdateTheme(theme: theme) + onTap: { [dependencies = viewModel.dependencies] in + ThemeManager.updateThemeState(theme: theme) + // Update trigger only if it's not set to true + if dependencies[defaults: .standard, key: .hasChangedTheme] == false { + dependencies[defaults: .standard, key: .hasChangedTheme] = true + } } ) } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index d080457b0b..f71c2e849a 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -851,7 +851,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl self.transitionToScreen(modal, transitionType: .present) // Mark app review flag that donate button was tapped - dependencies[singleton: .appReviewManager].triggerReview(for: .donateButtonPress) + if dependencies[defaults: .standard, key: .hasPressedDonateButton] == false { + dependencies[defaults: .standard, key: .hasPressedDonateButton] = true + } } private func openTokenUrl() { diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index f1ce627d3f..9f12ec8132 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -173,7 +173,7 @@ public extension UserDefaults.BoolKey { static let hasChangedTheme: UserDefaults.BoolKey = "hasChangedTheme" /// Indicates whether the user pressed the donate button - static let hasDonated: UserDefaults.BoolKey = "hasDonated" + static let hasPressedDonateButton: UserDefaults.BoolKey = "hasPressedDonateButton" /// Indicates wheter app has already presented the user the app review prompt dialog static let didShowAppReviewPrompt: UserDefaults.BoolKey = "didShowAppReviewPrompt" From 35f6a89eb10ee7c8861309430ebfe39bcf193165 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 20 Aug 2025 11:59:03 +0800 Subject: [PATCH 088/244] Update state change handling --- .../View/AppReviewPromptDialog.swift | 12 +------ Session/Home/HomeVC.swift | 31 +++++++++++------ Session/Home/HomeViewModel.swift | 33 ++++++++++++++++--- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 9574e6749c..4c09d7bb54 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -127,28 +127,18 @@ class AppReviewPromptDialog: UIView { @objc func close() { - setReviewPrompt(nil) + onCloseTapped?() } @objc func primaryEvent() { let current = prompt ?? .enjoyingSession - - if current == .enjoyingSession { - setReviewPrompt(.rateSession) - } - onPrimaryTapped?(current) } @objc func secondaryEvent() { let current = prompt ?? .enjoyingSession - - if current == .enjoyingSession { - setReviewPrompt(.feedback) - } - onSecondaryTapped?(current) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index d5ea25178d..1d64f894f5 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -44,6 +44,15 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi private var loadingConversationsLabelTopConstraint: NSLayoutConstraint? private var navBarProfileView: ProfilePictureView? + private lazy var tableViewBottomInsets: CGFloat = { + ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + ) + }() + private lazy var seedReminderView: SeedReminderView = { let result = SeedReminderView() result.accessibilityLabel = "Recovery phrase reminder" @@ -75,12 +84,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi result.contentInset = UIEdgeInsets( top: 0, left: 0, - bottom: ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - ), + bottom: tableViewBottomInsets, right: 0 ) result.showsVerticalScrollIndicator = false @@ -348,6 +352,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi appReviewPrompt.onPrimaryTapped = { [weak self] state in self?.onHandlePrimaryTappedForState(state) } appReviewPrompt.onSecondaryTapped = { [weak self] in self?.onHandleSecondayTappedForState($0) } + appReviewPrompt.onCloseTapped = { [weak self] in self?.viewModel.handlePromptChangeState(nil) } // Preview prompt view.addSubview(appReviewPrompt) @@ -481,6 +486,11 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi if let promptState = state.appReviewPromptState, state.appReviewPromptTimestamp != nil { appReviewPrompt.setReviewPrompt(promptState) viewModel.dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + + tableView.contentInset.bottom = tableViewBottomInsets + (appReviewPrompt.frame.size.height + 24) + } else { + appReviewPrompt.setReviewPrompt(nil) + tableView.contentInset.bottom = tableViewBottomInsets } } @@ -828,22 +838,23 @@ private extension HomeVC { func onHandlePrimaryTappedForState(_ state: AppReviewPromptState) { switch state { case .enjoyingSession: + viewModel.handlePromptChangeState(.rateSession) viewModel.scheduleAppReviewRetry() case .feedback: // Close prompt before showing survery - appReviewPrompt.setReviewPrompt(nil) + viewModel.handlePromptChangeState(nil) viewModel.submitFeedbackSurvery() case .rateSession: // Close prompt before showing app review - appReviewPrompt.setReviewPrompt(nil) + viewModel.handlePromptChangeState(nil) viewModel.submitAppStoreReview() } } func onHandleSecondayTappedForState(_ state: AppReviewPromptState) { switch state { - case .feedback, .rateSession: appReviewPrompt.setReviewPrompt(nil) - default: break + case .feedback, .rateSession: viewModel.handlePromptChangeState(nil) + case .enjoyingSession: viewModel.handlePromptChangeState(.feedback) } } } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index e630602278..2e004ead21 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -60,6 +60,7 @@ public class HomeViewModel: NavigatableStateHolder { public struct HomeViewModelEvent: Hashable { let appReviewPromptTimestamp: TimeInterval? + let appReviewPromptState: AppReviewPromptState? } // MARK: - State @@ -213,9 +214,9 @@ public class HomeViewModel: NavigatableStateHolder { )) } - /// Check if incomplete app review can be shown again to user + /// Check if incomplete app review can be shown again to user on next app launch let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 @@ -416,6 +417,7 @@ public class HomeViewModel: NavigatableStateHolder { if let event: HomeViewModelEvent = events.first?.value as? HomeViewModelEvent { appReviewPromptTimestamp = event.appReviewPromptTimestamp + appReviewPromptState = event.appReviewPromptState } /// Generate the new state @@ -544,12 +546,13 @@ public class HomeViewModel: NavigatableStateHolder { func viewDidAppear() { let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [dependencies] in + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in dependencies.notifyAsync( priority: .immediate, key: .updateScreen(HomeViewModel.self), value: HomeViewModelEvent( appReviewPromptTimestamp: timestamp, + appReviewPromptState: state.appReviewPromptState ) ) } @@ -558,13 +561,35 @@ public class HomeViewModel: NavigatableStateHolder { func scheduleAppReviewRetry() { let now = dependencies.dateNow - guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: now) else { + guard let retryDate = Calendar.current.date(byAdding: .minute, value: 2, to: now) else { return } dependencies[defaults: .standard, key: .rateAppRetryDate] = retryDate } + func handlePromptChangeState(_ state: AppReviewPromptState?) { + let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(HomeViewModel.self), + value: HomeViewModelEvent( + appReviewPromptTimestamp: timestamp, + appReviewPromptState: state + ) + ) + } + + func clearFlags() { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false + dependencies[defaults: .standard, key: .hasPressedDonateButton] = false + dependencies[defaults: .standard, key: .hasChangedTheme] = false + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + } + @MainActor func submitAppStoreReview() { dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 From 2385449be0d2812d15576a1e91161b3d0e708590 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 20 Aug 2025 12:19:13 +0800 Subject: [PATCH 089/244] Updated feedback url for app review Added reset app review defaults to developer settings --- Session/Home/HomeViewModel.swift | 13 ++-------- .../Settings/DeveloperSettingsViewModel.swift | 25 +++++++++++++++++++ SessionUIKit/Style Guide/Constants.swift | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 2e004ead21..81d0c6b532 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -561,7 +561,7 @@ public class HomeViewModel: NavigatableStateHolder { func scheduleAppReviewRetry() { let now = dependencies.dateNow - guard let retryDate = Calendar.current.date(byAdding: .minute, value: 2, to: now) else { + guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: now) else { return } @@ -580,16 +580,7 @@ public class HomeViewModel: NavigatableStateHolder { ) ) } - - func clearFlags() { - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false - dependencies[defaults: .standard, key: .hasPressedDonateButton] = false - dependencies[defaults: .standard, key: .hasChangedTheme] = false - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 - } - + @MainActor func submitAppStoreReview() { dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e61c5570c0..61eb9f721b 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -408,6 +408,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onTap: { [weak self] in self?.copyAppGroupPath() } + ), + SessionCell.Info( + id: .copyAppGroupPath, + title: "Reset App Review Prompt", + subtitle: """ + Clears user default settings for the app review prompt, enabling quicker testing of various display conditions. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Reset"), + onTap: { [weak self] in + self?.resetAppReviewPrompt() + } ) ] ) @@ -1430,6 +1441,20 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) } + private func resetAppReviewPrompt() { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false + dependencies[defaults: .standard, key: .hasPressedDonateButton] = false + dependencies[defaults: .standard, key: .hasChangedTheme] = false + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + + showToast( + text: "cleared".localized(), + backgroundColor: .backgroundSecondary + ) + } + // MARK: - SESH private func scheduleLocalNotification(button: SessionButton?) { diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index ce6b9f1022..c4e70f61f0 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -16,5 +16,5 @@ public enum Constants { public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" public static let store_variant: String = "App Store" - public static let feedback_url: String = "https://getsession.org/feedback" + public static let feedback_url: String = "https://www.surveymonkey.com/r/YLDZJR8" } From f13c8d7dc8b1b091c5c52c4e906012a28ce3b98f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 20 Aug 2025 15:14:43 +1000 Subject: [PATCH 090/244] Fixed an issue where reactions weren't working in groups --- Session/Conversations/ConversationVC+Interaction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..d807a5ef20 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1977,7 +1977,7 @@ extension ConversationVC: ) ), to: destination, - namespace: .default, + namespace: destination.defaultNamespace, interactionId: cellViewModel.id, attachments: nil, authMethod: authMethod, From 38237df9f2a6d0cfcd52661a94ba01f64fdba29b Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 20 Aug 2025 14:05:17 +0800 Subject: [PATCH 091/244] Additional buffers added for direction detection --- .../ImagePickerController.swift | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 7661dacac8..63509f5332 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -563,11 +563,29 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate { if let pan = gestureRecognizer as? UIPanGestureRecognizer { let velocity = pan.velocity(in: collectionView) - // Check the horizontal movement is greater than vertical then - // treat it as a "selection" pan rather than a "scroll". - // Vertical velocity == scrolling the list. - // Horizontal velocity == less common for scrolling (do panning multi select) - return abs(velocity.x) > abs(velocity.y) + // Threshold for what's considered "significant" movement in either direction. + let minVelocity: CGFloat = 30.0 + + // A buffer for diagonal detection. + // If the absolute difference between x and y velocity is within this buffer, + // it's considered diagonal. + let diagonalBuffer: CGFloat = 30.0 + + guard abs(velocity.x) > minVelocity || abs(velocity.y) > minVelocity else { + // Not enough movement to make a decision, let other gestures handle it or ignore. + return false + } + + if abs(velocity.x) > minVelocity && abs(velocity.y) > minVelocity && abs(abs(velocity.x) - abs(velocity.y)) <= diagonalBuffer { + // Dialognal detected + return false // Prevent the pan gesture for diagonal scrolls + } + + if abs(velocity.x) > abs(velocity.y) { + return true + } + + return false } return true From f1fbd862648321827f5254ad0474d5551541fb36 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 20 Aug 2025 15:21:15 +0800 Subject: [PATCH 092/244] Removed `expiresStartedAtMs` value for optimistic message data Code clean ups --- .../Conversations/Context Menu/ContextMenuVC+Action.swift | 7 +------ Session/Conversations/ConversationViewModel.swift | 3 --- .../Sending & Receiving/MessageSender+Convenience.swift | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index a76e50b43c..cc00d754d1 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -105,12 +105,7 @@ extension ContextMenuVC { static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { - let canCountdown: Bool = { - guard cellViewModel.variant.isOutgoing else { return true } - - let state = cellViewModel.state - return state != .sending && state != .failed - }() + let canCountdown: Bool = (!cellViewModel.variant.isOutgoing || ![.sending, .failed].contains(cellViewModel.state)) return Action( icon: Lucide.image(icon: .trash2, size: 24), diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3c8ede79ed..07c82e7e4b 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -741,9 +741,6 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold body: text ), expiresInSeconds: threadData.disappearingMessagesConfiguration?.expiresInSeconds(), - expiresStartedAtMs: threadData.disappearingMessagesConfiguration?.initialExpiresStartedAtMs( - sentTimestampMs: Double(sentTimestampMs) - ), linkPreviewUrl: linkPreviewDraft?.urlString, isProMessage: dependencies[cache: .libSession].isSessionPro, using: dependencies diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index fd3e8e8c03..cb7a63ffc6 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -214,8 +214,8 @@ extension MessageSender { // from the correct time. var scheduledTimestampForDeletion: Double? { guard interaction.isExpiringMessage else { return nil } - let sentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - return Double(sentTimestampMs) + let sentTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return sentTimestampMs } try interaction.with( From 93ba84b7af12b58da206fdbefc93ea9110f33934 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 09:38:04 +1000 Subject: [PATCH 093/244] Added a dev setting to control the poll limit for the CommunityPoller --- Session/Meta/Translations/InfoPlist.xcstrings | 52 ++++++- .../DeveloperSettingsViewModel+Testing.swift | 13 ++ .../Settings/DeveloperSettingsViewModel.swift | 135 ++++++++++++++++++ .../Open Groups/OpenGroupAPI.swift | 9 +- .../Dependency Injection/Dependencies.swift | 4 + .../Dependency Injection/FeatureConfig.swift | 1 - .../General/Feature+ServiceNetwork.swift | 2 +- SessionUtilitiesKit/General/Feature.swift | 73 ++++++++-- SessionUtilitiesKit/General/Logging.swift | 1 - 9 files changed, 274 insertions(+), 16 deletions(-) diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettingsViewModel+Testing.swift index 185528af9c..09381d297d 100644 --- a/Session/Settings/DeveloperSettingsViewModel+Testing.swift +++ b/Session/Settings/DeveloperSettingsViewModel+Testing.swift @@ -60,6 +60,11 @@ extension DeveloperSettingsViewModel { /// /// **Value:** `true`/`false` (default: `false`) case debugDisappearingMessageDurations + + /// Controls the number of messages that the CommunityPoller should try to retrieve every time it polls + /// + /// **Value:** `1-256` (default: `100`, a value of `0` will use the default) + case communityPollLimit } ProcessInfo.processInfo.environment.forEach { key, value in @@ -94,6 +99,14 @@ extension DeveloperSettingsViewModel { case .debugDisappearingMessageDurations: dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) + + case .communityPollLimit: + guard + let intValue: Int = Int(value), + intValue >= 1 && intValue < 256 + else { return } + + dependencies.set(feature: .communityPollLimit, to: intValue) } } #endif diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e61c5570c0..ef1a0ce91f 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -41,6 +41,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case logging case network case disappearingMessages + case communities case groups case database @@ -53,6 +54,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .logging: return "Logging" case .network: return "Network" case .disappearingMessages: return "Disappearing Messages" + case .communities: return "Communities" case .groups: return "Groups" case .database: return "Database" } @@ -93,6 +95,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case debugDisappearingMessageDurations + case communityPollLimit + case updatedGroupsDisableAutoApprove case updatedGroupsRemoveMessagesOnKick case updatedGroupsAllowHistoricAccessOnInvite @@ -131,6 +135,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .pushNotificationService: return "pushNotificationService" case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations" + + case .communityPollLimit: return "communityPollLimit" case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" @@ -181,6 +187,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough + case .communityPollLimit: result.append(.communityPollLimit); fallthrough + case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough case .updatedGroupsAllowHistoricAccessOnInvite: @@ -229,6 +237,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let debugDisappearingMessageDurations: Bool + let communityPollLimit: Int + let updatedGroupsDisableAutoApprove: Bool let updatedGroupsRemoveMessagesOnKick: Bool let updatedGroupsAllowHistoricAccessOnInvite: Bool @@ -282,6 +292,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], + communityPollLimit: dependencies[feature: .communityPollLimit], + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], @@ -584,6 +596,32 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) + let communities: SectionModel = SectionModel( + model: .communities, + elements: [ + SessionCell.Info( + id: .communityPollLimit, + title: "Community Poll Limit", + subtitle: """ + The number of messages to try to retrieve when polling a community (up to a maximum of 256). + + Note: An empty value, or a value of 0 will use the default value: \(dependencies.defaultValue(feature: .communityPollLimit).map { "\($0)"} ?? "N/A"). + """, + trailingAccessory: .custom(info: PollLimitInputView.Info( + limit: dependencies[feature: .communityPollLimit], + onChange: { [dependencies] value in + dependencies.set(feature: .communityPollLimit, to: value) + } + )), + onTapView: { view in + view?.subviews + .flatMap { $0.subviews } + .first(where: { $0 is UITextField })? + .becomeFirstResponder() + } + ) + ] + ) let groups: SectionModel = SectionModel( model: .groups, elements: [ @@ -922,6 +960,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, logging, network, disappearingMessages, + communities, groups, sessionPro, sessionNetwork, @@ -986,6 +1025,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .debugDisappearingMessageDurations, to: nil) + case .communityPollLimit: + guard dependencies.hasSet(feature: .communityPollLimit) else { return } + + dependencies.set(feature: .communityPollLimit, to: nil) + forceRefresh(type: .databaseQuery) + case .updatedGroupsDisableAutoApprove: guard dependencies.hasSet(feature: .updatedGroupsDisableAutoApprove) else { return } @@ -1908,6 +1953,96 @@ private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { } } +// MARK: - PollLimitInputView + +final class PollLimitInputView: UIView, UITextFieldDelegate, SessionCell.Accessory.CustomView { + struct Info: Equatable, SessionCell.Accessory.CustomViewInfo { + typealias View = PollLimitInputView + + let limit: Int + let onChange: (Int?) -> Void + + public static func ==(lhs: Info, rhs: Info) -> Bool { + return lhs.limit == rhs.limit + } + + public func hash(into hasher: inout Hasher) { + limit.hash(into: &hasher) + } + } + + static func create(maxContentWidth: CGFloat, using dependencies: Dependencies) -> PollLimitInputView { + return PollLimitInputView() + } + + public static let size: SessionCell.Accessory.Size = .fillWidthWrapHeight + private var onChange: ((Int?) -> Void)? + + // MARK: - Components + + private lazy var textField: UITextField = { + let result = UITextField() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.textAlignment = .center + result.delegate = self + + return result + }() + + // MARK: - Initializtion + + init() { + super.init(frame: .zero) + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("Use init(color:) instead") + } + + // MARK: - Layout + + private func setupUI() { + layer.borderWidth = 1 + layer.cornerRadius = 8 + themeBackgroundColor = .backgroundPrimary + themeBorderColor = .borderSeparator + + addSubview(textField) + textField.pin(to: self, withInset: Values.verySmallSpacing) + } + + // MARK: - Content + + func update(with info: Info) { + onChange = info.onChange + textField.text = "\(info.limit)" + } + + // MARK: - UITextFieldDelegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let currentText: String = (textField.text ?? "") + + guard let textRange: Range = Range(range, in: currentText) else { return false } + + let updatedText: String = currentText.replacingCharacters(in: textRange, with: string) + + // Allow an empty string (revert to the default in this case) + guard !updatedText.isEmpty else { + onChange?(nil) + return true + } + guard let value: Int = Int(updatedText) else { return false } + guard value >= 0 && value < 256 else { return false } + + onChange?(value) + return true + } +} + + // MARK: - Listable Conformance extension ServiceNetwork: @retroactive ContentIdentifiable {} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 78b97e3f08..3d0bc3ccb2 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -496,7 +496,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesRecent(roomToken), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), @@ -524,7 +525,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesBefore(roomToken, id: messageId), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), @@ -552,7 +554,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 8103d8bfe0..b60081b0c8 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -233,6 +233,10 @@ public extension Dependencies { ObservedEvent(key: .featureGroup(feature), value: nil) ]) } + + func defaultValue(feature: FeatureConfig) -> T? { + return feature.createInstance(self).defaultOption + } } // MARK: - DependenciesError diff --git a/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift b/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift index 58b4b12c59..30440e19ab 100644 --- a/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift +++ b/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift @@ -25,7 +25,6 @@ public class FeatureConfig: FeatureStorage { self.createInstance = { _ in Feature( identifier: identifier, - options: Array(T.allCases), defaultOption: defaultOption, automaticChangeBehaviour: automaticChangeBehaviour ) diff --git a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift index 6797926834..7ae5469b20 100644 --- a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift +++ b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift @@ -15,7 +15,7 @@ public extension FeatureStorage { // MARK: - ServiceNetwork Feature -public enum ServiceNetwork: Int, Sendable, FeatureOption { +public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { case mainnet = 1 case testnet = 2 diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index 9f73288a61..239739cda0 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -41,6 +41,11 @@ public extension FeatureStorage { identifier: "forceSlowDatabaseQueries" ) + static let communityPollLimit: FeatureConfig = Dependencies.create( + identifier: "communityPollLimit", + defaultOption: 100 + ) + static let updatedGroupsDisableAutoApprove: FeatureConfig = Dependencies.create( identifier: "updatedGroupsDisableAutoApprove" ) @@ -92,12 +97,14 @@ public extension FeatureStorage { // MARK: - FeatureOption -public protocol FeatureOption: RawRepresentable, CaseIterable, Equatable, Hashable { +public protocol FeatureOption: RawRepresentable, Equatable, Hashable { static var defaultOption: Self { get } var isValidOption: Bool { get } var title: String { get } var subtitle: String? { get } + + static func validateOptions(defaultOption: Self) } public extension FeatureOption { @@ -127,7 +134,6 @@ public struct Feature: FeatureType { } private let identifier: String - public let options: [T] public let defaultOption: T public let automaticChangeBehaviour: ChangeBehaviour? @@ -135,17 +141,12 @@ public struct Feature: FeatureType { public init( identifier: String, - options: [T], defaultOption: T, automaticChangeBehaviour: ChangeBehaviour? = nil ) { - guard - T.self == Bool.self || - !options.appending(defaultOption).contains(where: { ($0.rawValue as? Int) == 0 }) - else { preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") } + T.validateOptions(defaultOption: defaultOption) self.identifier = identifier - self.options = options self.defaultOption = defaultOption self.automaticChangeBehaviour = automaticChangeBehaviour } @@ -263,3 +264,59 @@ extension Bool: FeatureOption { return (self ? "Enabled" : "Disabled") } } + +// MARK: - Int FeatureOption + +extension Int: @retroactive RawRepresentable {} +extension Int: FeatureOption { + // MARK: - Initialization + + public var rawValue: Int { return self } + + public init?(rawValue: Int) { + self = rawValue + } + + // MARK: - Feature Option + + public static var defaultOption: Int = 0 + + public var title: String { + return "\(self)" + } + + public var subtitle: String? { + return "\(self)" + } +} + +// MARK: - FeatureOption Validation + +extension FeatureOption { + public static func validateOptions(defaultOption: Self) { + guard (defaultOption.rawValue as? Int) != 0 else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} + +extension FeatureOption where Self == Bool { + /// A `Bool` feature is always valid + public static func validateOptions(defaultOption: Bool) {} +} + +extension FeatureOption where Self == Int { + public static func validateOptions(defaultOption: Int) { + guard defaultOption != 0 else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} + +extension FeatureOption where Self: CaseIterable { + public static func validateOptions(defaultOption: Self) { + guard !Array(Self.allCases).appending(defaultOption).contains(where: { ($0.rawValue as? Int) == 0 }) else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 2dff548f87..8f722931ea 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -858,7 +858,6 @@ extension Log.Level: FeatureOption { // MARK: - AllLoggingCategories public struct AllLoggingCategories: FeatureOption { - public static let allCases: [AllLoggingCategories] = [] @ThreadSafeObject private static var registeredCategoryDefaults: Set = [] // MARK: - Initialization From f32e2f85bbf2feec42375332cc7d198a1a79c047 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 08:28:48 +0800 Subject: [PATCH 094/244] Removed usage of `canCountdown` --- .../Context Menu/ContextMenuVC+Action.swift | 7 +------ .../Context Menu/ContextMenuVC+ActionView.swift | 17 ++++------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index cc00d754d1..15d0c0a8b9 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -10,7 +10,6 @@ extension ContextMenuVC { struct ExpirationInfo { let expiresStartedAtMs: Double? let expiresInSeconds: TimeInterval? - let canCountdown: Bool } struct Action { @@ -104,16 +103,12 @@ extension ContextMenuVC { } static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { - - let canCountdown: Bool = (!cellViewModel.variant.isOutgoing || ![.sending, .failed].contains(cellViewModel.state)) - return Action( icon: Lucide.image(icon: .trash2, size: 24), title: "delete".localized(), expirationInfo: ExpirationInfo( expiresStartedAtMs: cellViewModel.expiresStartedAtMs, - expiresInSeconds: cellViewModel.expiresInSeconds, - canCountdown: canCountdown + expiresInSeconds: cellViewModel.expiresInSeconds ), themeColor: .danger, shouldDismissInfoScreen: true, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 638c3752a2..52dfc7a33f 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -121,8 +121,8 @@ extension ContextMenuVC { private func setUpSubtitle() { guard let expirationInfo = self.action.expirationInfo, - let expiresInSeconds = expirationInfo.expiresInSeconds, - let expiresStartedAtMs = expirationInfo.expiresStartedAtMs + let expiresStartedAtMs = expirationInfo.expiresStartedAtMs, + let expiresInSeconds = expirationInfo.expiresInSeconds else { subtitleLabel.isHidden = true subtitleWidthConstraint.isActive = false @@ -133,21 +133,12 @@ extension ContextMenuVC { subtitleWidthConstraint.isActive = true // To prevent a negative timer - var timeToExpireInSeconds: TimeInterval { - // If canCountdown = false, use base expiration timer value - guard expirationInfo.canCountdown else { - return expiresInSeconds - } - - return max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) - } + let timeToExpireInSeconds = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) subtitleLabel.text = "disappearingMessagesCountdownBigMobile" - .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits, minimumUnit: expirationInfo.canCountdown ? .second : .minute)) + .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits, minimumUnit: .second)) .localized() - guard expirationInfo.canCountdown else { return } - timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, using: dependencies, block: { [weak self, dependencies] _ in let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000 if timeToExpireInSeconds <= 0 { From 96b27eb98b08ae9d028d976f3efc705962c424ba Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 08:33:41 +0800 Subject: [PATCH 095/244] Updated pan gesture computation --- .../ImagePickerController.swift | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 63509f5332..ae89e63281 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -562,30 +562,21 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let pan = gestureRecognizer as? UIPanGestureRecognizer { let velocity = pan.velocity(in: collectionView) - + // Threshold for what's considered "significant" movement in either direction. let minVelocity: CGFloat = 30.0 - // A buffer for diagonal detection. - // If the absolute difference between x and y velocity is within this buffer, - // it's considered diagonal. - let diagonalBuffer: CGFloat = 30.0 - guard abs(velocity.x) > minVelocity || abs(velocity.y) > minVelocity else { // Not enough movement to make a decision, let other gestures handle it or ignore. return false } - if abs(velocity.x) > minVelocity && abs(velocity.y) > minVelocity && abs(abs(velocity.x) - abs(velocity.y)) <= diagonalBuffer { - // Dialognal detected - return false // Prevent the pan gesture for diagonal scrolls - } - - if abs(velocity.x) > abs(velocity.y) { - return true - } - - return false + // We only want to activate the "drag to select" within a ~30 degree angle in either + // direction so approximate if the velocity is within this angle + let ratio = abs(velocity.y / velocity.x) + let tangentOf30DegreeBuffer: CGFloat = 0.577 // This is about `tan(30)` + + return (ratio < tangentOf30DegreeBuffer) } return true From bfc5f9bad1e4f82bc0084903a7e164496b3a2371 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 09:10:23 +0800 Subject: [PATCH 096/244] Updated invalid ONS search error message --- Session/Closed Groups/EditGroupViewModel.swift | 7 +------ Session/Home/New Conversation/NewMessageScreen.swift | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 01916f533c..0b496ca816 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -544,12 +544,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case .finished: break case .failure(let error): modalActivityIndicator.dismiss { - switch error { - case SnodeAPIError.onsNotFound: - return showError("onsErrorNotRecognized".localized()) - default: - return showError("onsErrorUnableToSearch".localized()) - } + return showError("onsErrorNotRecognized".localized()) } } }, diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index c478eb8331..93a6b76f15 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -98,12 +98,7 @@ struct NewMessageScreen: View { case .failure(let error): modalActivityIndicator.dismiss { let message: String = { - switch error { - case SnodeAPIError.onsNotFound: - return "onsErrorNotRecognized".localized() - default: - return "onsErrorUnableToSearch".localized() - } + return "onsErrorNotRecognized".localized() }() errorString = message From 404ce04dc14b356bef8b65e8f9628cea6789f89c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 09:13:12 +0800 Subject: [PATCH 097/244] Changed voice message reply placeholder subtitle --- SessionMessagingKit/Database/Models/Attachment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 601023baf3..a6f3b6dc47 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -651,7 +651,7 @@ extension Attachment { public var shortDescription: String { if isImage { return "image".localized() } - if isAudio { return "audio".localized() } + if isAudio { return "messageVoice".localized() } if isVideo { return "video".localized() } return "document".localized() } From 8f6f60f3580d4de5a3966d041477e06a745949d6 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 10:21:56 +0800 Subject: [PATCH 098/244] Revert switch case indentations --- Session/Home/HomeVC.swift | 266 +++++++++++++++++++------------------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 414a711314..b8f76dec5a 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -514,9 +514,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi profile: userProfile, profileIcon: { switch (serviceNetwork, forceOffline) { - case (.testnet, false): return .letter("T", false) // stringlint:ignore - case (.testnet, true): return .letter("T", true) // stringlint:ignore - default: return .none + case (.testnet, false): return .letter("T", false) // stringlint:ignore + case (.testnet, true): return .letter("T", true) // stringlint:ignore + default: return .none } }(), additionalProfile: nil, @@ -565,23 +565,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[indexPath.section] switch section.model { - case .messageRequests: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath) - cell.accessibilityIdentifier = "Message requests banner" - cell.isAccessibilityElement = true - cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0)) - return cell - - case .threads: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.update(with: threadViewModel, using: viewModel.dependencies) - cell.accessibilityIdentifier = "Conversation list item" - cell.accessibilityLabel = threadViewModel.displayName - return cell - - default: preconditionFailure("Other sections should have no content") + case .messageRequests: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath) + cell.accessibilityIdentifier = "Message requests banner" + cell.isAccessibilityElement = true + cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0)) + return cell + + case .threads: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) + cell.update(with: threadViewModel, using: viewModel.dependencies) + cell.accessibilityIdentifier = "Conversation list item" + cell.accessibilityLabel = threadViewModel.displayName + return cell + + default: preconditionFailure("Other sections should have no content") } } @@ -611,15 +611,15 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[section] switch section.model { - case .loadMore: return HomeVC.loadingHeaderHeight - default: return 0 + case .loadMore: return HomeVC.loadingHeaderHeight + default: return 0 } } public func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { switch sections[section].model { - case .loadMore: self.viewModel.loadNextPage() - default: break + case .loadMore: self.viewModel.loadNextPage() + default: break } } @@ -629,23 +629,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let section: HomeViewModel.SectionModel = sections[indexPath.section] switch section.model { - case .messageRequests: - let viewController: SessionTableViewController = SessionTableViewController( - viewModel: MessageRequestsViewModel(using: viewModel.dependencies) - ) - self.navigationController?.pushViewController(viewController, animated: true) - - case .threads: - let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let viewController: ConversationVC = ConversationVC( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - focusedInteractionInfo: nil, - using: viewModel.dependencies - ) - self.navigationController?.pushViewController(viewController, animated: true) + case .messageRequests: + let viewController: SessionTableViewController = SessionTableViewController( + viewModel: MessageRequestsViewModel(using: viewModel.dependencies) + ) + self.navigationController?.pushViewController(viewController, animated: true) + + case .threads: + let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] + let viewController: ConversationVC = ConversationVC( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + focusedInteractionInfo: nil, + using: viewModel.dependencies + ) + self.navigationController?.pushViewController(viewController, animated: true) - default: break + default: break } } @@ -666,32 +666,32 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] switch section.model { - case .threads: - // Cannot properly sync outgoing blinded message requests so don't provide the option, - // the 'Note to Self' conversation also doesn't support 'mark as unread' so don't - // provide it there either - guard - threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadId != threadViewModel.currentUserSessionId && ( - threadViewModel.threadVariant != .contact || - (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard + case .threads: + // Cannot properly sync outgoing blinded message requests so don't provide the option, + // the 'Note to Self' conversation also doesn't support 'mark as unread' so don't + // provide it there either + guard + threadViewModel.threadVariant != .legacyGroup && + threadViewModel.threadId != threadViewModel.currentUserSessionId && ( + threadViewModel.threadVariant != .contact || + (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard + ) + else { return nil } + + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [.toggleReadStatus], + for: .leading, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) - else { return nil } - - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [.toggleReadStatus], - for: .leading, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies ) - ) - - default: return nil + + default: return nil } } @@ -700,76 +700,76 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] switch section.model { - case .messageRequests: - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [.hide], - for: .trailing, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies - ) - ) - - case .threads: - let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) - - // Cannot properly sync outgoing blinded message requests so only provide valid options - let shouldHavePinAction: Bool = { - switch threadViewModel.threadVariant { - // Only allow unpin for legacy groups - case .legacyGroup: return threadViewModel.threadPinnedPriority > 0 - - default: - return ( - sessionIdPrefix != .blinded15 && - sessionIdPrefix != .blinded25 + case .messageRequests: + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [.hide], + for: .trailing, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) - } - }() - let shouldHaveMuteAction: Bool = { - switch threadViewModel.threadVariant { - case .contact: return ( - !threadViewModel.threadIsNoteToSelf && - sessionIdPrefix != .blinded15 && - sessionIdPrefix != .blinded25 ) - - case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) - - case .legacyGroup: return false - case .community: return true - } - }() - let destructiveAction: UIContextualAction.SwipeAction = { - switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember, threadViewModel.currentUserIsClosedGroupAdmin) { - case (.contact, true, _, _): return .hide - case (.group, _, true, false), (.community, _, _, _): return .leave - default: return .delete - } - }() - - return UIContextualAction.configuration( - for: UIContextualAction.generateSwipeActions( - [ - (!shouldHavePinAction ? nil : .pin), - (!shouldHaveMuteAction ? nil : .mute), - destructiveAction - ].compactMap { $0 }, - for: .trailing, - indexPath: indexPath, - tableView: tableView, - threadViewModel: threadViewModel, - viewController: self, - navigatableStateHolder: viewModel, - using: viewModel.dependencies + + case .threads: + let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) + + // Cannot properly sync outgoing blinded message requests so only provide valid options + let shouldHavePinAction: Bool = { + switch threadViewModel.threadVariant { + // Only allow unpin for legacy groups + case .legacyGroup: return threadViewModel.threadPinnedPriority > 0 + + default: + return ( + sessionIdPrefix != .blinded15 && + sessionIdPrefix != .blinded25 + ) + } + }() + let shouldHaveMuteAction: Bool = { + switch threadViewModel.threadVariant { + case .contact: return ( + !threadViewModel.threadIsNoteToSelf && + sessionIdPrefix != .blinded15 && + sessionIdPrefix != .blinded25 + ) + + case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) + + case .legacyGroup: return false + case .community: return true + } + }() + let destructiveAction: UIContextualAction.SwipeAction = { + switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember, threadViewModel.currentUserIsClosedGroupAdmin) { + case (.contact, true, _, _): return .hide + case (.group, _, true, false), (.community, _, _, _): return .leave + default: return .delete + } + }() + + return UIContextualAction.configuration( + for: UIContextualAction.generateSwipeActions( + [ + (!shouldHavePinAction ? nil : .pin), + (!shouldHaveMuteAction ? nil : .mute), + destructiveAction + ].compactMap { $0 }, + for: .trailing, + indexPath: indexPath, + tableView: tableView, + threadViewModel: threadViewModel, + viewController: self, + navigatableStateHolder: viewModel, + using: viewModel.dependencies + ) ) - ) - - default: return nil + + default: return nil } } @@ -853,8 +853,8 @@ private extension HomeVC { func onHandleSecondayTappedForState(_ state: AppReviewPromptState) { switch state { - case .feedback, .rateSession: viewModel.handlePromptChangeState(nil) - case .enjoyingSession: viewModel.handlePromptChangeState(.feedback) + case .feedback, .rateSession: viewModel.handlePromptChangeState(nil) + case .enjoyingSession: viewModel.handlePromptChangeState(.feedback) } } } From 6b1a423403d75453d14af91cfbf9a26049f8101c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 11:08:15 +0800 Subject: [PATCH 099/244] Fix wrong fonts and margins --- .../View/AppReviewPromptDialog.swift | 110 ++++++++++-------- Session/Meta/Translations/InfoPlist.xcstrings | 52 ++++++++- 2 files changed, 109 insertions(+), 53 deletions(-) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 4c09d7bb54..3be8c2360f 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -9,93 +9,101 @@ class AppReviewPromptDialog: UIView { var onSecondaryTapped: ((AppReviewPromptState) -> Void)? private lazy var closeButton: UIButton = { - let button = UIButton(type: .custom) - button.setImage( + let result = UIButton(type: .custom) + result.setImage( UIImage(named: "X")? .withRenderingMode(.alwaysTemplate), for: .normal ) - button.themeTintColor = .textPrimary - button.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside) - button.set(.width, to: Values.largeSpacing) - button.set(.height, to: Values.largeSpacing) + result.themeTintColor = .textPrimary + result.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside) + result.set(.width, to: Values.largeSpacing) + result.set(.height, to: Values.largeSpacing) - return button + return result }() private lazy var titleLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - label.themeTextColor = .textPrimary - label.font = .systemFont(ofSize: Values.mediumFontSize, weight: .medium) - return label + let result = UILabel() + result.textAlignment = .center + result.numberOfLines = 0 + result.themeTextColor = .textPrimary + result.font = .systemFont(ofSize: 18, weight: .bold) + + return result }() private lazy var messageLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.numberOfLines = 0 - label.themeTextColor = .textSecondary - label.font = .systemFont(ofSize: Values.smallFontSize, weight: .regular) - return label + let result = UILabel() + result.textAlignment = .center + result.numberOfLines = 0 + result.themeTextColor = .textSecondary + result.font = .systemFont(ofSize: 16, weight: .regular) + + return result }() private lazy var primaryButton: UIButton = { - let button = UIButton(type: .custom) - button.setThemeTitleColor(.sessionButton_text, for: .normal) - button.setThemeTitleColor(.sessionButton_highlight, for: .highlighted) - - button.titleLabel?.numberOfLines = 3 - button.titleLabel?.textAlignment = .center - - button.addTarget(self, action: #selector(primaryEvent), for: .touchUpInside) + let result = UIButton(type: .custom) + result.setThemeTitleColor(.sessionButton_text, for: .normal) + result.setThemeTitleColor(.sessionButton_highlight, for: .highlighted) + result.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) + result.titleLabel?.numberOfLines = 0 + result.titleLabel?.textAlignment = .center + result.addTarget(self, action: #selector(primaryEvent), for: .touchUpInside) - return button + return result }() private lazy var secondaryButton: UIButton = { - let button = UIButton(type: .custom) - button.setThemeTitleColor(.textPrimary, for: .normal) - button.setThemeTitleColor(.textSecondary, for: .highlighted) - - button.titleLabel?.numberOfLines = 3 - button.titleLabel?.textAlignment = .center - - button.addTarget(self, action: #selector(secondaryEvent), for: .touchUpInside) + let result = UIButton(type: .custom) + result.setThemeTitleColor(.textPrimary, for: .normal) + result.setThemeTitleColor(.textSecondary, for: .highlighted) + result.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) + result.titleLabel?.numberOfLines = 0 + result.titleLabel?.textAlignment = .center + result.addTarget(self, action: #selector(secondaryEvent), for: .touchUpInside) - return button + return result }() private lazy var buttonStack: UIStackView = { - let stack = UIStackView(arrangedSubviews: [ + let result = UIStackView(arrangedSubviews: [ primaryButton, secondaryButton ]) - stack.axis = .horizontal - stack.distribution = .fillEqually - stack.alignment = .fill - stack.isLayoutMarginsRelativeArrangement = true - stack.layoutMargins = .init( + result.axis = .horizontal + result.distribution = .fillEqually + result.alignment = .fill + result.isLayoutMarginsRelativeArrangement = true + result.layoutMargins = .init( top: Values.mediumSpacing, left: 0, bottom: Values.mediumSpacing, right: 0 ) - return stack + return result }() private lazy var contentStack: UIStackView = { - let stack = UIStackView(arrangedSubviews: [ + let result = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStack ]) - stack.axis = .vertical - stack.distribution = .fill - stack.spacing = 6 - return stack + result.axis = .vertical + result.distribution = .fill + result.spacing = 8 + result.isLayoutMarginsRelativeArrangement = true + result.layoutMargins = .init( + top: 0, + left: Values.largeSpacing, + bottom: 0, + right: Values.largeSpacing + ) + + return result }() private var prompt: AppReviewPromptState? @@ -150,8 +158,8 @@ private extension AppReviewPromptDialog { } func setupLayout() { - closeButton.pin(.top, to: .top, of: self, withInset: Values.smallSpacing) - closeButton.pin(.right, to: .right, of: self, withInset: -Values.smallSpacing) + closeButton.pin(.top, to: .top, of: self, withInset: Values.mediumSmallSpacing) + closeButton.pin(.right, to: .right, of: self, withInset: -Values.mediumSmallSpacing) contentStack.pin(.top, to: .bottom, of: closeButton) contentStack.pin(.left, to: .left, of: self, withInset: Values.mediumSmallSpacing) diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { From 513e1b295b3a839f40e22bad536305f90dd1e6cf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 13:15:43 +1000 Subject: [PATCH 100/244] Apparently we don't need to escape the % character in notifications anymore --- Session/Meta/Translations/InfoPlist.xcstrings | 52 ++++++++++++++++++- .../NotificationsManagerType.swift | 1 - .../General/String+Utilities.swift | 9 ---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift index c65132e554..b844d336dc 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift @@ -251,7 +251,6 @@ public extension NotificationsManagerType { ) }? .filteredForDisplay - .filteredForNotification .nullIfEmpty? .replacingMentions( currentUserSessionIds: currentUserSessionIds, diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index bbe0503875..7b35101c3b 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -223,15 +223,6 @@ public extension String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } - /// iOS strips anything that looks like a printf formatting character from the notification body, so if we want to dispay a literal "%" in - /// a notification it must be escaped. - /// - /// See https://developer.apple.com/documentation/usernotifications/unnotificationcontent/body for - /// more details. - var filteredForNotification: String { - self.replacingOccurrences(of: "%", with: "%%") - } - private var hasExcessiveDiacriticals: Bool { for char in self.enumerated() { let scalarCount = String(char.element).unicodeScalars.count From 3262797238945f795e64ff5ac5a3e834de9e4158 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 11:20:00 +0800 Subject: [PATCH 101/244] Moved primary and secondary tap handler inside view model --- Session/Home/HomeVC.swift | 46 +++++++------------------------- Session/Home/HomeViewModel.swift | 30 +++++++++++++++++++-- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index b8f76dec5a..33bacab024 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -98,15 +98,18 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi }() private lazy var appReviewPrompt: AppReviewPromptDialog = { - let prompt = AppReviewPromptDialog() + let result = AppReviewPromptDialog() // Layers - prompt.themeBorderColor = .borderSeparator - prompt.layer.borderWidth = 1 - prompt.layer.cornerRadius = 12 - prompt.themeBackgroundColor = .backgroundSecondary + result.themeBorderColor = .borderSeparator + result.layer.borderWidth = 1 + result.layer.cornerRadius = 12 + result.themeBackgroundColor = .backgroundSecondary + result.onPrimaryTapped = { [viewModel = self.viewModel] state in viewModel.handlePrimaryTappedForState(state) } + result.onSecondaryTapped = { [viewModel = self.viewModel] in viewModel.handleSecondayTappedForState($0) } + result.onCloseTapped = { [viewModel = self.viewModel] in viewModel.handlePromptChangeState(nil) } - return prompt + return result }() private lazy var newConversationButton: UIView = { @@ -350,13 +353,8 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi newConversationButton.center(.horizontal, in: view) newConversationButton.pin(.bottom, to: .bottom, of: view.safeAreaLayoutGuide, withInset: -Values.smallSpacing) - appReviewPrompt.onPrimaryTapped = { [weak self] state in self?.onHandlePrimaryTappedForState(state) } - appReviewPrompt.onSecondaryTapped = { [weak self] in self?.onHandleSecondayTappedForState($0) } - appReviewPrompt.onCloseTapped = { [weak self] in self?.viewModel.handlePromptChangeState(nil) } - // Preview prompt view.addSubview(appReviewPrompt) - appReviewPrompt.pin(.left, to: .left, of: view, withInset: 12) appReviewPrompt.pin(.right, to: .right, of: view, withInset: -12) appReviewPrompt.pin(.bottom, to: .top, of: newConversationButton, withInset: -10) @@ -832,29 +830,3 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi present(navigationController, animated: true, completion: nil) } } - -// MARK: - Alert for survey -private extension HomeVC { - func onHandlePrimaryTappedForState(_ state: AppReviewPromptState) { - switch state { - case .enjoyingSession: - viewModel.handlePromptChangeState(.rateSession) - viewModel.scheduleAppReviewRetry() - case .feedback: - // Close prompt before showing survery - viewModel.handlePromptChangeState(nil) - viewModel.submitFeedbackSurvery() - case .rateSession: - // Close prompt before showing app review - viewModel.handlePromptChangeState(nil) - viewModel.submitAppStoreReview() - } - } - - func onHandleSecondayTappedForState(_ state: AppReviewPromptState) { - switch state { - case .feedback, .rateSession: viewModel.handlePromptChangeState(nil) - case .enjoyingSession: viewModel.handlePromptChangeState(.feedback) - } - } -} diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 81d0c6b532..a36ceb3800 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -581,7 +581,8 @@ public class HomeViewModel: NavigatableStateHolder { ) } - @MainActor func submitAppStoreReview() { + @MainActor + func submitAppStoreReview() { dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 @@ -590,7 +591,8 @@ public class HomeViewModel: NavigatableStateHolder { } } - @MainActor func submitFeedbackSurvery() { + @MainActor + func submitFeedbackSurvery() { guard let url: URL = URL(string: Constants.feedback_url) else { return } var surverUrl: URL { @@ -635,6 +637,30 @@ public class HomeViewModel: NavigatableStateHolder { self.transitionToScreen(modal, transitionType: .present) } + + @MainActor + func handlePrimaryTappedForState(_ state: AppReviewPromptState) { + switch state { + case .enjoyingSession: + handlePromptChangeState(.rateSession) + scheduleAppReviewRetry() + case .feedback: + // Close prompt before showing survery + handlePromptChangeState(nil) + submitFeedbackSurvery() + case .rateSession: + // Close prompt before showing app review + handlePromptChangeState(nil) + submitAppStoreReview() + } + } + + func handleSecondayTappedForState(_ state: AppReviewPromptState) { + switch state { + case .feedback, .rateSession: handlePromptChangeState(nil) + case .enjoyingSession: handlePromptChangeState(.feedback) + } + } // MARK: - Functions From f46083d22027fd44fa66c63ab94950b46a82edf8 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 11:57:38 +0800 Subject: [PATCH 102/244] Updated app review prompt calls and updates --- Session/Home/HomeViewModel.swift | 80 ++++++++----------- Session/Path/PathVC.swift | 2 +- Session/Settings/AppearanceViewModel.swift | 5 +- Session/Settings/SettingsViewModel.swift | 2 +- SessionUIKit/Style Guide/Constants+URLs.swift | 1 + SessionUIKit/Style Guide/Constants.swift | 1 - 6 files changed, 37 insertions(+), 54 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index a36ceb3800..357d18cf68 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -138,6 +138,19 @@ public class HomeViewModel: NavigatableStateHolder { } static func initialState(using dependencies: Dependencies) -> State { + /// Check if incomplete app review can be shown again to user on next app launch + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + var promptState: AppReviewPromptState? + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .rateSession + } + return State( viewState: .loading, userProfile: Profile(id: dependencies[cache: .general].sessionId.hexString, name: ""), @@ -162,7 +175,7 @@ public class HomeViewModel: NavigatableStateHolder { orderSQL: SessionThreadViewModel.homeOrderSQL ), itemCache: [:], - appReviewPromptState: nil, + appReviewPromptState: promptState, appReviewPromptTimestamp: nil ) } @@ -213,17 +226,6 @@ public class HomeViewModel: NavigatableStateHolder { value: nil )) } - - /// Check if incomplete app review can be shown again to user on next app launch - let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - - if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - - appReviewPromptState = .rateSession - } } /// If there are no events we want to process then just return the current state @@ -389,30 +391,24 @@ public class HomeViewModel: NavigatableStateHolder { } } - groupedOtherEvents?[.userDefault]?.forEach { event in - if let updatedValue = event.value as? Bool { - - switch event.key { - case .userDefault(.hasVisitedPathScreen): - if updatedValue == true { - appReviewPromptState = .enjoyingSession - } - case .userDefault(.hasPressedDonateButton): - if updatedValue == true { - appReviewPromptState = .enjoyingSession - } - case .userDefault(.hasChangedTheme): - if updatedValue == true { - appReviewPromptState = .enjoyingSession - } - default: break - } - } - } - /// Next trigger should be ignored if `didShowAppReviewPrompt` is true if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] == true { appReviewPromptState = nil + } else { + groupedOtherEvents?[.userDefault]?.forEach { event in + switch (event.key, event.value) { + case (.userDefault(.hasVisitedPathScreen), let value as Bool) where value == true: + appReviewPromptState = .enjoyingSession + + case (.userDefault(.hasPressedDonateButton), let value as Bool) where value == true: + appReviewPromptState = .enjoyingSession + + case (.userDefault(.hasChangedTheme), let value as Bool) where value == true: + appReviewPromptState = .enjoyingSession + + default: break + } + } } if let event: HomeViewModelEvent = events.first?.value as? HomeViewModelEvent { @@ -537,12 +533,6 @@ public class HomeViewModel: NavigatableStateHolder { } // MARK: - Handle App review - private static func handleAppReviewTriggerFlag(_ flag: Bool) -> AppReviewPromptState? { - guard flag == true else { return nil } - - return .enjoyingSession - } - func viewDidAppear() { let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 @@ -559,13 +549,9 @@ public class HomeViewModel: NavigatableStateHolder { } func scheduleAppReviewRetry() { - let now = dependencies.dateNow - - guard let retryDate = Calendar.current.date(byAdding: .weekOfYear, value: 2, to: now) else { - return - } - - dependencies[defaults: .standard, key: .rateAppRetryDate] = retryDate + /// Wait 2 weeks before trying again + dependencies[defaults: .standard, key: .rateAppRetryDate] = dependencies.dateNow + .addingTimeInterval(2 * 7 * 24 * 60 * 60) } func handlePromptChangeState(_ state: AppReviewPromptState?) { @@ -593,7 +579,7 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor func submitFeedbackSurvery() { - guard let url: URL = URL(string: Constants.feedback_url) else { return } + guard let url: URL = URL(string: Constants.session_feedback_url) else { return } var surverUrl: URL { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 6dec383517..4bf304de7a 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -73,7 +73,7 @@ final class PathVC: BaseVC { setUpNavBar() setUpViewHierarchy() - if dependencies[defaults: .standard, key: .hasVisitedPathScreen] == false { + if !dependencies[defaults: .standard, key: .hasVisitedPathScreen] { dependencies[defaults: .standard, key: .hasVisitedPathScreen] = true } } diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index 44c397c1e3..6f93c3f507 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -149,9 +149,6 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ ) } - @MainActor private func didUpdateTheme(theme: Theme?) { - } - private static func sections( state: State, previousState: State, @@ -173,7 +170,7 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ onTap: { [dependencies = viewModel.dependencies] in ThemeManager.updateThemeState(theme: theme) // Update trigger only if it's not set to true - if dependencies[defaults: .standard, key: .hasChangedTheme] == false { + if !dependencies[defaults: .standard, key: .hasChangedTheme] { dependencies[defaults: .standard, key: .hasChangedTheme] = true } } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index f71c2e849a..2220e6279f 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -851,7 +851,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl self.transitionToScreen(modal, transitionType: .present) // Mark app review flag that donate button was tapped - if dependencies[defaults: .standard, key: .hasPressedDonateButton] == false { + if !dependencies[defaults: .standard, key: .hasPressedDonateButton] { dependencies[defaults: .standard, key: .hasPressedDonateButton] = true } } diff --git a/SessionUIKit/Style Guide/Constants+URLs.swift b/SessionUIKit/Style Guide/Constants+URLs.swift index e00ba52de6..d289535060 100644 --- a/SessionUIKit/Style Guide/Constants+URLs.swift +++ b/SessionUIKit/Style Guide/Constants+URLs.swift @@ -6,4 +6,5 @@ public extension Constants { static let session_staking_url = "https://docs.getsession.org/session-network/staking" static let session_token_url = "https://token.getsession.org" static let session_donations_url = "https://session.foundation/donate#app" + static let session_feedback_url = "https://www.surveymonkey.com/r/YLDZJR8" } diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index c4e70f61f0..ba9fe47fd6 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -16,5 +16,4 @@ public enum Constants { public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" public static let store_variant: String = "App Store" - public static let feedback_url: String = "https://www.surveymonkey.com/r/YLDZJR8" } From 6612affe9353c3b4ae44ca2421b252f0960a6c1d Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 13:35:42 +0800 Subject: [PATCH 103/244] Additional code clean ups and conventions --- .../View/AppReviewPromptDialog.swift | 4 +- Session/Home/HomeVC.swift | 37 +++++++++------- Session/Home/HomeViewModel.swift | 43 +++++++++++-------- .../Settings/DeveloperSettingsViewModel.swift | 8 +++- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 3be8c2360f..2faca30ad4 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -126,8 +126,8 @@ class AppReviewPromptDialog: UIView { isHidden = prompt == nil - titleLabel.text = prompt?.promptContent.title ?? "" - messageLabel.text = prompt?.promptContent.message ?? "" + titleLabel.text = prompt?.promptContent.title + messageLabel.text = prompt?.promptContent.message primaryButton.setTitle(prompt?.promptContent.primaryButtonTitle, for: .normal) secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 33bacab024..0339b599a6 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -44,15 +44,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi private var loadingConversationsLabelTopConstraint: NSLayoutConstraint? private var navBarProfileView: ProfilePictureView? - private lazy var tableViewBottomInsets: CGFloat = { - ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - ) - }() - private lazy var seedReminderView: SeedReminderView = { let result = SeedReminderView() result.accessibilityLabel = "Recovery phrase reminder" @@ -84,7 +75,12 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi result.contentInset = UIEdgeInsets( top: 0, left: 0, - bottom: tableViewBottomInsets, + bottom: ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + ), right: 0 ) result.showsVerticalScrollIndicator = false @@ -480,16 +476,27 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi self?.sections = updatedData } - // App reivew + // App reivew, check if `state.appReviewPromptState` has value and `state.appReviewPromptTimestamp` + // `state.appReviewPromptTimestamp` will only have value if triggered via viewDidAppear or review prompt events if let promptState = state.appReviewPromptState, state.appReviewPromptTimestamp != nil { appReviewPrompt.setReviewPrompt(promptState) - viewModel.dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - - tableView.contentInset.bottom = tableViewBottomInsets + (appReviewPrompt.frame.size.height + 24) + viewModel.didShowAppReviewPrompt() } else { appReviewPrompt.setReviewPrompt(nil) - tableView.contentInset.bottom = tableViewBottomInsets } + + tableView.contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + + ((state.appReviewPromptState != nil && state.appReviewPromptTimestamp != nil) ? (appReviewPrompt.frame.size.height + 24) : 0) + ), + right: 0 + ) } private func updateNavBarButtons( diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 357d18cf68..3144d8bbc0 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -554,6 +554,10 @@ public class HomeViewModel: NavigatableStateHolder { .addingTimeInterval(2 * 7 * 24 * 60 * 60) } + func didShowAppReviewPrompt() { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + } + func handlePromptChangeState(_ state: AppReviewPromptState?) { let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 @@ -581,21 +585,11 @@ public class HomeViewModel: NavigatableStateHolder { func submitFeedbackSurvery() { guard let url: URL = URL(string: Constants.session_feedback_url) else { return } - var surverUrl: URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return url - } - - // stringlint:ignore_contents - components.queryItems = [ - .init(name: "platform", value: "iOS"), - .init(name: "version", value: dependencies[cache: .appVersion].appVersion) - ] - - guard let finalURL = components.url else { return url } - - return finalURL - } + var surveyUrl: URL = url.appending(queryItems: [ + .init(name: "platform", value: "iOS"), + .init(name: "version", value: dependencies[cache: .appVersion].appVersion) + ]) + let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "urlOpen".localized(), @@ -610,11 +604,11 @@ public class HomeViewModel: NavigatableStateHolder { cancelStyle: .alert_text, hasCloseButton: true, onConfirm: { modal in - UIApplication.shared.open(surverUrl, options: [:], completionHandler: nil) + UIApplication.shared.open(surveyUrl, options: [:], completionHandler: nil) modal.dismiss(animated: true) }, onCancel: { modal in - UIPasteboard.general.string = surverUrl.absoluteString + UIPasteboard.general.string = surveyUrl.absoluteString modal.dismiss(animated: true) } @@ -707,3 +701,18 @@ private extension ObservedEvent { } } } + +private extension URL { + @available(iOS, introduced: 13.0, obsoleted: 16.0) + func appending(queryItems: [URLQueryItem]) -> URL { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + var existingItems = components.queryItems ?? [] + existingItems.append(contentsOf: queryItems) + components.queryItems = existingItems + + return components.url ?? self + } +} diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 61eb9f721b..e2e044ff5c 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -81,6 +81,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case truncatePubkeysInLogs case copyDocumentsPath case copyAppGroupPath + case resetAppReviewPrompt case defaultLogLevel case advancedLogging @@ -120,6 +121,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .truncatePubkeysInLogs: return "truncatePubkeysInLogs" case .copyDocumentsPath: return "copyDocumentsPath" case .copyAppGroupPath: return "copyAppGroupPath" + case .resetAppReviewPrompt: return "resetAppReviewPrompt" case .defaultLogLevel: return "defaultLogLevel" case .advancedLogging: return "advancedLogging" @@ -169,6 +171,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .truncatePubkeysInLogs: result.append(.truncatePubkeysInLogs); fallthrough case .copyDocumentsPath: result.append(.copyDocumentsPath); fallthrough case .copyAppGroupPath: result.append(.copyAppGroupPath); fallthrough + case .resetAppReviewPrompt: result.append(.resetAppReviewPrompt); fallthrough case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough case .advancedLogging: result.append(.advancedLogging); fallthrough @@ -410,7 +413,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } ), SessionCell.Info( - id: .copyAppGroupPath, + id: .resetAppReviewPrompt, title: "Reset App Review Prompt", subtitle: """ Clears user default settings for the app review prompt, enabling quicker testing of various display conditions. @@ -968,6 +971,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .copyDocumentsPath: break // Not a feature case .copyAppGroupPath: break // Not a feature + case .resetAppReviewPrompt: break case .resetSnodeCache: break // Not a feature case .createMockContacts: break // Not a feature case .exportDatabase: break // Not a feature @@ -1450,7 +1454,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 showToast( - text: "cleared".localized(), + text: "Cleared", backgroundColor: .backgroundSecondary ) } From dec6d265a20c21fd78507d62082c51a6c58b9c28 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 13:46:31 +0800 Subject: [PATCH 104/244] Added clear all emoji confirmation dialog --- .../ConversationVC+Interaction.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..ca5c97894c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1710,6 +1710,33 @@ extension ConversationVC: } func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { + // Dismiss current reaction sheet to present alert dialog + currentReactionListSheet?.dismiss(animated: true) + currentReactionListSheet = nil + + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "clearAll".localized(), + body: .attributedText( + "emojiReactsClearAll" + .put(key: "emoji", value: emoji) + .localizedFormatted(baseFont: ConfirmationModal.explanationFont) + ), + confirmTitle: "clear".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] modal in + // Call clear reaction event + self?.clearAllReactions(cellViewModel, for: emoji) + modal.dismiss(animated: true) + } + ) + ) + + present(modal, animated: true, completion: nil) + } + + func clearAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { guard cellViewModel.threadVariant == .community, let roomToken: String = viewModel.threadData.openGroupRoomToken, From 4be8cea6b7a72969473064a83ef60e95e9087023 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 22 Aug 2025 14:12:43 +0800 Subject: [PATCH 105/244] Fix showing 0 values in time remaining displays --- SessionUtilitiesKit/General/String+Utilities.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index bbe0503875..ec389cb861 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -147,7 +147,7 @@ public extension String { case .twoUnits: // 2 units, no localization, short version e.g 1w 1d dateComponentsFormatter.maximumUnitCount = 2 dateComponentsFormatter.unitsStyle = .abbreviated - dateComponentsFormatter.zeroFormattingBehavior = .dropLeading + dateComponentsFormatter.zeroFormattingBehavior = .dropAll // Changed from .dropLeading to also remove trailing 0's ei. 12h 0m -> 12h calendar.locale = Locale(identifier: "en-US") dateComponentsFormatter.calendar = calendar return dateComponentsFormatter.string(from: duration) ?? "" From 102d04cb4a50a398ee611b520ab42603dba748ef Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 25 Aug 2025 14:10:21 +1000 Subject: [PATCH 106/244] Fixed an issue with the home screen empty state and MR banner --- Session/Home/HomeViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index ade9c3f15e..71f81abccc 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -363,7 +363,7 @@ public class HomeViewModel: NavigatableStateHolder { /// Generate the new state return State( - viewState: (loadResult.info.totalCount == 0 ? + viewState: (loadResult.info.totalCount == 0 && unreadMessageRequestThreadCount == 0 ? .empty(isNewUser: (startedAsNewUser && !hasSavedThread && !hasSavedMessage)) : .loaded ), From 693cc9507f12ff24f023140009169f0eb6de72fa Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 09:08:05 +0800 Subject: [PATCH 107/244] Added confirmation dialog for deleting for everyone --- .../ConversationVC+Interaction.swift | 140 ++++++++++++------ 1 file changed, 98 insertions(+), 42 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ca5c97894c..c4f1883f98 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2341,7 +2341,7 @@ extension ConversationVC: cancelTitle: "cancel".localized(), cancelStyle: .alert_text, dismissOnConfirm: false, - onConfirm: { [weak self, dependencies = viewModel.dependencies] modal in + onConfirm: { [weak self] modal in /// Determine the selected action index let selectedIndex: Int = { switch modal.info.body { @@ -2356,51 +2356,26 @@ extension ConversationVC: } }() - /// Stop the messages audio if needed - messagesToDelete.forEach { cellViewModel in - self?.viewModel.stopAudioIfNeeded(for: cellViewModel) + if selectedIndex != 0 { + modal.dismiss(animated: true) } - /// Trigger the deletion behaviours - deletionBehaviours - .publisherForAction(at: selectedIndex, using: dependencies) - .showingBlockingLoading( - in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ? - self?.viewModel.navigatableState : - nil + if selectedIndex == 0 { + // Delete for self + self?.willDeleteMessages( + cellViewModel, + selectedIndex: selectedIndex, + dissmissModal: modal, + completion: completion ) - .sinkUntilComplete( - receiveCompletion: { result in - DispatchQueue.main.async { - switch result { - case .finished: - modal.dismiss(animated: true) { - /// Dispatch after a delay because becoming the first responder can cause - /// an odd appearance animation - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { - self?.viewModel.showToast( - text: "deleteMessageDeleted" - .putNumber(messagesToDelete.count) - .localized(), - backgroundColor: .backgroundSecondary, - inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing - ) - } - } - - case .failure: - self?.viewModel.showToast( - text: "deleteMessageFailed" - .putNumber(messagesToDelete.count) - .localized(), - backgroundColor: .backgroundSecondary, - inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing - ) - } - completion?() - } - } + } else { + // Delete for everyone + self?.showDeleteForEveryoneConfirmation( + cellViewModel, + selectedIndex: selectedIndex, + completion: completion ) + } }, afterClosed: { [weak self] in self?.becomeFirstResponder() @@ -2697,6 +2672,87 @@ extension ConversationVC: ) self.present(modal, animated: true) } + + private func showDeleteForEveryoneConfirmation(_ cellViewModel: MessageViewModel, selectedIndex: Int, completion: (() -> Void)?) { + let messagesToDelete: [MessageViewModel] = [cellViewModel] + + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "deleteMessage" + .putNumber(messagesToDelete.count) + .localized(), + body: .text("deleteMessagesDescriptionEveryone".localized()), + confirmTitle: "delete".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] modal in + self?.willDeleteMessages( + cellViewModel, + selectedIndex: selectedIndex, + dissmissModal: modal, + completion: completion + ) + } + ) + ) + + present(modal, animated: true, completion: nil) + } + + private func willDeleteMessages(_ cellViewModel: MessageViewModel, selectedIndex: Int, dissmissModal: ConfirmationModal, completion: (() -> Void)?) { + /// Retrieve the deletion actions for the selected message(s) of there are any + let messagesToDelete: [MessageViewModel] = [cellViewModel] + + guard let deletionBehaviours: MessageViewModel.DeletionBehaviours = self.viewModel.deletionActions(for: messagesToDelete) else { + return + } + + /// Stop the messages audio if needed + messagesToDelete.forEach { cellViewModel in + viewModel.stopAudioIfNeeded(for: cellViewModel) + } + + /// Trigger the deletion behaviours + deletionBehaviours + .publisherForAction(at: selectedIndex, using: viewModel.dependencies) + .showingBlockingLoading( + in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ? + viewModel.navigatableState : + nil + ) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + DispatchQueue.main.async { + switch result { + case .finished: + dissmissModal.dismiss(animated: true) { + /// Dispatch after a delay because becoming the first responder can cause + /// an odd appearance animation + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { + self?.viewModel.showToast( + text: "deleteMessageDeleted" + .putNumber(messagesToDelete.count) + .localized(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } + } + + case .failure: + self?.viewModel.showToast( + text: "deleteMessageFailed" + .putNumber(messagesToDelete.count) + .localized(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } + completion?() + } + } + ) + } // MARK: - VoiceMessageRecordingViewDelegate From 85a00408ca7f30c63f1a6c01cb1fe65690598c15 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 12:39:32 +0800 Subject: [PATCH 108/244] Fix some highlighted search bubble covered by keyboard --- Session/Conversations/ConversationVC.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 4922d700cd..07683ae677 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -2104,6 +2104,15 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour) self.focusedInteractionInfo = nil self.focusBehaviour = .none + + // Check if the last known keyboard frame exists, + // if it does not intersect with the target rectangle (the cell to be scrolled to), + // and if the keyboard's height is greater than 80 points (to check if keyboard is visible currently isKeyboardVisible is not set via notif). + if let keyboardFrame = lastKnownKeyboardFrame, !keyboardFrame.intersects(targetRect), keyboardFrame.height > 80 { + // If all conditions are met, scroll the table view to make the target rectangle visible. + // This is to ensure a cell is not covered by the keyboard. + self.tableView.scrollRectToVisible(targetRect, animated: true) + } return } From 162580125384d1c81ddfc518d18d09ad89de90c2 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 13:26:53 +0800 Subject: [PATCH 109/244] Revert "Updated invalid ONS search error message" This reverts commit bfc5f9bad1e4f82bc0084903a7e164496b3a2371. --- Session/Closed Groups/EditGroupViewModel.swift | 7 ++++++- Session/Home/New Conversation/NewMessageScreen.swift | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 0b496ca816..01916f533c 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -544,7 +544,12 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case .finished: break case .failure(let error): modalActivityIndicator.dismiss { - return showError("onsErrorNotRecognized".localized()) + switch error { + case SnodeAPIError.onsNotFound: + return showError("onsErrorNotRecognized".localized()) + default: + return showError("onsErrorUnableToSearch".localized()) + } } } }, diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index 93a6b76f15..c478eb8331 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -98,7 +98,12 @@ struct NewMessageScreen: View { case .failure(let error): modalActivityIndicator.dismiss { let message: String = { - return "onsErrorNotRecognized".localized() + switch error { + case SnodeAPIError.onsNotFound: + return "onsErrorNotRecognized".localized() + default: + return "onsErrorUnableToSearch".localized() + } }() errorString = message From 8fbd37a67c6113907635aa18220b9f4358c22771 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 13:27:01 +0800 Subject: [PATCH 110/244] Revert "Added confirmation dialog for deleting for everyone" This reverts commit 693cc9507f12ff24f023140009169f0eb6de72fa. --- .../ConversationVC+Interaction.swift | 140 ++++++------------ 1 file changed, 42 insertions(+), 98 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index c4f1883f98..ca5c97894c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2341,7 +2341,7 @@ extension ConversationVC: cancelTitle: "cancel".localized(), cancelStyle: .alert_text, dismissOnConfirm: false, - onConfirm: { [weak self] modal in + onConfirm: { [weak self, dependencies = viewModel.dependencies] modal in /// Determine the selected action index let selectedIndex: Int = { switch modal.info.body { @@ -2356,26 +2356,51 @@ extension ConversationVC: } }() - if selectedIndex != 0 { - modal.dismiss(animated: true) + /// Stop the messages audio if needed + messagesToDelete.forEach { cellViewModel in + self?.viewModel.stopAudioIfNeeded(for: cellViewModel) } - if selectedIndex == 0 { - // Delete for self - self?.willDeleteMessages( - cellViewModel, - selectedIndex: selectedIndex, - dissmissModal: modal, - completion: completion + /// Trigger the deletion behaviours + deletionBehaviours + .publisherForAction(at: selectedIndex, using: dependencies) + .showingBlockingLoading( + in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ? + self?.viewModel.navigatableState : + nil ) - } else { - // Delete for everyone - self?.showDeleteForEveryoneConfirmation( - cellViewModel, - selectedIndex: selectedIndex, - completion: completion + .sinkUntilComplete( + receiveCompletion: { result in + DispatchQueue.main.async { + switch result { + case .finished: + modal.dismiss(animated: true) { + /// Dispatch after a delay because becoming the first responder can cause + /// an odd appearance animation + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { + self?.viewModel.showToast( + text: "deleteMessageDeleted" + .putNumber(messagesToDelete.count) + .localized(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } + } + + case .failure: + self?.viewModel.showToast( + text: "deleteMessageFailed" + .putNumber(messagesToDelete.count) + .localized(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } + completion?() + } + } ) - } }, afterClosed: { [weak self] in self?.becomeFirstResponder() @@ -2672,87 +2697,6 @@ extension ConversationVC: ) self.present(modal, animated: true) } - - private func showDeleteForEveryoneConfirmation(_ cellViewModel: MessageViewModel, selectedIndex: Int, completion: (() -> Void)?) { - let messagesToDelete: [MessageViewModel] = [cellViewModel] - - let modal: ConfirmationModal = ConfirmationModal( - info: ConfirmationModal.Info( - title: "deleteMessage" - .putNumber(messagesToDelete.count) - .localized(), - body: .text("deleteMessagesDescriptionEveryone".localized()), - confirmTitle: "delete".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text, - onConfirm: { [weak self] modal in - self?.willDeleteMessages( - cellViewModel, - selectedIndex: selectedIndex, - dissmissModal: modal, - completion: completion - ) - } - ) - ) - - present(modal, animated: true, completion: nil) - } - - private func willDeleteMessages(_ cellViewModel: MessageViewModel, selectedIndex: Int, dissmissModal: ConfirmationModal, completion: (() -> Void)?) { - /// Retrieve the deletion actions for the selected message(s) of there are any - let messagesToDelete: [MessageViewModel] = [cellViewModel] - - guard let deletionBehaviours: MessageViewModel.DeletionBehaviours = self.viewModel.deletionActions(for: messagesToDelete) else { - return - } - - /// Stop the messages audio if needed - messagesToDelete.forEach { cellViewModel in - viewModel.stopAudioIfNeeded(for: cellViewModel) - } - - /// Trigger the deletion behaviours - deletionBehaviours - .publisherForAction(at: selectedIndex, using: viewModel.dependencies) - .showingBlockingLoading( - in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ? - viewModel.navigatableState : - nil - ) - .sinkUntilComplete( - receiveCompletion: { [weak self] result in - DispatchQueue.main.async { - switch result { - case .finished: - dissmissModal.dismiss(animated: true) { - /// Dispatch after a delay because becoming the first responder can cause - /// an odd appearance animation - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { - self?.viewModel.showToast( - text: "deleteMessageDeleted" - .putNumber(messagesToDelete.count) - .localized(), - backgroundColor: .backgroundSecondary, - inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing - ) - } - } - - case .failure: - self?.viewModel.showToast( - text: "deleteMessageFailed" - .putNumber(messagesToDelete.count) - .localized(), - backgroundColor: .backgroundSecondary, - inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing - ) - } - completion?() - } - } - ) - } // MARK: - VoiceMessageRecordingViewDelegate From c44f90b3247f75c5ec5fd492c5b8762c2cd2a9a3 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 13:30:03 +0800 Subject: [PATCH 111/244] Added variant check in isAudio short description --- SessionMessagingKit/Database/Models/Attachment.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index a6f3b6dc47..4110492f4c 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -651,7 +651,12 @@ extension Attachment { public var shortDescription: String { if isImage { return "image".localized() } - if isAudio { return "messageVoice".localized() } + if isAudio { + switch variant { + case .voiceMessage: return "messageVoice".localized() + case .standard: return "audio".localized() + } + } if isVideo { return "video".localized() } return "document".localized() } From 0b84bcf514b6d111779fd1e67acf755b47c32f56 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 13:31:22 +0800 Subject: [PATCH 112/244] Clean up comments --- SessionUtilitiesKit/General/String+Utilities.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index ec389cb861..55a22b5a95 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -144,10 +144,10 @@ public extension String { dateComponentsFormatter.unitsStyle = .full return dateComponentsFormatter.string(from: duration) ?? "" - case .twoUnits: // 2 units, no localization, short version e.g 1w 1d + case .twoUnits: // 2 units, no localization, short version e.g 1w 1d, remove trailing 0's e.g 12h 0m -> 12h dateComponentsFormatter.maximumUnitCount = 2 dateComponentsFormatter.unitsStyle = .abbreviated - dateComponentsFormatter.zeroFormattingBehavior = .dropAll // Changed from .dropLeading to also remove trailing 0's ei. 12h 0m -> 12h + dateComponentsFormatter.zeroFormattingBehavior = .dropAll calendar.locale = Locale(identifier: "en-US") dateComponentsFormatter.calendar = calendar return dateComponentsFormatter.string(from: duration) ?? "" From fc8937682ebccb6568d7adec50642b0afac00c4d Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 13:36:20 +0800 Subject: [PATCH 113/244] Removed out of place height check --- Session/Conversations/ConversationVC.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 07683ae677..ab8471b452 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -2107,8 +2107,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Check if the last known keyboard frame exists, // if it does not intersect with the target rectangle (the cell to be scrolled to), - // and if the keyboard's height is greater than 80 points (to check if keyboard is visible currently isKeyboardVisible is not set via notif). - if let keyboardFrame = lastKnownKeyboardFrame, !keyboardFrame.intersects(targetRect), keyboardFrame.height > 80 { + if let keyboardFrame = lastKnownKeyboardFrame, !keyboardFrame.intersects(targetRect) { // If all conditions are met, scroll the table view to make the target rectangle visible. // This is to ensure a cell is not covered by the keyboard. self.tableView.scrollRectToVisible(targetRect, animated: true) From 7806f6349734b6f6ece8548bd85200fe3f998612 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 16:25:32 +1000 Subject: [PATCH 114/244] Fixed an issue with processing reactions from SOGS --- SessionMessagingKit/Messages/Message+Origin.swift | 2 +- SessionMessagingKit/Open Groups/Models/SOGSMessage.swift | 4 ++-- SessionMessagingKit/Sending & Receiving/MessageReceiver.swift | 4 ++-- SessionMessagingKit/Sending & Receiving/MessageSender.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/SessionMessagingKit/Messages/Message+Origin.swift b/SessionMessagingKit/Messages/Message+Origin.swift index 4da490c326..0e0cedf2f2 100644 --- a/SessionMessagingKit/Messages/Message+Origin.swift +++ b/SessionMessagingKit/Messages/Message+Origin.swift @@ -16,7 +16,7 @@ public extension Message { case community( openGroupId: String, sender: String, - timestamp: TimeInterval, + timestamp: TimeInterval?, messageServerId: Int64, whisper: Bool, whisperMods: Bool, diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 219021f442..ef9aa97060 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -25,7 +25,7 @@ extension OpenGroupAPI { public let id: Int64 public let sender: String? - public let posted: TimeInterval + public let posted: TimeInterval? public let edited: TimeInterval? public let deleted: Bool? public let seqNo: Int64 @@ -105,7 +105,7 @@ extension OpenGroupAPI.Message { self = OpenGroupAPI.Message( id: try container.decode(Int64.self, forKey: .id), sender: try container.decodeIfPresent(String.self, forKey: .sender), - posted: try container.decode(TimeInterval.self, forKey: .posted), + posted: try container.decodeIfPresent(TimeInterval.self, forKey: .posted), edited: try container.decodeIfPresent(TimeInterval.self, forKey: .edited), deleted: try container.decodeIfPresent(Bool.self, forKey: .deleted), seqNo: try container.decode(Int64.self, forKey: .seqNo), diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 912f308871..fa3d97aa08 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -28,7 +28,7 @@ public enum MessageReceiver { var customProto: SNProtoContent? = nil var customMessage: Message? = nil let sender: String - let sentTimestampMs: UInt64 + let sentTimestampMs: UInt64? let serverHash: String? let openGroupServerMessageId: UInt64? let openGroupWhisper: Bool @@ -53,7 +53,7 @@ public enum MessageReceiver { uniqueIdentifier = "\(messageServerId)" plaintext = data.removePadding() // Remove the padding sender = messageSender - sentTimestampMs = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency + sentTimestampMs = timestamp.map { UInt64(floor($0 * 1000)) } // Convert to ms for database consistency serverHash = nil openGroupServerMessageId = UInt64(messageServerId) openGroupWhisper = messageWhisper diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 510819e3f4..9c89f145c4 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -300,9 +300,9 @@ public final class MessageSender { .map { _, response in let updatedMessage: Message = message updatedMessage.openGroupServerMessageId = UInt64(response.id) - updatedMessage.sentTimestampMs = UInt64(floor(response.posted * 1000)) + updatedMessage.sentTimestampMs = response.posted.map { UInt64(floor($0 * 1000)) } - return (updatedMessage, Int64(floor(response.posted * 1000)), nil) + return (updatedMessage, response.posted.map { Int64(floor($0 * 1000)) }, nil) } } From 68e66cb858fe640faa28b2110e09005c7259b37b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 16:38:36 +1000 Subject: [PATCH 115/244] Fixed a string linter issue due to a string change --- Session/Meta/Translations/InfoPlist.xcstrings | 52 ++++++++++++++++++- .../Settings/PrivacySettingsViewModel.swift | 4 +- SessionUIKit/Style Guide/Constants.swift | 1 + 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 6653336dd1..86f4dc37b7 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -532,7 +532,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "callsVoiceAndVideoBeta".localized(), - body: .text("callsVoiceAndVideoModalDescription".localized()), + body: .text("callsVoiceAndVideoModalDescription" + .put(key: "session_foundation", value: Constants.session_foundation) + .localized()), showCondition: .disabled, confirmTitle: "theContinue".localized(), confirmStyle: .danger, diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index 7ff7fa8564..f8cd66b791 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -15,4 +15,5 @@ public enum Constants { public static let usd_name_short: String = "USD" public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" + public static let session_foundation: String = "Session Foundation" } From ee136a4f8d168f4096c4bbb7eb7b1e8df404076e Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 14:41:49 +0800 Subject: [PATCH 116/244] Aligned padding and fonts with design --- .../App Review/AppReviewPromptModel.swift | 2 +- .../View/AppReviewPromptDialog.swift | 61 ++++++++++--------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 4fb183e058..d413bfcf2e 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -42,7 +42,7 @@ extension AppReviewPromptState { .localized(), message: "rateSessionModalDescription" .put(key: "app_name", value: Constants.app_name) - .put(key: "storevariant", value: Constants.store_variant) + .put(key: "storevariant", value: "App Store") .localized(), primaryButtonTitle: "rateSessionApp".localized(), secondaryButtonTitle: "notNow".localized() diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 2faca30ad4..272a86463d 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -8,27 +8,35 @@ class AppReviewPromptDialog: UIView { var onPrimaryTapped: ((AppReviewPromptState) -> Void)? var onSecondaryTapped: ((AppReviewPromptState) -> Void)? - private lazy var closeButton: UIButton = { - let result = UIButton(type: .custom) - result.setImage( - UIImage(named: "X")? - .withRenderingMode(.alwaysTemplate), - for: .normal + private static let closeSize: CGFloat = 24 + + private lazy var closeButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.close() }) + .withConfiguration( + UIButton.Configuration + .plain() + .withImage(UIImage(named: "X")?.withRenderingMode(.alwaysTemplate)) + .withContentInsets(NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)) ) - result.themeTintColor = .textPrimary - result.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside) - result.set(.width, to: Values.largeSpacing) - result.set(.height, to: Values.largeSpacing) - - return result - }() + .withConfigurationUpdateHandler { button in + switch button.state { + case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed + default: button.imageView?.tintAdjustmentMode = .normal + } + } + .withImageViewContentMode(.scaleAspectFit) + .withThemeTintColor(.textPrimary) + .withAccessibility( + identifier: "Close button" + ) + .with(.width, of: AppReviewPromptDialog.closeSize) + .with(.height, of: AppReviewPromptDialog.closeSize) private lazy var titleLabel: UILabel = { let result = UILabel() result.textAlignment = .center result.numberOfLines = 0 result.themeTextColor = .textPrimary - result.font = .systemFont(ofSize: 18, weight: .bold) + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) return result }() @@ -38,7 +46,7 @@ class AppReviewPromptDialog: UIView { result.textAlignment = .center result.numberOfLines = 0 result.themeTextColor = .textSecondary - result.font = .systemFont(ofSize: 16, weight: .regular) + result.font = ConfirmationModal.explanationFont return result }() @@ -47,7 +55,7 @@ class AppReviewPromptDialog: UIView { let result = UIButton(type: .custom) result.setThemeTitleColor(.sessionButton_text, for: .normal) result.setThemeTitleColor(.sessionButton_highlight, for: .highlighted) - result.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) + result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.titleLabel?.numberOfLines = 0 result.titleLabel?.textAlignment = .center result.addTarget(self, action: #selector(primaryEvent), for: .touchUpInside) @@ -59,7 +67,7 @@ class AppReviewPromptDialog: UIView { let result = UIButton(type: .custom) result.setThemeTitleColor(.textPrimary, for: .normal) result.setThemeTitleColor(.textSecondary, for: .highlighted) - result.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) + result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.titleLabel?.numberOfLines = 0 result.titleLabel?.textAlignment = .center result.addTarget(self, action: #selector(secondaryEvent), for: .touchUpInside) @@ -96,11 +104,11 @@ class AppReviewPromptDialog: UIView { result.distribution = .fill result.spacing = 8 result.isLayoutMarginsRelativeArrangement = true - result.layoutMargins = .init( - top: 0, - left: Values.largeSpacing, - bottom: 0, - right: Values.largeSpacing + result.layoutMargins = UIEdgeInsets( + top: Values.largeSpacing, + left: Values.veryLargeSpacing, + bottom: Values.verySmallSpacing, + right: Values.veryLargeSpacing ) return result @@ -158,12 +166,9 @@ private extension AppReviewPromptDialog { } func setupLayout() { - closeButton.pin(.top, to: .top, of: self, withInset: Values.mediumSmallSpacing) - closeButton.pin(.right, to: .right, of: self, withInset: -Values.mediumSmallSpacing) + closeButton.pin(.top, to: .top, of: self, withInset: Values.smallSpacing) + closeButton.pin(.right, to: .right, of: self, withInset: -Values.smallSpacing) - contentStack.pin(.top, to: .bottom, of: closeButton) - contentStack.pin(.left, to: .left, of: self, withInset: Values.mediumSmallSpacing) - contentStack.pin(.right, to: .right, of: self, withInset: -Values.mediumSmallSpacing) - contentStack.pin(.bottom, to: .bottom, of: self, withInset: -Values.mediumSmallSpacing) + contentStack.pin(to: self) } } From 7e73aa34d93b5ac29821eddf176237b0ba108444 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 26 Aug 2025 16:09:21 +0800 Subject: [PATCH 117/244] Clean up app review prompt observer logic --- .../View/AppReviewPromptDialog.swift | 4 +- Session/Home/HomeVC.swift | 50 +++++-------- Session/Home/HomeViewModel.swift | 70 ++++++++++--------- SessionUIKit/Style Guide/Constants.swift | 1 - 4 files changed, 56 insertions(+), 69 deletions(-) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 272a86463d..2b15dc067c 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -9,7 +9,7 @@ class AppReviewPromptDialog: UIView { var onSecondaryTapped: ((AppReviewPromptState) -> Void)? private static let closeSize: CGFloat = 24 - + private lazy var closeButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.close() }) .withConfiguration( UIButton.Configuration @@ -161,8 +161,8 @@ class AppReviewPromptDialog: UIView { private extension AppReviewPromptDialog { func setupHierarchy() { - addSubview(closeButton) addSubview(contentStack) + addSubview(closeButton) } func setupLayout() { diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 0339b599a6..104479c1e8 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -72,17 +72,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let result = UITableView() result.separatorStyle = .none result.themeBackgroundColor = .clear - result.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - ), - right: 0 - ) result.showsVerticalScrollIndicator = false result.register(view: MessageRequestsCell.self) result.register(view: FullConversationCell.self) @@ -450,6 +439,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // don't want to trigger the callbacks until a successful load) guard state.viewState != .loading else { return } + // App reivew, check if `state.appReviewPromptState` has value + // `state.appReviewPromptState` will only have value if triggered via viewDidAppear or review prompt events + appReviewPrompt.setReviewPrompt(state.appReviewPromptState) + + tableView.contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + + (state.appReviewPromptState != nil ? (appReviewPrompt.frame.size.height + 24) : 0) + ), + right: 0 + ) + // Reload the table content (update without animations on the first render) guard initialConversationLoadComplete else { sections = state.sections(viewModel: viewModel) @@ -475,28 +481,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi ) { [weak self] updatedData in self?.sections = updatedData } - - // App reivew, check if `state.appReviewPromptState` has value and `state.appReviewPromptTimestamp` - // `state.appReviewPromptTimestamp` will only have value if triggered via viewDidAppear or review prompt events - if let promptState = state.appReviewPromptState, state.appReviewPromptTimestamp != nil { - appReviewPrompt.setReviewPrompt(promptState) - viewModel.didShowAppReviewPrompt() - } else { - appReviewPrompt.setReviewPrompt(nil) - } - - tableView.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + - ((state.appReviewPromptState != nil && state.appReviewPromptTimestamp != nil) ? (appReviewPrompt.frame.size.height + 24) : 0) - ), - right: 0 - ) } private func updateNavBarButtons( diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 3144d8bbc0..28d4fa9f91 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -47,7 +47,24 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor init(using dependencies: Dependencies) { self.dependencies = dependencies self.userSessionId = dependencies[cache: .general].sessionId - self.state = State.initialState(using: dependencies) + + /// Check if incomplete app review can be shown again to user on next app launch + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + var promptState: AppReviewPromptState? + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .rateSession + } + + self.state = State.initialState( + using: dependencies, + appReviewPromptState: promptState + ) /// Bind the state self.observationTask = ObservationBuilder @@ -59,7 +76,7 @@ public class HomeViewModel: NavigatableStateHolder { } public struct HomeViewModelEvent: Hashable { - let appReviewPromptTimestamp: TimeInterval? + let pendingAppReviewPromptState: AppReviewPromptState? let appReviewPromptState: AppReviewPromptState? } @@ -84,7 +101,7 @@ public class HomeViewModel: NavigatableStateHolder { let loadedPageInfo: PagedData.LoadedInfo let itemCache: [String: SessionThreadViewModel] let appReviewPromptState: AppReviewPromptState? - let appReviewPromptTimestamp: TimeInterval? + let pendingAppReviewPromptState: AppReviewPromptState? @MainActor public func sections(viewModel: HomeViewModel) -> [SectionModel] { HomeViewModel.sections(state: self, viewModel: viewModel) @@ -137,20 +154,7 @@ public class HomeViewModel: NavigatableStateHolder { return result } - static func initialState(using dependencies: Dependencies) -> State { - /// Check if incomplete app review can be shown again to user on next app launch - let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - - var promptState: AppReviewPromptState? - - if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - - promptState = .rateSession - } - + static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?) -> State { return State( viewState: .loading, userProfile: Profile(id: dependencies[cache: .general].sessionId.hexString, name: ""), @@ -175,8 +179,8 @@ public class HomeViewModel: NavigatableStateHolder { orderSQL: SessionThreadViewModel.homeOrderSQL ), itemCache: [:], - appReviewPromptState: promptState, - appReviewPromptTimestamp: nil + appReviewPromptState: nil, + pendingAppReviewPromptState: appReviewPromptState ) } } @@ -199,7 +203,7 @@ public class HomeViewModel: NavigatableStateHolder { var loadResult: PagedData.LoadResult = previousState.loadedPageInfo.asResult var itemCache: [String: SessionThreadViewModel] = previousState.itemCache var appReviewPromptState: AppReviewPromptState? = previousState.appReviewPromptState - var appReviewPromptTimestamp: TimeInterval? = nil + var pendingAppReviewPromptState: AppReviewPromptState? = previousState.pendingAppReviewPromptState /// Store a local copy of the events so we can manipulate it based on the state changes var eventsToProcess: [ObservedEvent] = events @@ -393,18 +397,18 @@ public class HomeViewModel: NavigatableStateHolder { /// Next trigger should be ignored if `didShowAppReviewPrompt` is true if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] == true { - appReviewPromptState = nil + pendingAppReviewPromptState = nil } else { groupedOtherEvents?[.userDefault]?.forEach { event in switch (event.key, event.value) { case (.userDefault(.hasVisitedPathScreen), let value as Bool) where value == true: - appReviewPromptState = .enjoyingSession + pendingAppReviewPromptState = .enjoyingSession case (.userDefault(.hasPressedDonateButton), let value as Bool) where value == true: - appReviewPromptState = .enjoyingSession + pendingAppReviewPromptState = .enjoyingSession case (.userDefault(.hasChangedTheme), let value as Bool) where value == true: - appReviewPromptState = .enjoyingSession + pendingAppReviewPromptState = .enjoyingSession default: break } @@ -412,7 +416,7 @@ public class HomeViewModel: NavigatableStateHolder { } if let event: HomeViewModelEvent = events.first?.value as? HomeViewModelEvent { - appReviewPromptTimestamp = event.appReviewPromptTimestamp + pendingAppReviewPromptState = event.pendingAppReviewPromptState appReviewPromptState = event.appReviewPromptState } @@ -433,7 +437,7 @@ public class HomeViewModel: NavigatableStateHolder { loadedPageInfo: loadResult.info, itemCache: itemCache, appReviewPromptState: appReviewPromptState, - appReviewPromptTimestamp: appReviewPromptTimestamp + pendingAppReviewPromptState: pendingAppReviewPromptState ) } @@ -533,16 +537,18 @@ public class HomeViewModel: NavigatableStateHolder { } // MARK: - Handle App review - func viewDidAppear() { - let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + @MainActor func viewDidAppear() { + guard state.pendingAppReviewPromptState != nil else { return } DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in + self.didShowAppReviewPrompt() + dependencies.notifyAsync( priority: .immediate, key: .updateScreen(HomeViewModel.self), value: HomeViewModelEvent( - appReviewPromptTimestamp: timestamp, - appReviewPromptState: state.appReviewPromptState + pendingAppReviewPromptState: nil, + appReviewPromptState: state.pendingAppReviewPromptState ) ) } @@ -559,13 +565,11 @@ public class HomeViewModel: NavigatableStateHolder { } func handlePromptChangeState(_ state: AppReviewPromptState?) { - let timestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - dependencies.notifyAsync( priority: .immediate, key: .updateScreen(HomeViewModel.self), value: HomeViewModelEvent( - appReviewPromptTimestamp: timestamp, + pendingAppReviewPromptState: nil, appReviewPromptState: state ) ) diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index ba9fe47fd6..7ff7fa8564 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -15,5 +15,4 @@ public enum Constants { public static let usd_name_short: String = "USD" public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" - public static let store_variant: String = "App Store" } From 1488bf89676f85bfbb4c38c00d3b01d46d17258a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Aug 2025 10:16:22 +1000 Subject: [PATCH 118/244] Centralised message deletion logic --- .../ConversationVC+Interaction.swift | 7 +- .../MediaPageViewController.swift | 9 +- .../MediaTileViewController.swift | 16 ++-- Session/Meta/AppDelegate.swift | 2 +- .../Database/Models/ClosedGroup.swift | 15 +--- .../DisappearingMessageConfiguration.swift | 76 +++++++++------- .../Database/Models/Interaction.swift | 89 +++++++++++++++++-- .../Database/Models/SessionThread.swift | 29 ++++-- .../Jobs/DisappearingMessagesJob.swift | 27 +++--- .../Jobs/GarbageCollectionJob.swift | 9 +- .../Jobs/GetExpirationJob.swift | 5 +- .../Open Groups/OpenGroupManager.swift | 8 +- .../MessageReceiver+Groups.swift | 14 +-- .../MessageViewModel+DeletionActions.swift | 26 ++---- .../Open Groups/OpenGroupManagerSpec.swift | 6 +- .../LibSession/Types/ObservingDatabase.swift | 36 +++++++- 16 files changed, 245 insertions(+), 129 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..063d46b96b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2991,10 +2991,11 @@ extension ConversationVC { .writePublisher { [dependencies = viewModel.dependencies] db in /// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a /// duplicate one from inside the group history) - _ = try Interaction - .filter(Interaction.Columns.threadId == group.id) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == group.id), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) /// Optimistically insert a `standard` member for the current user in this group (it'll be update to the correct /// one once we receive the first `GROUP_MEMBERS` config message but adding it here means the `canWrite` diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 51523207fb..63bb6e82d7 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -592,10 +592,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou ) // Delete any interactions which had all of their attachments removed - _ = try Interaction - .filter(id: itemToDelete.interactionId) - .having(Interaction.interactionAttachments.isEmpty) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.id == itemToDelete.interactionId), + .hasAttachments(false) + ) } } actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel)) diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index 5518050f80..a5cc997b8c 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -733,16 +733,12 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour ) // Delete any interactions which had all of their attachments removed - try items.forEach { item in - let remainingAttachmentCount: Int = try InteractionAttachment - .filter(InteractionAttachment.Columns.interactionId == item.interactionId) - .fetchCount(db) - - if remainingAttachmentCount == 0 { - _ = try Interaction.deleteOne(db, id: item.interactionId) - db.addMessageEvent(id: item.interactionId, threadId: threadId, type: .deleted) - } - } + try Interaction.deleteWhere( + db, + .filter(items.map { $0.interactionId }.contains(Interaction.Columns.id)), + .filter(Interaction.Columns.threadId == threadId), + .hasAttachments(false) + ) } self?.endSelectMode() diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 6299100d40..9252c1016e 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -463,7 +463,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// We need to do a clean up for disappear after send messages that are received by push notifications before /// the app set up the main screen and load initial data to prevent a case when the PagedDatabaseObserver /// hasn't been setup yet then the conversation screen can show stale (ie. deleted) interactions incorrectly - DisappearingMessagesJob.cleanExpiredMessagesOnLaunch(using: dependencies) + DisappearingMessagesJob.cleanExpiredMessagesOnResume(using: dependencies) /// Now that the database is setup we can load in any messages which were processed by the extensions (flag that we will load /// them in this thread and create a task to _actually_ load them asynchronously diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index e525398357..ce182b7a81 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -334,19 +334,7 @@ public extension ClosedGroup { } if dataToRemove.contains(.messages) { - struct InteractionThreadInfo: Codable, FetchableRecord, Hashable { - let id: Int64 - let threadId: String - } - - let interactionInfo: Set = try Interaction - .select(.id, .threadId) - .filter(threadIds.contains(Interaction.Columns.threadId)) - .asRequest(of: InteractionThreadInfo.self) - .fetchSet(db) - try Interaction.deleteAll(db, ids: interactionInfo.map { $0.id }) - - interactionInfo.forEach { db.addMessageEvent(id: $0.id, threadId: $0.threadId, type: .deleted) } + try Interaction.deleteWhere(db, .filter(threadIds.contains(Interaction.Columns.threadId))) /// Delete any `MessageDeduplication` entries that we want to reprocess if the member gets /// re-invited to the group with historic access (these are repeatable records so won't cause issues if we re-run them) @@ -381,6 +369,7 @@ public extension ClosedGroup { } if dataToRemove.contains(.thread) { + try Interaction.deleteWhere(db, .filter(threadIds.contains(Interaction.Columns.threadId))) try SessionThread // Intentionally use `deleteAll` here as this gets triggered via `deleteOrLeave` .filter(ids: threadIds) .deleteAll(db) diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 22c7d9a580..1950e7239c 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -248,11 +248,12 @@ public extension DisappearingMessagesConfiguration { using dependencies: Dependencies ) throws { guard threadVariant == .contact else { - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), .filter(Interaction.Columns.expiresInSeconds != self.durationSeconds) - .deleteAll(db) + ) return } @@ -260,28 +261,41 @@ public extension DisappearingMessagesConfiguration { switch (self.isEnabled, self.type) { case (false, _): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) - .filter(Interaction.Columns.expiresInSeconds != 0) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), + .filter(Interaction.Columns.expiresInSeconds != 0), + ) case (true, .disappearAfterRead): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) - .filter(!(Interaction.Columns.expiresInSeconds == self.durationSeconds && Interaction.Columns.expiresStartedAtMs != Interaction.Columns.timestampMs)) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), + .filter( + !( + Interaction.Columns.expiresInSeconds == self.durationSeconds && + Interaction.Columns.expiresStartedAtMs != Interaction.Columns.timestampMs + ) + ) + ) case (true, .disappearAfterSend): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) - .filter(!(Interaction.Columns.expiresInSeconds == self.durationSeconds && Interaction.Columns.expiresStartedAtMs == Interaction.Columns.timestampMs)) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), + .filter( + !( + Interaction.Columns.expiresInSeconds == self.durationSeconds && + Interaction.Columns.expiresStartedAtMs == Interaction.Columns.timestampMs + ) + ) + ) default: break } @@ -298,16 +312,18 @@ public extension DisappearingMessagesConfiguration { ) throws -> MessageReceiver.InsertedInteractionInfo? { switch threadVariant { case .contact: - _ = try Interaction - .filter(Interaction.Columns.threadId == threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), .filter(Interaction.Columns.authorId == authorId) - .deleteAll(db) + ) case .legacyGroup, .group: - _ = try Interaction - .filter(Interaction.Columns.threadId == threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + ) case .community: break } diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 59373a3201..ab50549c0b 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -893,7 +893,7 @@ public extension Interaction { } } - struct ThreadInfo: FetchableRecord, Codable { + struct ThreadInfo: FetchableRecord, Codable, Hashable { public let id: Int64 public let threadId: String @@ -1295,6 +1295,19 @@ public extension Interaction.Variant { // MARK: - Deletion public extension Interaction { + enum Filter { + case filter(SQLSpecificExpressible) + case hasAttachments(Bool) + case deleteAll + + var isDeleteAll: Bool { + switch self { + case .deleteAll: return true + default: return false + } + } + } + private struct InteractionVariantInfo: Codable, FetchableRecord { public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { @@ -1448,7 +1461,9 @@ public extension Interaction { .filter { $0.variant.isInfoMessage } .compactMap { $0.id } .asSet() - _ = try Interaction.deleteAll(db, ids: infoMessageIds) + try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction.deleteAll(db, ids: infoMessageIds) + } let localOnly: Bool = (options.contains(.local) && !options.contains(.network)) @@ -1470,9 +1485,11 @@ public extension Interaction { }() if options.contains(.noArtifacts) { - try Interaction - .filter(ids: info.map { $0.id }) - .deleteAll(db) + try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction + .filter(ids: info.map { $0.id }) + .deleteAll(db) + } } else { try Interaction .filter(ids: info.map { $0.id }) @@ -1506,4 +1523,66 @@ public extension Interaction { } } } + + /// Whenever a message gets deleted we need to send an event to ensure the home screen updates correctly, this function manages + /// that logic so should be used instead of `delete(db)`/`deleteAll(db)` + @discardableResult static func deleteWhere( + _ db: ObservingDatabase, + _ filters: Filter... + ) throws -> Int { + var query: QueryInterfaceRequest = Interaction.select(.id, .threadId) + let shouldDeleteAll: Bool = filters.contains(where: { $0.isDeleteAll }) + var hasAttachmentsFilter: Bool? = nil + + /// Apply each of the filters to the query (unless the filters contains `deleteAll`, in which case ignore all filters) + if !shouldDeleteAll { + for filter in filters { + switch filter { + case .deleteAll: break + case .filter(let expressible): query = query.filter(expressible) + case .hasAttachments(let value): hasAttachmentsFilter = value + } + } + } + + /// Get the `id`/`threadId` combination + var info: Set = try query.asRequest(of: ThreadInfo.self).fetchSet(db) + + /// Since the `hasAttachments` filter is based on another table, we need custom logic for it so fetch all ids with attachments + /// and filter the above result based on the `hasAttachments` value + switch (shouldDeleteAll, hasAttachmentsFilter) { + case (true, _), (_, .none): break + case (_, .some(let requireAttachments)): + let interactionIdsWithAttachments: Set = try InteractionAttachment + .filter(info.map { $0.id }.contains(InteractionAttachment.Columns.interactionId)) + .asRequest(of: Int64.self) + .fetchSet(db) + + info = info.filter { interactionIdsWithAttachments.contains($0.id) == requireAttachments } + } + + /// Actually delete the messages + let numDeleted: Int = try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction + .filter(info.map { $0.id }.contains(Interaction.Columns.id)) + .deleteAll(db) + } + + /// Notify any observers of message deletion + info.forEach { info in + db.addMessageEvent(id: info.id, threadId: info.threadId, type: .deleted) + } + + return numDeleted + } +} + +extension Interaction: LoggingDatabaseRecord { + public func logDeletion() { Interaction.logDeletion() } + public static func logDeletion() { + Log.critical("Incorrectly deleted interaction directly instead of via `deleteWhere` or `markAsDeleted`.") + #if DEBUG + fatalError("Incorrectly deleted interaction directly instead of via `deleteWhere` or `markAsDeleted`.") + #endif + } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 3e069e9268..5a8c10ae42 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -582,9 +582,10 @@ public extension SessionThread { case .hideContactConversationAndDeleteContentDirectly: // Clear any interactions for the deleted thread - _ = try Interaction + try Interaction.deleteWhere( + db, .filter(threadIds.contains(Interaction.Columns.threadId)) - .deleteAll(db) + ) // Hide the threads try SessionThread.updateVisibility( @@ -598,7 +599,13 @@ public extension SessionThread { try MessageDeduplication.deleteIfNeeded(db, threadIds: threadIds, using: dependencies) case .deleteContactConversationAndMarkHidden: - _ = try SessionThread + // Clear any interactions for the deleted thread + try Interaction.deleteWhere( + db, + .filter(remainingThreadIds.contains(Interaction.Columns.threadId)) + ) + + try SessionThread .filter(ids: remainingThreadIds) .deleteAll(db) @@ -620,9 +627,10 @@ public extension SessionThread { // hidden locally rather than deleted) if threadIds.contains(userSessionId.hexString) { // Clear any interactions for the deleted thread - _ = try Interaction + try Interaction.deleteWhere( + db, .filter(Interaction.Columns.threadId == userSessionId.hexString) - .deleteAll(db) + ) try SessionThread.updateVisibility( db, @@ -643,15 +651,20 @@ public extension SessionThread { // custom data for this contact) try LibSession.remove(db, contactIds: Array(remainingThreadIds), using: dependencies) - _ = try Profile + try Profile .filter(ids: remainingThreadIds) .updateAll(db, Profile.Columns.nickname.set(to: nil)) - _ = try Contact + try Contact .filter(ids: remainingThreadIds) .deleteAll(db) - _ = try SessionThread + try Interaction.deleteWhere( + db, + .filter(remainingThreadIds.contains(Interaction.Columns.threadId)) + ) + + try SessionThread .filter(ids: remainingThreadIds) .deleteAll(db) diff --git a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift index 0f7e16f812..14b34fe97d 100644 --- a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift @@ -35,19 +35,11 @@ public enum DisappearingMessagesJob: JobExecutor { var numDeleted: Int = -1 let updatedJob: Job? = dependencies[singleton: .storage].write { db in - let interactionInfo: Set = try Interaction - .select(.id, .threadId) - .filter(Interaction.Columns.expiresStartedAtMs != nil) + numDeleted = try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresStartedAtMs != nil), .filter((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)) <= timestampNowMs) - .asRequest(of: InteractionThreadInfo.self) - .fetchSet(db) - try Interaction.filter(interactionInfo.map { $0.id }.contains(Interaction.Columns.id)).deleteAll(db) - numDeleted = interactionInfo.count - - // Notify of the deletion - interactionInfo.forEach { info in - db.addMessageEvent(id: info.id, threadId: info.threadId, type: .deleted) - } + ) // Update the next run timestamp for the DisappearingMessagesJob (if the call // to 'updateNextRunIfNeeded' returns 'nil' then it doesn't need to re-run so @@ -73,20 +65,21 @@ private struct InteractionThreadInfo: Codable, FetchableRecord, Hashable { // MARK: - Clean expired messages on app launch public extension DisappearingMessagesJob { - static func cleanExpiredMessagesOnLaunch(using dependencies: Dependencies) { + static func cleanExpiredMessagesOnResume(using dependencies: Dependencies) { guard dependencies[cache: .general].userExists else { return } let timestampNowMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() var numDeleted: Int = -1 dependencies[singleton: .storage].write { db in - numDeleted = try Interaction - .filter(Interaction.Columns.expiresStartedAtMs != nil) + numDeleted = try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresStartedAtMs != nil), .filter((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)) <= timestampNowMs) - .deleteAll(db) + ) } - Log.info(.cat, "Deleted \(numDeleted) expired messages on app launch.") + Log.info(.cat, "Deleted \(numDeleted) expired messages on app resume.") } } diff --git a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift index b799931f85..5629508213 100644 --- a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift @@ -309,11 +309,12 @@ public enum GarbageCollectionJob: JobExecutor { /// Remove interactions which should be disappearing after read but never be read within 14 days if finalTypesToCollect.contains(.expiredUnreadDisappearingMessages) { - _ = try Interaction - .filter(Interaction.Columns.expiresInSeconds != 0) - .filter(Interaction.Columns.expiresStartedAtMs == nil) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresInSeconds != 0), + .filter(Interaction.Columns.expiresStartedAtMs == nil), .filter(Interaction.Columns.timestampMs < (timestampNow - fourteenDaysInSeconds) * 1000) - .deleteAll(db) + ) } if finalTypesToCollect.contains(.expiredPendingReadReceipts) { diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index bf15ef97b7..e367edd0df 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -90,9 +90,10 @@ public enum GetExpirationJob: JobExecutor { hashesWithNoExiprationInfo = hashesWithNoExiprationInfo.subtracting(inferredExpiredMessageHashes) if !inferredExpiredMessageHashes.isEmpty { - try Interaction + try Interaction.deleteWhere( + db, .filter(inferredExpiredMessageHashes.contains(Interaction.Columns.serverHash)) - .deleteAll(db) + ) } try Interaction diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4c6d534abe..6336a3efd4 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -323,6 +323,7 @@ public final class OpenGroupManager { } // Remove all the data (everything should cascade delete) + _ = try? Interaction.deleteWhere(db, .filter(Interaction.Columns.threadId == openGroupId)) _ = try? SessionThread .filter(id: openGroupId) .deleteAll(db) @@ -656,10 +657,11 @@ public final class OpenGroupManager { // Handle any deletions that are needed if !messageServerInfoToRemove.isEmpty { let messageServerIdsToRemove: [Int64] = messageServerInfoToRemove.map { $0.id } - _ = try? Interaction - .filter(Interaction.Columns.threadId == openGroup.threadId) + _ = try? Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == openGroup.threadId), .filter(messageServerIdsToRemove.contains(Interaction.Columns.openGroupServerMessageId)) - .deleteAll(db) + ) // Update the seqNo for deletions largestValidSeqNo = max(largestValidSeqNo, (messageServerInfoToRemove.map({ $0.seqNo }).max() ?? 0)) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 95a548578b..6ebcee8041 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -472,10 +472,11 @@ extension MessageReceiver { /// If the message is about adding the current user then we should remove any existing `infoGroupInfoInvited` interactions /// from the group (don't want to have two different messages indicating the current user was added to the group) if messageContainsCurrentUser && message.changeType == .added { - _ = try Interaction - .filter(Interaction.Columns.threadId == groupSessionId.hexString) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == groupSessionId.hexString), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) } switch messageInfo.infoString(using: dependencies) { @@ -943,10 +944,11 @@ extension MessageReceiver { /// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a duplicate one in case /// the group was created via a `USER_GROUPS` config when syncing a new device) - _ = try Interaction - .filter(Interaction.Columns.threadId == groupSessionId.hexString) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == groupSessionId.hexString), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) /// Unline most control messages we don't bother setting expiration values for this message, this is because we won't actually /// have the current disappearing messages config as we won't have polled the group yet (and the settings are stored in the diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 05553088ed..9cbad6bf0b 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -11,7 +11,7 @@ public extension MessageViewModel { struct DeletionBehaviours { public enum Behaviour { case markAsDeleted(ids: [Int64], options: Interaction.DeletionOption, threadId: String, threadVariant: SessionThread.Variant) - case deleteFromDatabase(ids: [Int64], threadId: String) + case deleteFromDatabase([Int64]) case cancelPendingSendJobs([Int64]) case preparedRequest(Network.PreparedRequest) } @@ -97,14 +97,9 @@ public extension MessageViewModel { ) } - case .deleteFromDatabase(let ids, let threadId): + case .deleteFromDatabase(let ids): result = result.flatMapStorageWritePublisher(using: dependencies) { db, _ in - _ = try Interaction - .filter(ids: ids) - .deleteAll(db) - ids.forEach { id in - db.addMessageEvent(id: id, threadId: threadId, type: .deleted) - } + try Interaction.deleteWhere(db, .filter(ids.contains(Interaction.Columns.id))) } case .preparedRequest(let preparedRequest): @@ -203,13 +198,12 @@ public extension MessageViewModel.DeletionBehaviours { /// Control messages and deleted messages should be immediately deleted from the database .deleteFromDatabase( - ids: cellViewModels + cellViewModels .filter { viewModel in viewModel.variant.isInfoMessage || viewModel.variant.isDeletedMessage } - .map { $0.id }, - threadId: threadData.threadId + .map { $0.id } ), /// Other message types should only be marked as deleted @@ -447,10 +441,7 @@ public extension MessageViewModel.DeletionBehaviours { ) .appending(threadData.threadIsNoteToSelf ? /// If it's the `Note to Self`conversation then we want to just delete the interaction - .deleteFromDatabase( - ids: cellViewModels.map { $0.id }, - threadId: threadData.threadId - ) : + .deleteFromDatabase(cellViewModels.map { $0.id }) : .markAsDeleted( ids: targetViewModels.map { $0.id }, options: [.local, .network], @@ -693,10 +684,7 @@ public extension MessageViewModel.DeletionBehaviours { } ) .appending( - .deleteFromDatabase( - ids: cellViewModels.map { $0.id }, - threadId: threadData.threadId - ) + .deleteFromDatabase(cellViewModels.map { $0.id }) ) } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 556a2adee0..de38e4e215 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -854,7 +854,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when deleting") { beforeEach { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) try SessionThread.deleteAll(db) try testGroupThread.insert(db) @@ -1759,7 +1759,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores a message with no sender it("ignores a message with no sender") { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) } mockStorage.write { db in @@ -1793,7 +1793,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores a message with invalid data it("ignores a message with invalid data") { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) } mockStorage.write { db in diff --git a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift index c060be2aa0..6a30c902fc 100644 --- a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift +++ b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift @@ -47,11 +47,25 @@ public extension ObservingDatabase { addEvent(ObservedEvent(key: key, value: nil)) } - func addEvent(_ value: AnyHashable?, forKey key: ObservableKey) { + func addEvent(_ value: T?, forKey key: ObservableKey) { addEvent(ObservedEvent(key: key, value: value)) } } +// MARK: - LoggingDatabaseRecord + +public enum LoggingDatabaseRecordContext { + /// This `TaskLocal` variable is set and accessible within the context of a single `Task` and allows any code running within + /// the task to access the isntance without running into threading issues or needing to manage multiple instances + @TaskLocal + public static var suppressLogs: Bool? +} + +public protocol LoggingDatabaseRecord { + func logDeletion() + static func logDeletion() +} + // MARK: - ObservationContext public enum ObservationContext { @@ -192,6 +206,10 @@ public extension MutablePersistableRecord { @discardableResult func delete(_ db: ObservingDatabase) throws -> Bool { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord)?.logDeletion() + } + return try self.delete(db.originalDb) } } @@ -239,6 +257,10 @@ public extension QueryInterfaceRequest { @discardableResult func deleteAll(_ db: ObservingDatabase) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (RowDecoder.self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb) } } @@ -306,6 +328,10 @@ public extension TableRecord { @discardableResult static func deleteAll(_ db: ObservingDatabase) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb) } } @@ -317,11 +343,19 @@ public extension TableRecord where Self: Identifiable, Self.ID: DatabaseValueCon @discardableResult static func deleteAll(_ db: ObservingDatabase, ids: some Collection) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb, ids: ids) } @discardableResult static func deleteOne(_ db: ObservingDatabase, id: Self.ID) throws -> Bool { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteOne(db.originalDb, id: id) } } From 1031c5a4e2178047c4e8b6a9923bfcd7b327c159 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Aug 2025 10:23:13 +1000 Subject: [PATCH 119/244] Fixed extra comma issue CI Xcode complains about --- .../Database/Models/DisappearingMessageConfiguration.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 1950e7239c..de46275da9 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -266,7 +266,7 @@ public extension DisappearingMessagesConfiguration { .filter(Interaction.Columns.threadId == threadId), .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), .filter(Interaction.Columns.authorId == userSessionId.hexString), - .filter(Interaction.Columns.expiresInSeconds != 0), + .filter(Interaction.Columns.expiresInSeconds != 0) ) case (true, .disappearAfterRead): @@ -322,7 +322,7 @@ public extension DisappearingMessagesConfiguration { try Interaction.deleteWhere( db, .filter(Interaction.Columns.threadId == threadId), - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) ) case .community: break } From 0290fe0a98b9c66a85c733c664432eebc68a4a2c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 27 Aug 2025 09:11:51 +0800 Subject: [PATCH 120/244] Renamed Constants+URLs to Constants+Apple Added app_name to constants class --- Session.xcodeproj/project.pbxproj | 8 ++++---- Session/Home/App Review/AppReviewPromptModel.swift | 2 +- .../{Constants+URLs.swift => Constants+Apple.swift} | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) rename SessionUIKit/Style Guide/{Constants+URLs.swift => Constants+Apple.swift} (85%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 645a0c555e..157c149838 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -189,7 +189,7 @@ 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; - 94A6B9DB2DD6BF7C00DB4B44 /* Constants+URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */; }; + 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */; }; 94AAB14D2E1F39B500A6FA18 /* ProCTAModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */; }; 94AAB14F2E1F6CC100A6FA18 /* SessionProBadge+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14E2E1F6CB300A6FA18 /* SessionProBadge+SwiftUI.swift */; }; @@ -1565,7 +1565,7 @@ 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; - 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+URLs.swift"; sourceTree = ""; }; + 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Modal+SwiftUI.swift"; sourceTree = ""; }; 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProCTAModal.swift; sourceTree = ""; }; 94AAB14E2E1F6CB300A6FA18 /* SessionProBadge+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBadge+SwiftUI.swift"; sourceTree = ""; }; @@ -3304,7 +3304,7 @@ children = ( FD37E9C428A1C701003AE748 /* Themes */, 947AD68F2C8968FF000B2730 /* Constants.swift */, - 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */, + 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */, B8BB82BD2394D4CE00BA5194 /* Fonts.swift */, FDF848F029406A30007DCAE5 /* Format.swift */, FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */, @@ -6153,7 +6153,7 @@ 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */, 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */, FD37E9D728A20B5D003AE748 /* UIColor+Utilities.swift in Sources */, - 94A6B9DB2DD6BF7C00DB4B44 /* Constants+URLs.swift in Sources */, + 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */, FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */, FD8A5B0A2DBF246A004C689B /* Constants.swift in Sources */, C331FF9A2558FA6B00070591 /* Values.swift in Sources */, diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index d413bfcf2e..3f1a4c444f 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -42,7 +42,7 @@ extension AppReviewPromptState { .localized(), message: "rateSessionModalDescription" .put(key: "app_name", value: Constants.app_name) - .put(key: "storevariant", value: "App Store") + .put(key: "storevariant", value: Constants.store_name) .localized(), primaryButtonTitle: "rateSessionApp".localized(), secondaryButtonTitle: "notNow".localized() diff --git a/SessionUIKit/Style Guide/Constants+URLs.swift b/SessionUIKit/Style Guide/Constants+Apple.swift similarity index 85% rename from SessionUIKit/Style Guide/Constants+URLs.swift rename to SessionUIKit/Style Guide/Constants+Apple.swift index d289535060..70d680583a 100644 --- a/SessionUIKit/Style Guide/Constants+URLs.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -2,9 +2,13 @@ // // stringlint:disable public extension Constants { + // MARK: - URL static let session_network_url = "https://docs.getsession.org/session-network" static let session_staking_url = "https://docs.getsession.org/session-network/staking" static let session_token_url = "https://token.getsession.org" static let session_donations_url = "https://session.foundation/donate#app" static let session_feedback_url = "https://www.surveymonkey.com/r/YLDZJR8" + + // MARK: - Names + static let store_name = "App Store" } From bab762bed082f40369a702590bf618ac5c5d9a89 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 27 Aug 2025 09:12:03 +0800 Subject: [PATCH 121/244] Additional code cleanups --- Session/Home/HomeViewModel.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 28d4fa9f91..3c570899ab 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -541,7 +541,8 @@ public class HomeViewModel: NavigatableStateHolder { guard state.pendingAppReviewPromptState != nil else { return } DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in - self.didShowAppReviewPrompt() + // Set flag that review prompt was already presented + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true dependencies.notifyAsync( priority: .immediate, @@ -560,10 +561,6 @@ public class HomeViewModel: NavigatableStateHolder { .addingTimeInterval(2 * 7 * 24 * 60 * 60) } - func didShowAppReviewPrompt() { - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - } - func handlePromptChangeState(_ state: AppReviewPromptState?) { dependencies.notifyAsync( priority: .immediate, @@ -589,7 +586,7 @@ public class HomeViewModel: NavigatableStateHolder { func submitFeedbackSurvery() { guard let url: URL = URL(string: Constants.session_feedback_url) else { return } - var surveyUrl: URL = url.appending(queryItems: [ + let surveyUrl: URL = url.appending(queryItems: [ .init(name: "platform", value: "iOS"), .init(name: "version", value: dependencies[cache: .appVersion].appVersion) ]) From 0e1fb9a3f2666f843440dd13f20031e1475dedfb Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 27 Aug 2025 11:07:51 +0800 Subject: [PATCH 122/244] Fix issue on some device long delete message is not wrapping --- .../Message Cells/Content Views/DeletedMessageView.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 566ddceca1..c235b80e21 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -33,7 +33,7 @@ final class DeletedMessageView: UIView { let imageContainerView: UIView = UIView() imageContainerView.set(.width, to: DeletedMessageView.iconImageViewSize) imageContainerView.set(.height, to: DeletedMessageView.iconImageViewSize) - + let imageView = UIImageView(image: Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)?.withRenderingMode(.alwaysTemplate)) imageView.themeTintColor = textColor imageView.contentMode = .scaleAspectFit @@ -45,7 +45,6 @@ final class DeletedMessageView: UIView { // Body label let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) - titleLabel.preferredMaxLayoutWidth = maxWidth - 6 // `6` for the `stackView.layoutMargins` titleLabel.font = .systemFont(ofSize: Values.smallFontSize) titleLabel.text = { switch variant { @@ -69,6 +68,6 @@ final class DeletedMessageView: UIView { let calculatedSize: CGSize = stackView.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 999)) stackView.pin(to: self, withInset: Values.smallSpacing) - stackView.set(.height, to: calculatedSize.height) + stackView.set(.height, greaterThanOrEqualTo: calculatedSize.height) } } From 06015d8cda38e648518ed926632e1d966e314001 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 27 Aug 2025 14:12:58 +0800 Subject: [PATCH 123/244] Fix input field not hiding when showing link preview modal --- .../ConversationVC+Interaction.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..24ce7ec4f0 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -860,6 +860,9 @@ extension ConversationVC: } func showLinkPreviewSuggestionModal() { + // Hides accessory view while link preview confirmation is presented + hideInputAccessoryView() + let linkPreviewModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "linkPreviewsEnable".localized(), @@ -870,12 +873,17 @@ extension ConversationVC: ), confirmTitle: "enable".localized(), confirmStyle: .danger, - cancelStyle: .alert_text - ) { [weak self, dependencies = viewModel.dependencies] _ in - dependencies.setAsync(.areLinkPreviewsEnabled, true) { - self?.snInputView.autoGenerateLinkPreview() + cancelStyle: .alert_text, + onConfirm: { [weak self, dependencies = viewModel.dependencies] _ in + dependencies.setAsync(.areLinkPreviewsEnabled, true) { + self?.snInputView.autoGenerateLinkPreview() + } + }, + afterClosed: { [weak self] in + // Bring back accessory view after confirmation action + self?.showInputAccessoryView() } - } + ) ) present(linkPreviewModal, animated: true, completion: nil) From 6e945eb6660f91275a8363420ed036f51f0a07aa Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 08:41:21 +0800 Subject: [PATCH 124/244] Added accessibility identifier on review prompt buttons and content --- .../Home/App Review/AppReviewPromptModel.swift | 15 ++++++++++++--- .../App Review/View/AppReviewPromptDialog.swift | 8 ++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 3f1a4c444f..0921d9dca4 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -8,7 +8,10 @@ struct AppReviewPromptModel { let message: String let primaryButtonTitle: String + let primaryButtonAccessibilityIdentifier: String + let secondaryButtonTitle: String + let secondaryButtonAccessibilityIdentifier: String } enum AppReviewPromptState { @@ -31,9 +34,11 @@ extension AppReviewPromptState { primaryButtonTitle: "enjoyingSessionButtonPositive" .put(key: "emoji", value: "❤️") .localized(), + primaryButtonAccessibilityIdentifier: "enjoy-session-positive-button", secondaryButtonTitle: "enjoyingSessionButtonNegative" .put(key: "emoji", value: "😕") - .localized() + .localized(), + secondaryButtonAccessibilityIdentifier: "enjoy-session-negative-button" ) case .rateSession: return .init( @@ -45,7 +50,9 @@ extension AppReviewPromptState { .put(key: "storevariant", value: Constants.store_name) .localized(), primaryButtonTitle: "rateSessionApp".localized(), - secondaryButtonTitle: "notNow".localized() + primaryButtonAccessibilityIdentifier: "rate-app-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" ) case .feedback: return .init( @@ -54,7 +61,9 @@ extension AppReviewPromptState { .put(key: "app_name", value: Constants.app_name) .localized(), primaryButtonTitle: "openSurvey".localized(), - secondaryButtonTitle: "notNow".localized() + primaryButtonAccessibilityIdentifier: "open-survey-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" ) } } diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 2b15dc067c..af5e9edb47 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -135,10 +135,18 @@ class AppReviewPromptDialog: UIView { isHidden = prompt == nil titleLabel.text = prompt?.promptContent.title + titleLabel.accessibilityIdentifier = "Modal heading" + titleLabel.accessibilityLabel = titleLabel.text + messageLabel.text = prompt?.promptContent.message + messageLabel.accessibilityIdentifier = "Modal description" + messageLabel.accessibilityLabel = messageLabel.text primaryButton.setTitle(prompt?.promptContent.primaryButtonTitle, for: .normal) + primaryButton.accessibilityIdentifier = prompt?.promptContent.primaryButtonAccessibilityIdentifier + secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) + secondaryButton.accessibilityIdentifier = prompt?.promptContent.secondaryButtonAccessibilityIdentifier } @objc From 91301d448a7347674a565e7b303d16cf91e31b31 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 09:01:24 +0800 Subject: [PATCH 125/244] Updated displayed survey url to full path --- Session/Home/HomeViewModel.swift | 5 +++-- SessionUIKit/Style Guide/Constants+Apple.swift | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 3c570899ab..016c00dde1 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -586,8 +586,9 @@ public class HomeViewModel: NavigatableStateHolder { func submitFeedbackSurvery() { guard let url: URL = URL(string: Constants.session_feedback_url) else { return } + // stringlint:disable let surveyUrl: URL = url.appending(queryItems: [ - .init(name: "platform", value: "iOS"), + .init(name: "platform", value: Constants.platform_name), .init(name: "version", value: dependencies[cache: .appVersion].appVersion) ]) @@ -596,7 +597,7 @@ public class HomeViewModel: NavigatableStateHolder { title: "urlOpen".localized(), body: .attributedText( "urlOpenDescription" - .put(key: "url", value: url.absoluteString) + .put(key: "url", value: surveyUrl.absoluteString) .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) ), confirmTitle: "open".localized(), diff --git a/SessionUIKit/Style Guide/Constants+Apple.swift b/SessionUIKit/Style Guide/Constants+Apple.swift index 70d680583a..17b9f02c72 100644 --- a/SessionUIKit/Style Guide/Constants+Apple.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -11,4 +11,5 @@ public extension Constants { // MARK: - Names static let store_name = "App Store" + static let platform_name = "iOS" } From 8d0bffec10918dd35cb7f39b75b631a5915f7679 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 09:21:48 +0800 Subject: [PATCH 126/244] Fix content and button colors to pass standards for ocean light theme --- Session/Home/App Review/View/AppReviewPromptDialog.swift | 4 ++-- Session/Home/HomeVC.swift | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index af5e9edb47..120b0c1d83 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -35,7 +35,7 @@ class AppReviewPromptDialog: UIView { let result = UILabel() result.textAlignment = .center result.numberOfLines = 0 - result.themeTextColor = .textPrimary + result.themeTextColor = .alert_text result.font = .boldSystemFont(ofSize: Values.mediumFontSize) return result @@ -45,7 +45,7 @@ class AppReviewPromptDialog: UIView { let result = UILabel() result.textAlignment = .center result.numberOfLines = 0 - result.themeTextColor = .textSecondary + result.themeTextColor = .alert_text result.font = ConfirmationModal.explanationFont return result diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 104479c1e8..811a4d5796 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -86,10 +86,11 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let result = AppReviewPromptDialog() // Layers - result.themeBorderColor = .borderSeparator result.layer.borderWidth = 1 result.layer.cornerRadius = 12 - result.themeBackgroundColor = .backgroundSecondary + result.themeBorderColor = .borderSeparator + result.themeBackgroundColor = .alert_background + result.themeShadowColor = .black result.onPrimaryTapped = { [viewModel = self.viewModel] state in viewModel.handlePrimaryTappedForState(state) } result.onSecondaryTapped = { [viewModel = self.viewModel] in viewModel.handleSecondayTappedForState($0) } result.onCloseTapped = { [viewModel = self.viewModel] in viewModel.handlePromptChangeState(nil) } From 4eee81c08bf690718e722934beb485b42313fe39 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Aug 2025 12:54:15 +1000 Subject: [PATCH 127/244] Fixed a crash when tapping on a group member in the members list --- Session/Conversations/ConversationVC+Interaction.swift | 4 ++-- .../Conversations/Settings/ThreadSettingsViewModel.swift | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..521078a8fb 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -26,14 +26,14 @@ extension ConversationVC: { // MARK: - Open Settings - @objc func handleTitleViewTapped() { + @MainActor @objc func handleTitleViewTapped() { // Don't take the user to settings for unapproved threads guard viewModel.threadData.threadRequiresApproval == false else { return } openSettingsFromTitleView() } - func openSettingsFromTitleView() { + @MainActor func openSettingsFromTitleView() { // If we shouldn't be able to access settings then disable the title view shortcuts guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index c2462e2193..b5c39cc9ce 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1280,7 +1280,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob public static func createMemberListViewController( threadId: String, - transitionToConversation: @escaping (String) -> Void, + transitionToConversation: @escaping @MainActor (String) -> Void, using dependencies: Dependencies ) -> UIViewController { return SessionTableViewController( @@ -1315,7 +1315,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob ) }, completion: { _ in - transitionToConversation(memberInfo.profileId) + Task { @MainActor in + transitionToConversation(memberInfo.profileId) + } } ) }, From 905711593e5302073de7ef344901e13c3b7824c3 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 11:20:30 +0800 Subject: [PATCH 128/244] Added check if app was installed or updated to app review version --- Session/Home/HomeViewModel.swift | 36 +++++++++++++++---- .../Style Guide/Constants+Apple.swift | 3 ++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 016c00dde1..71d7a7f150 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -61,9 +61,29 @@ public class HomeViewModel: NavigatableStateHolder { promptState = .rateSession } + // Checks if new version of app is from install or update + var didInstallAppReviewPromptVersion: Bool { + let availabilityVersion = Constants.review_prompt_availability_version + + guard let firstAppVersion = dependencies[cache: .appVersion].firstAppVersion else { + return true + } + + let comparisonResult = firstAppVersion.compare(availabilityVersion, options: .numeric) + + if comparisonResult == .orderedAscending { + // App was updated to the latest version with app review prompt + return false + } else { + // App was installed not updated to new version + return true + } + } + self.state = State.initialState( using: dependencies, - appReviewPromptState: promptState + appReviewPromptState: promptState, + didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion ) /// Bind the state @@ -102,6 +122,7 @@ public class HomeViewModel: NavigatableStateHolder { let itemCache: [String: SessionThreadViewModel] let appReviewPromptState: AppReviewPromptState? let pendingAppReviewPromptState: AppReviewPromptState? + let didInstallAppReviewPromptVersion: Bool @MainActor public func sections(viewModel: HomeViewModel) -> [SectionModel] { HomeViewModel.sections(state: self, viewModel: viewModel) @@ -154,7 +175,7 @@ public class HomeViewModel: NavigatableStateHolder { return result } - static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?) -> State { + static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?, didInstallAppReviewPromptVersion: Bool) -> State { return State( viewState: .loading, userProfile: Profile(id: dependencies[cache: .general].sessionId.hexString, name: ""), @@ -180,7 +201,8 @@ public class HomeViewModel: NavigatableStateHolder { ), itemCache: [:], appReviewPromptState: nil, - pendingAppReviewPromptState: appReviewPromptState + pendingAppReviewPromptState: appReviewPromptState, + didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion ) } } @@ -204,6 +226,7 @@ public class HomeViewModel: NavigatableStateHolder { var itemCache: [String: SessionThreadViewModel] = previousState.itemCache var appReviewPromptState: AppReviewPromptState? = previousState.appReviewPromptState var pendingAppReviewPromptState: AppReviewPromptState? = previousState.pendingAppReviewPromptState + var didInstallAppReviewPromptVersion: Bool = previousState.didInstallAppReviewPromptVersion /// Store a local copy of the events so we can manipulate it based on the state changes var eventsToProcess: [ObservedEvent] = events @@ -402,13 +425,13 @@ public class HomeViewModel: NavigatableStateHolder { groupedOtherEvents?[.userDefault]?.forEach { event in switch (event.key, event.value) { case (.userDefault(.hasVisitedPathScreen), let value as Bool) where value == true: - pendingAppReviewPromptState = .enjoyingSession + if didInstallAppReviewPromptVersion { pendingAppReviewPromptState = .enjoyingSession } case (.userDefault(.hasPressedDonateButton), let value as Bool) where value == true: pendingAppReviewPromptState = .enjoyingSession case (.userDefault(.hasChangedTheme), let value as Bool) where value == true: - pendingAppReviewPromptState = .enjoyingSession + if didInstallAppReviewPromptVersion { pendingAppReviewPromptState = .enjoyingSession } default: break } @@ -437,7 +460,8 @@ public class HomeViewModel: NavigatableStateHolder { loadedPageInfo: loadResult.info, itemCache: itemCache, appReviewPromptState: appReviewPromptState, - pendingAppReviewPromptState: pendingAppReviewPromptState + pendingAppReviewPromptState: pendingAppReviewPromptState, + didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion ) } diff --git a/SessionUIKit/Style Guide/Constants+Apple.swift b/SessionUIKit/Style Guide/Constants+Apple.swift index 17b9f02c72..74224c7009 100644 --- a/SessionUIKit/Style Guide/Constants+Apple.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -12,4 +12,7 @@ public extension Constants { // MARK: - Names static let store_name = "App Store" static let platform_name = "iOS" + + // MARK: - + static let review_prompt_availability_version = "2.14.1" } From 295cf51d3614c67b71cd4ee5e0f3033166b79a07 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Aug 2025 13:27:34 +1000 Subject: [PATCH 129/244] Disable xcbeautify to debug step failure --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 04afc476a2..a3ab7350e9 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -80,7 +80,7 @@ local clean_up_old_test_sims_on_commit_trigger = { 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', 'echo "If tests fail inconsistently from local builds this is likely the difference"', 'echo ""', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1', ], depends_on: [ 'Reset SPM Cache if Needed', From 7521732a28f6b6a60c956c99e164ab2d7dc8c0f1 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 12:21:13 +0800 Subject: [PATCH 130/244] Added 24hr buffer on the review retry date check --- Session/Home/HomeViewModel.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 71d7a7f150..6905922fff 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -52,8 +52,14 @@ public class HomeViewModel: NavigatableStateHolder { let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] var promptState: AppReviewPromptState? + + // A buffer of 24 hours + let buffer: TimeInterval = 24 * 60 * 60 - if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow >= retryDate { + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { + // This block will execute if the current time is within 24 hours of the retryDate + // or if the current time is past the retryDate. + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false From 0d06edef873af077e9f293cdc72322943d1dd316 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 12:36:32 +0800 Subject: [PATCH 131/244] Show review prompt when force closing app --- Session/Home/HomeViewModel.swift | 12 ++++++++++++ SessionUtilitiesKit/Types/UserDefaultsType.swift | 3 +++ 2 files changed, 15 insertions(+) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 6905922fff..d3d956e794 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -65,6 +65,10 @@ public class HomeViewModel: NavigatableStateHolder { dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false promptState = .rateSession + } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .enjoyingSession } // Checks if new version of app is from install or update @@ -573,6 +577,7 @@ public class HomeViewModel: NavigatableStateHolder { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in // Set flag that review prompt was already presented dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = true dependencies.notifyAsync( priority: .immediate, @@ -592,6 +597,9 @@ public class HomeViewModel: NavigatableStateHolder { } func handlePromptChangeState(_ state: AppReviewPromptState?) { + // Prompt closed + if state == nil { dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false } + dependencies.notifyAsync( priority: .immediate, key: .updateScreen(HomeViewModel.self), @@ -652,6 +660,8 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor func handlePrimaryTappedForState(_ state: AppReviewPromptState) { + dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false + switch state { case .enjoyingSession: handlePromptChangeState(.rateSession) @@ -668,6 +678,8 @@ public class HomeViewModel: NavigatableStateHolder { } func handleSecondayTappedForState(_ state: AppReviewPromptState) { + dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false + switch state { case .feedback, .rateSession: handlePromptChangeState(nil) case .enjoyingSession: handlePromptChangeState(.feedback) diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 9f12ec8132..9b46b05a60 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -177,6 +177,9 @@ public extension UserDefaults.BoolKey { /// Indicates wheter app has already presented the user the app review prompt dialog static let didShowAppReviewPrompt: UserDefaults.BoolKey = "didShowAppReviewPrompt" + + /// Idicates whether app review prompt was ignored or no iteraction was done to dismiss it (closed app) + static let didIgnoreAppReviewPrompt: UserDefaults.BoolKey = "didIgnoreAppReviewPrompt" } public extension UserDefaults.DateKey { From 911031a2bdd499c4a8faf27c3ab47d5b1d7b7b0b Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 28 Aug 2025 14:36:01 +0800 Subject: [PATCH 132/244] Added app review rate limit check and debug flag --- .../App Review/AppReviewPromptModel.swift | 100 ++++++++++-------- .../View/AppReviewPromptDialog.swift | 2 + Session/Home/HomeViewModel.swift | 34 +++++- .../Settings/DeveloperSettingsViewModel.swift | 31 +++++- SessionUtilitiesKit/General/Feature.swift | 4 + 5 files changed, 121 insertions(+), 50 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 0921d9dca4..306e64fb01 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -7,64 +7,72 @@ struct AppReviewPromptModel { let title: String let message: String - let primaryButtonTitle: String - let primaryButtonAccessibilityIdentifier: String + var primaryButtonTitle: String? + var primaryButtonAccessibilityIdentifier: String? - let secondaryButtonTitle: String - let secondaryButtonAccessibilityIdentifier: String + var secondaryButtonTitle: String? + var secondaryButtonAccessibilityIdentifier: String? } enum AppReviewPromptState { case enjoyingSession case rateSession case feedback + case rateLimit } extension AppReviewPromptState { var promptContent: AppReviewPromptModel { switch self { - case .enjoyingSession: - return .init( - title: "enjoyingSession" - .put(key: "app_name", value: Constants.app_name) - .localized(), - message: "enjoyingSessionDescription" - .put(key: "app_name", value: Constants.app_name) - .localized(), - primaryButtonTitle: "enjoyingSessionButtonPositive" - .put(key: "emoji", value: "❤️") - .localized(), - primaryButtonAccessibilityIdentifier: "enjoy-session-positive-button", - secondaryButtonTitle: "enjoyingSessionButtonNegative" - .put(key: "emoji", value: "😕") - .localized(), - secondaryButtonAccessibilityIdentifier: "enjoy-session-negative-button" - ) - case .rateSession: - return .init( - title: "rateSession" - .put(key: "app_name", value: Constants.app_name) - .localized(), - message: "rateSessionModalDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "storevariant", value: Constants.store_name) - .localized(), - primaryButtonTitle: "rateSessionApp".localized(), - primaryButtonAccessibilityIdentifier: "rate-app-button", - secondaryButtonTitle: "notNow".localized(), - secondaryButtonAccessibilityIdentifier: "not-now-button" - ) - case .feedback: - return .init( - title: "giveFeedback".localized(), - message: "giveFeedbackDescription" - .put(key: "app_name", value: Constants.app_name) - .localized(), - primaryButtonTitle: "openSurvey".localized(), - primaryButtonAccessibilityIdentifier: "open-survey-button", - secondaryButtonTitle: "notNow".localized(), - secondaryButtonAccessibilityIdentifier: "not-now-button" - ) + case .enjoyingSession: + return .init( + title: "enjoyingSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "enjoyingSessionDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "enjoyingSessionButtonPositive" + .put(key: "emoji", value: "❤️") + .localized(), + primaryButtonAccessibilityIdentifier: "enjoy-session-positive-button", + secondaryButtonTitle: "enjoyingSessionButtonNegative" + .put(key: "emoji", value: "😕") + .localized(), + secondaryButtonAccessibilityIdentifier: "enjoy-session-negative-button" + ) + case .rateSession: + return .init( + title: "rateSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "rateSessionModalDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "storevariant", value: Constants.store_name) + .localized(), + primaryButtonTitle: "rateSessionApp".localized(), + primaryButtonAccessibilityIdentifier: "rate-app-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" + ) + case .feedback: + return .init( + title: "giveFeedback".localized(), + message: "giveFeedbackDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "openSurvey".localized(), + primaryButtonAccessibilityIdentifier: "open-survey-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" + ) + case .rateLimit: + return .init( + title: "reviewLimit".localized(), + message: "reviewLimitDescription" + .put(key: "app_name", value: Constants.app_name) + .localized() + ) } } } diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 120b0c1d83..e37cd44601 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -142,9 +142,11 @@ class AppReviewPromptDialog: UIView { messageLabel.accessibilityIdentifier = "Modal description" messageLabel.accessibilityLabel = messageLabel.text + primaryButton.isHidden = prompt?.promptContent.primaryButtonTitle == nil primaryButton.setTitle(prompt?.promptContent.primaryButtonTitle, for: .normal) primaryButton.accessibilityIdentifier = prompt?.promptContent.primaryButtonAccessibilityIdentifier + secondaryButton.isHidden = prompt?.promptContent.secondaryButtonTitle == nil secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) secondaryButton.accessibilityIdentifier = prompt?.promptContent.secondaryButtonAccessibilityIdentifier } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index d3d956e794..d2b6e3200d 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -37,6 +37,7 @@ public class HomeViewModel: NavigatableStateHolder { public let dependencies: Dependencies private let userSessionId: SessionId + private var didPresentAppReviewPrompt: Bool = false /// This value is the current state of the view @MainActor @Published private(set) var state: State @@ -598,7 +599,7 @@ public class HomeViewModel: NavigatableStateHolder { func handlePromptChangeState(_ state: AppReviewPromptState?) { // Prompt closed - if state == nil { dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false } + if state == nil || state == .rateLimit { dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false } dependencies.notifyAsync( priority: .immediate, @@ -616,10 +617,37 @@ public class HomeViewModel: NavigatableStateHolder { dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - SKStoreReviewController.requestReview(in: scene) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.systemHasPresentedNewWindow(notification:)), + name: UIWindow.didBecomeVisibleNotification, object: nil + ) + + if !dependencies[feature: .simulateAppReviewLimit] { + SKStoreReviewController.requestReview(in: scene) + } + + // Added 2 sec delay to give time for requet review to proc + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [self] in + NotificationCenter.default.removeObserver(self, name: UIWindow.didBecomeVisibleNotification, object: nil) + + guard didPresentAppReviewPrompt else { + // Show rate limit prompt + handlePromptChangeState(.rateLimit) + return + } + + // Reset flag just in case it will be triggered again + didPresentAppReviewPrompt = false + } } } + @objc + func systemHasPresentedNewWindow(notification: Notification) { + didPresentAppReviewPrompt = true + } + @MainActor func submitFeedbackSurvery() { guard let url: URL = URL(string: Constants.session_feedback_url) else { return } @@ -674,6 +702,7 @@ public class HomeViewModel: NavigatableStateHolder { // Close prompt before showing app review handlePromptChangeState(nil) submitAppStoreReview() + default: break } } @@ -683,6 +712,7 @@ public class HomeViewModel: NavigatableStateHolder { switch state { case .feedback, .rateSession: handlePromptChangeState(nil) case .enjoyingSession: handlePromptChangeState(.feedback) + default: break } } diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e2e044ff5c..fdbd81cc1e 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -82,6 +82,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case copyDocumentsPath case copyAppGroupPath case resetAppReviewPrompt + case simulateAppReviewLimit case defaultLogLevel case advancedLogging @@ -122,6 +123,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .copyDocumentsPath: return "copyDocumentsPath" case .copyAppGroupPath: return "copyAppGroupPath" case .resetAppReviewPrompt: return "resetAppReviewPrompt" + case .simulateAppReviewLimit: return "simulateAppReviewLimit" case .defaultLogLevel: return "defaultLogLevel" case .advancedLogging: return "advancedLogging" @@ -172,6 +174,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .copyDocumentsPath: result.append(.copyDocumentsPath); fallthrough case .copyAppGroupPath: result.append(.copyAppGroupPath); fallthrough case .resetAppReviewPrompt: result.append(.resetAppReviewPrompt); fallthrough + case .simulateAppReviewLimit: result.append(.simulateAppReviewLimit); fallthrough case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough case .advancedLogging: result.append(.advancedLogging); fallthrough @@ -247,6 +250,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let treatAllIncomingMessagesAsProMessages: Bool let forceSlowDatabaseQueries: Bool + + let updateSimulateAppReviewLimit: Bool } let title: String = "Developer Settings" @@ -299,7 +304,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages], - forceSlowDatabaseQueries: dependencies[feature: .forceSlowDatabaseQueries] + forceSlowDatabaseQueries: dependencies[feature: .forceSlowDatabaseQueries], + updateSimulateAppReviewLimit: dependencies[feature: .simulateAppReviewLimit] ) } .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } @@ -422,7 +428,24 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onTap: { [weak self] in self?.resetAppReviewPrompt() } - ) + ), + SessionCell.Info( + id: .simulateAppReviewLimit, + title: "Simulate App Review Limit", + subtitle: """ + Controls whether the in-app rating prompt is displayed. This can will simulate a rate limit, preventing the prompt from appearing. + """, + trailingAccessory: .toggle( + current.updateSimulateAppReviewLimit, + oldValue: previous?.updateSimulateAppReviewLimit + ), + onTap: { [weak self] in + self?.updateFlag( + for: .simulateAppReviewLimit, + to: !current.updateSimulateAppReviewLimit + ) + } + ), ] ) let logging: SectionModel = SectionModel( @@ -972,6 +995,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .copyDocumentsPath: break // Not a feature case .copyAppGroupPath: break // Not a feature case .resetAppReviewPrompt: break + case .simulateAppReviewLimit: + guard dependencies.hasSet(feature: .simulateAppReviewLimit) else { return } + + updateFlag(for: .simulateAppReviewLimit, to: nil) case .resetSnodeCache: break // Not a feature case .createMockContacts: break // Not a feature case .exportDatabase: break // Not a feature diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index 9f73288a61..5407bf1e04 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -88,6 +88,10 @@ public extension FeatureStorage { static let treatAllIncomingMessagesAsProMessages: FeatureConfig = Dependencies.create( identifier: "treatAllIncomingMessagesAsProMessages" ) + + static let simulateAppReviewLimit: FeatureConfig = Dependencies.create( + identifier: "simulateAppReviewLimit" + ) } // MARK: - FeatureOption From f650a52772dbde345f17f0a0fdef4cfefd6fa27a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 29 Aug 2025 08:55:51 +1000 Subject: [PATCH 133/244] Revert "Disable xcbeautify to debug step failure" This reverts commit 295cf51d3614c67b71cd4ee5e0f3033166b79a07. --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index a3ab7350e9..04afc476a2 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -80,7 +80,7 @@ local clean_up_old_test_sims_on_commit_trigger = { 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', 'echo "If tests fail inconsistently from local builds this is likely the difference"', 'echo ""', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1', + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', ], depends_on: [ 'Reset SPM Cache if Needed', From be0407fa91a8c9d0d09fb601638bd6ed4b92f4c0 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 29 Aug 2025 09:16:11 +0800 Subject: [PATCH 134/244] Clean up app review prompt code --- .../App Review/AppReviewPromptModel.swift | 58 +++++++- Session/Home/HomeViewModel.swift | 137 ++++++++---------- .../Style Guide/Constants+Apple.swift | 3 - .../Types/UserDefaultsType.swift | 2 +- 4 files changed, 114 insertions(+), 86 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 306e64fb01..26332b1b64 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -2,6 +2,7 @@ import Foundation import SessionUIKit +import SessionUtilitiesKit struct AppReviewPromptModel { let title: String @@ -14,14 +15,65 @@ struct AppReviewPromptModel { var secondaryButtonAccessibilityIdentifier: String? } +extension AppReviewPromptModel { + /// Determines the initial state of the app review prompt. + static func loadInitialAppReviewPromptState(_ dependencies: Dependencies) -> (promptState: AppReviewPromptState?, wasInstalledPriorToAppReviewRelease: Bool) { + /// Check if incomplete app review can be shown again to user on next app launch + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + var promptState: AppReviewPromptState? + + // A buffer of 24 hours + let buffer: TimeInterval = 24 * 60 * 60 + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { + // This block will execute if the current time is within 24 hours of the retryDate + // or if the current time is past the retryDate. + + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .rateSession + } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && !dependencies[defaults: .standard, key: .didActionAppReviewPrompt] { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .enjoyingSession + } + + let wasInstalledPriorToAppReviewRelease = checkIfAppWasInstalledPriorToAppReviewRelease(dependencies) + + return (promptState: promptState, wasInstalledPriorToAppReviewRelease: wasInstalledPriorToAppReviewRelease) + } + + /// Checks if version was from install or from update + static private func checkIfAppWasInstalledPriorToAppReviewRelease(_ dependencies: Dependencies) -> Bool { + // Base version where app review prompt became available + // TODO: Update this once a version to include app review prompt is decided + let reviewPromptAvailabilityVersion = "2.14.1" // stringlint:ignore + + guard let firstAppVersion = dependencies[cache: .appVersion].firstAppVersion else { + return true + } + + let comparisonResult = firstAppVersion.compare(reviewPromptAvailabilityVersion, options: .numeric) + + if comparisonResult == .orderedAscending { + // App was updated to the latest version with app review prompt + return false + } else { + // App was installed not updated to new version + return true + } + } +} + enum AppReviewPromptState { case enjoyingSession case rateSession case feedback case rateLimit -} - -extension AppReviewPromptState { + var promptContent: AppReviewPromptModel { switch self { case .enjoyingSession: diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index d2b6e3200d..b47f0e6918 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -44,57 +44,19 @@ public class HomeViewModel: NavigatableStateHolder { private var observationTask: Task? // MARK: - Initialization - @MainActor init(using dependencies: Dependencies) { self.dependencies = dependencies self.userSessionId = dependencies[cache: .general].sessionId - /// Check if incomplete app review can be shown again to user on next app launch - let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - - var promptState: AppReviewPromptState? - - // A buffer of 24 hours - let buffer: TimeInterval = 24 * 60 * 60 - - if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { - // This block will execute if the current time is within 24 hours of the retryDate - // or if the current time is past the retryDate. - - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil - dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - - promptState = .rateSession - } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] { - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - - promptState = .enjoyingSession - } - - // Checks if new version of app is from install or update - var didInstallAppReviewPromptVersion: Bool { - let availabilityVersion = Constants.review_prompt_availability_version - - guard let firstAppVersion = dependencies[cache: .appVersion].firstAppVersion else { - return true - } - - let comparisonResult = firstAppVersion.compare(availabilityVersion, options: .numeric) - - if comparisonResult == .orderedAscending { - // App was updated to the latest version with app review prompt - return false - } else { - // App was installed not updated to new version - return true - } - } + // The `appReview` variable is a tuple with the following elements: + // .promptState: AppReviewPromptState? + // .wasInstalledPriorToAppReviewRelease: Bool + let appReview = AppReviewPromptModel.loadInitialAppReviewPromptState(dependencies) self.state = State.initialState( using: dependencies, - appReviewPromptState: promptState, - didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion + appReviewPromptState: appReview.promptState, + appWasInstalledPriorToAppReviewRelease: appReview.wasInstalledPriorToAppReviewRelease ) /// Bind the state @@ -106,6 +68,10 @@ public class HomeViewModel: NavigatableStateHolder { .assign { [weak self] updatedState in self?.state = updatedState } } + deinit { + NotificationCenter.default.removeObserver(self) + } + public struct HomeViewModelEvent: Hashable { let pendingAppReviewPromptState: AppReviewPromptState? let appReviewPromptState: AppReviewPromptState? @@ -133,7 +99,7 @@ public class HomeViewModel: NavigatableStateHolder { let itemCache: [String: SessionThreadViewModel] let appReviewPromptState: AppReviewPromptState? let pendingAppReviewPromptState: AppReviewPromptState? - let didInstallAppReviewPromptVersion: Bool + let appWasInstalledPriorToAppReviewRelease: Bool @MainActor public func sections(viewModel: HomeViewModel) -> [SectionModel] { HomeViewModel.sections(state: self, viewModel: viewModel) @@ -186,7 +152,7 @@ public class HomeViewModel: NavigatableStateHolder { return result } - static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?, didInstallAppReviewPromptVersion: Bool) -> State { + static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?, appWasInstalledPriorToAppReviewRelease: Bool) -> State { return State( viewState: .loading, userProfile: Profile(id: dependencies[cache: .general].sessionId.hexString, name: ""), @@ -213,7 +179,7 @@ public class HomeViewModel: NavigatableStateHolder { itemCache: [:], appReviewPromptState: nil, pendingAppReviewPromptState: appReviewPromptState, - didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion + appWasInstalledPriorToAppReviewRelease: appWasInstalledPriorToAppReviewRelease ) } } @@ -237,7 +203,7 @@ public class HomeViewModel: NavigatableStateHolder { var itemCache: [String: SessionThreadViewModel] = previousState.itemCache var appReviewPromptState: AppReviewPromptState? = previousState.appReviewPromptState var pendingAppReviewPromptState: AppReviewPromptState? = previousState.pendingAppReviewPromptState - var didInstallAppReviewPromptVersion: Bool = previousState.didInstallAppReviewPromptVersion + let appWasInstalledPriorToAppReviewRelease: Bool = previousState.appWasInstalledPriorToAppReviewRelease /// Store a local copy of the events so we can manipulate it based on the state changes var eventsToProcess: [ObservedEvent] = events @@ -436,13 +402,13 @@ public class HomeViewModel: NavigatableStateHolder { groupedOtherEvents?[.userDefault]?.forEach { event in switch (event.key, event.value) { case (.userDefault(.hasVisitedPathScreen), let value as Bool) where value == true: - if didInstallAppReviewPromptVersion { pendingAppReviewPromptState = .enjoyingSession } + if appWasInstalledPriorToAppReviewRelease { pendingAppReviewPromptState = .enjoyingSession } case (.userDefault(.hasPressedDonateButton), let value as Bool) where value == true: pendingAppReviewPromptState = .enjoyingSession case (.userDefault(.hasChangedTheme), let value as Bool) where value == true: - if didInstallAppReviewPromptVersion { pendingAppReviewPromptState = .enjoyingSession } + if appWasInstalledPriorToAppReviewRelease { pendingAppReviewPromptState = .enjoyingSession } default: break } @@ -472,7 +438,7 @@ public class HomeViewModel: NavigatableStateHolder { itemCache: itemCache, appReviewPromptState: appReviewPromptState, pendingAppReviewPromptState: pendingAppReviewPromptState, - didInstallAppReviewPromptVersion: didInstallAppReviewPromptVersion + appWasInstalledPriorToAppReviewRelease: appWasInstalledPriorToAppReviewRelease ) } @@ -578,7 +544,7 @@ public class HomeViewModel: NavigatableStateHolder { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in // Set flag that review prompt was already presented dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = true + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false dependencies.notifyAsync( priority: .immediate, @@ -598,8 +564,8 @@ public class HomeViewModel: NavigatableStateHolder { } func handlePromptChangeState(_ state: AppReviewPromptState?) { - // Prompt closed - if state == nil || state == .rateLimit { dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false } + // Prompt closed from `x` button of prompt + if state == nil || state == .rateLimit { dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true } dependencies.notifyAsync( priority: .immediate, @@ -616,35 +582,48 @@ public class HomeViewModel: NavigatableStateHolder { dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - NotificationCenter.default.addObserver( - self, - selector: #selector(self.systemHasPresentedNewWindow(notification:)), - name: UIWindow.didBecomeVisibleNotification, object: nil - ) - - if !dependencies[feature: .simulateAppReviewLimit] { - SKStoreReviewController.requestReview(in: scene) - } + NotificationCenter.default.addObserver( + self, + selector: #selector(self.windowBecameVisibleAfterTriggeringAppStoreReview(notification:)), + name: UIWindow.didBecomeVisibleNotification, object: nil + ) + + if !dependencies[feature: .simulateAppReviewLimit] { + requestAppReview() + } - // Added 2 sec delay to give time for requet review to proc - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [self] in - NotificationCenter.default.removeObserver(self, name: UIWindow.didBecomeVisibleNotification, object: nil) - - guard didPresentAppReviewPrompt else { - // Show rate limit prompt - handlePromptChangeState(.rateLimit) - return - } - - // Reset flag just in case it will be triggered again - didPresentAppReviewPrompt = false + // Added 2 sec delay to give time for requet review to proc + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [weak self] in + guard let this = self else { return } + + NotificationCenter.default.removeObserver(this, name: UIWindow.didBecomeVisibleNotification, object: nil) + + guard this.didPresentAppReviewPrompt else { + // Show rate limit prompt + this.handlePromptChangeState(.rateLimit) + return } + + // Reset flag just in case it will be triggered again + this.didPresentAppReviewPrompt = false + } + } + + @MainActor + private func requestAppReview() { + guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else { + return + } + + if #available(iOS 16.0, *) { + AppStore.requestReview(in: scene) + } else { + SKStoreReviewController.requestReview(in: scene) } } @objc - func systemHasPresentedNewWindow(notification: Notification) { + private func windowBecameVisibleAfterTriggeringAppStoreReview(notification: Notification) { didPresentAppReviewPrompt = true } @@ -688,7 +667,7 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor func handlePrimaryTappedForState(_ state: AppReviewPromptState) { - dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true switch state { case .enjoyingSession: @@ -707,7 +686,7 @@ public class HomeViewModel: NavigatableStateHolder { } func handleSecondayTappedForState(_ state: AppReviewPromptState) { - dependencies[defaults: .standard, key: .didIgnoreAppReviewPrompt] = false + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true switch state { case .feedback, .rateSession: handlePromptChangeState(nil) diff --git a/SessionUIKit/Style Guide/Constants+Apple.swift b/SessionUIKit/Style Guide/Constants+Apple.swift index 74224c7009..17b9f02c72 100644 --- a/SessionUIKit/Style Guide/Constants+Apple.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -12,7 +12,4 @@ public extension Constants { // MARK: - Names static let store_name = "App Store" static let platform_name = "iOS" - - // MARK: - - static let review_prompt_availability_version = "2.14.1" } diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 9b46b05a60..e7db0491ee 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -179,7 +179,7 @@ public extension UserDefaults.BoolKey { static let didShowAppReviewPrompt: UserDefaults.BoolKey = "didShowAppReviewPrompt" /// Idicates whether app review prompt was ignored or no iteraction was done to dismiss it (closed app) - static let didIgnoreAppReviewPrompt: UserDefaults.BoolKey = "didIgnoreAppReviewPrompt" + static let didActionAppReviewPrompt: UserDefaults.BoolKey = "didActionAppReviewPrompt" } public extension UserDefaults.DateKey { From 242912ca69cd6624466727335ab57d27ad548c88 Mon Sep 17 00:00:00 2001 From: ThomasSession <171472362+ThomasSession@users.noreply.github.com> Date: Fri, 29 Aug 2025 02:06:55 +0000 Subject: [PATCH 135/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 3633 ++++++----------- SessionUIKit/Style Guide/Constants.swift | 1 + 2 files changed, 1170 insertions(+), 2464 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 5e4040a9a3..4b0b2bdd5b 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -30956,6 +30956,17 @@ } } }, + "appProBadge" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Badge" + } + } + } + }, "attachment" : { "extractionState" : "manual", "localizations" : { @@ -65016,7 +65027,7 @@ } } }, - "blockedContactsmanageDescription" : { + "blockedContactsManageDescription" : { "extractionState" : "manual", "localizations" : { "en" : { @@ -78435,478 +78446,10 @@ "callsVoiceAndVideoModalDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou IP is sigbaar vir jou oproepmaat en 'n Oxen Foundation-bediener terwyl beta-oproepe gebruik word." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "عنوان IP الخاص بك مرئي لشريك الاتصال وخادم Oxen Foundation أثناء استخدام المكالمات التجريبية." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beta zənglərini istifadə edərkən IP ünvanınız zəng tərəfdaşınıza və Oxen Foundation serverinə görünür." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی آپ کہ آئیں طرفه IP پدنی Oxen Foundation کہ سرورے ہ مغامیگیا." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш IP будзе бачныя вашаму партнёру па званках і серверу Oxen Foundation падчас выкарыстання бэта-званкоў." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашият IP е видим за вашият партньор по време нослужване на beta обаждания и Oxen Foundation сървър." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "বেটা কলগুলি ব্যবহার করার সময় আপনার আইপি আপনার কল পার্টনার এবং একটি Oxen Foundation সার্ভারের কাছে দৃশ্যমান হবে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra IP és visible a la vostra parella de trucada i a un servidor de Oxen Foundation mentre utilitzeu trucades beta." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše IP adresa je při používání beta hovorů viditelná pro vašeho volacího partnera a server Oxen Foundation." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich GPS yn weladwy i'ch partner galwad ac i Wasanaeth Node Foundation Oxen wrth ddefnyddio galwadau beta." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din IP synlig for din opkaldspartner og en Oxen Foundation-server, mens du bruger betaopkald." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deine IP ist für deinen Gesprächspartner und einen Oxen Foundation Server sichtbar, während du Beta-Anrufe nutzt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Η διεύθυνση IP σας είναι ορατή στον συνομιλητή σας και σε έναν διακομιστή του Oxen Foundation κατά τη χρήση κλήσεων beta." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via IP-adreso estas videbla al via vokopartnero kaj al servilo de Oxen Foundation dum vi uzas betajn vokojn." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu IP es visible para tu compañero de llamada y un servidor de la Oxen Foundation mientras usas llamadas beta." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu IP es visible para tu socio de llamada y un servidor de Oxen Foundation mientras usas las llamadas beta." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie IP on nähtav teie kõnepartnerile ja Oxen Foundation serverile beetakõnede ajal." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure IPa ikusgai egongo da zure dei-bikotekidearentzako eta Oxen Foundation zerbitzari batentzako, beta-deiak egiten ari bazara." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "آدرس IP شما برای مخاطب تماس شما و همچنین سرور Oxen Foundation در هنگام استفاده از تماس آزمایشی مشخص خواهد بود." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-osoitteesi on näkyvissä puhelun aikana vastaanottajalle ja Oxen Foundation palvelimelle, kun käytät beta-puheluita." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ang iyong IP ay nakikita ng iyong kasamahan sa tawag at isang server ng Oxen Foundation habang gumagamit ng beta calls." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre adresse IP est visible par votre interlocuteur et un serveur d'Oxen Foundation pendant que vous utilisez des appels beta." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu IP é visible para o teu compañeire de chamada e un servidor da Oxen Foundation mentres utilizas chamadas beta." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP ɗinku yana bayyane ga abokin kiran ku da sabar Oxen Foundation yayin amfani da ƙiran beta." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "כתובת ה-IP שלך גלויה לשותפת השיחה שלך ולשרת קרן Oxen בעת שימוש בשיחות בטא." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपने खाते में एन्क्रिप्टेड संदेश भेजते समय आपका IP आपके कॉल पार्टनर और Oxen Foundation सर्वर को दिखाई देगा।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je vidljiv vašem sugovorniku i poslužitelju Oxen Foundation dok koristite beta pozive." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Az IP címed látható a hívópartner és egy Oxen Foundation szerver számára a béta hívások használata közben." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր IP հասցեն տեսանելի է ձեր զանգի գործընկերոջը և մի 'Oxen Foundation' սերվերին ծիծլած օգտագործման ժամանակ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP Anda terlihat oleh mitra panggilan Anda dan server Oxen Foundation saat menggunakan panggilan beta." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il tuo IP è visibile all'utente che stai chiamando e a un server di Oxen Foundation durante l'utilizzo delle chiamate beta." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "音声通話とビデオ通話の使用中、あなたのIPはあなたの通話相手とOxen Foundationサーバーに表示されます。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი IP გააშკარავდება თქვენი ზარის პარტნიორს და Oxen Foundation-ის სერვერს, ბეტა ზარების გამოყენებისას." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP អ្នក​ត្រូវបាន​លេចឮ​ច្បាស់នៅពេល​អ្នក​ប្រើកិច្ចប្រជុំ Beta ក្នុង Oxen Foundation។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಬೇಟ ಕಾಲ್ಲನ್ನು ಬಳಸಿದಾಗ ನಿಮ್ಮ IP ಕರೆ ಸಹವಾಸಿಗೂ ಮತ್ತು Oxen Foundation ಸರ್ವರ್‌ಗೆ ಗೋಚರುತ್ತದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "베타 통화를 사용하는 동안 IP가 호출 파트너와 Oxen Foundation 서버에 보입니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "پته‌ی IP ی تۆ بۆ شەریکەکەی تیپە و سێروێری Oxen Foundation پشت بە پشت ڕەنگە بونی بوو بێت کاتێک بەکارهێنانی بەتاکوڵ بوون بوزیەکان." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adresa IP̧ya te bi dirising ti paşinspectek an hîn ya Fundaceya Oxen bêyê dîtin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yo efulugettaka mu mateeka ne server ya Oxen Foundation nga okozeza omitting ekikugya ekiriotto." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naudojant beta skambučius, jūsų IP adresas matomas jūsų pašnekovui ir Oxen Foundation serveriui." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izmantojot beta zvanus, jūsu IP ir redzams jūsu zvana partnerim un Oxen Foundation serverim." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашето IP е видливо за вашиот партнер за повик и серверот на Oxen Foundation додека користите бета повици." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны IP хаягийг ярианы хамтрагч болон Oxen Foundation сервер харагдана." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alamat IP anda boleh dilihat oleh rakan panggilan anda dan pelayan Oxen Foundation semasa menggunakan panggilan beta." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင့် IP ကို ဖုန်းခေါ်စဉ်တွင် သင်၏ ချိန်းညွှန်းပါတနာများနှင့် Oxen Foundation ဆာဗာကို မြင်ရပါသည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-adressen din er synlig for samtalepartneren din og en Oxen Foundation-server mens du bruker betasamtaler." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din IP er synlig for samtalepartneren din og en Oxen Foundation-server mens du bruker beta-samtaler." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको आइपी तपाईको कल पार्टनर र ओक्सन फाउन्डेसन सर्भरलाई बेटा कलहरू प्रयोग गर्दा देखिनेछ।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uw IP is zichtbaar voor uw oproep partner en een Oxen Foundation server tijdens het gebruik van bètagesprekken." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-adressa di er synlig for samtalepartnaren din og ein Oxen Foundation-server mens du bruker beta-samtaler." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yanu ikuwoneka kwa mnzake wanu ndi seva ya Oxen Foundation mukamagwiritsa ntchito mayitanidwe ozungulira." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡੀ ਕਾਲ ਦੇ ਦੌਰਾਨ ਤੁਹਾਡਾ IP ਸਹਿਯੋਗੀ ਅਤੇ ਇਕ Oxen Foundation ਸਰਵਰ ਨੂੰ ਦੇਖਾਈ ਦੇਵੇਗਾ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podczas korzystania z połączeń w wersji beta Twój adres IP jest widoczny dla partnera rozmowy i serwera Oxen Foundation." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو IP ستاسو د زنګ ملګري او یو Oxen Foundation سرور ته ښکاره کیږي کله چې بیتا زنګونه کاروئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seu IP estará visível para seu parceiro de chamada e para um servidor da Oxen Foundation enquanto usa chamadas beta." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "O seu IP está visível para seu parceiro de chamada e para um servidor Oxen Foundation enquanto usa chamadas beta." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-ul dumneavoastră este vizibil către partenerul de apel și către un server al Oxen Foundation atunci când utilizați apeluri beta." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш IP виден вашему собеседнику и серверу Oxen Foundation при использовании бета-вызовов." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoj IP je vidljiv tvom partneru na pozivu i serveru Oxen Foundation dok koristiš beta pozive." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ IP වැටීමට වයිස් සහ Oxen Foundation සේවාදායකයකු වෙත දැක්වේදීද පරීක්ෂණ ඇමතුම් භාවිතා කිරීමේදී." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša IP adresa je viditeľná vášmu volajúcemu partnerovi a serveru Oxen Foundation pri používaní beta hovory." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je viden vašemu partnerskemu klicatelju in strežniku Oxen Foundation med uporabo beta klicev." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-ja juaj është e dukshme për partnerin tuaj të thirrjes dhe një server të Fondacionit Oxen gjatë përdorimit të thirrjeve beta." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша IP адреса је видљива вашем сајговорнику и серверу Oxen Foundation док користите бета позиве." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je vidljiv vašem partneru za poziv i serveru Oxen Foundation dok koristite beta pozive." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din IP är synlig för din samtalspartner och en Oxen Foundation server när du använder beta-samtal." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yako inaonekana kwa mshirika wako wa simu na seva ya Oxen Foundation wakati unatumia simu za beta." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "அழைப்பு பிறையாளர் மற்றும் Oxen Foundation சர்வரில் உங்கள் ஐபி காட்டப்படவும் செய்தி சேவையின்போது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "బీటా కాల్‌లను ఉపయోగిస్తున్నప్పుడు మీ ఐపి మీ కాల్ భాగస్వామికి మరియు ఒక Oxen Foundation సర్వర్‌కు కనిపిస్తుంది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP ของคุณจะสามารถมองเห็นได้โดยคู่สายของคุณและเซิร์ฟเวอร์ของ Oxen Foundation ในขณะที่ใช้เบต้าการโทร" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deneme aramaları sırasında IP adresiniz arama ortağınıza ve Oxen Foundation sunucusuna görünür olacaktır." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша IP-адреса видима вашому партнеру по дзвінку та серверу Oxen Foundation при використанні бета-дзвінків." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "بیٹا کالز استعمال کرتے وقت آپ کا آئی پی آپ کے کال پارٹنر اور ایک اوکسن فاؤنڈیشن سرور کو نظر آئے گا۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hozirgi parolingiz noto'g'ri." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Địa chỉ IP của bạn hiển thị với đối tác cuộc gọi và máy chủ Oxen Foundation trong khi sử dụng cuộc gọi beta." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "I-IP yakho iyabonakala komnxibelele wakho nakwiOxen Foundation Server ngelixa usebenzisa i-beta calls." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "在使用测试版通话时,您的IP会暴露给您的通话对象和Oxen Foundation服务器。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您在使用測試版通話時,您的 IP 將會被通話夥伴和 Oxen Foundation 伺服器看到。" + "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." } } } @@ -83725,24 +83268,24 @@ } } }, - "change" : { + "cancelPlan" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Change" + "value" : "Cancel Plan" } } } }, - "changePasswordDescription" : { + "change" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." + "value" : "Change" } } } @@ -84226,6 +83769,17 @@ } } }, + "changePasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." + } + } + } + }, "clear" : { "extractionState" : "manual", "localizations" : { @@ -126324,6 +125878,28 @@ } } }, + "currentPassword" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current Password" + } + } + } + }, + "currentPlan" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current Plan" + } + } + } + }, "cut" : { "extractionState" : "manual", "localizations" : { @@ -127047,7 +126623,7 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "データベースエラーが発生しました。

    \nトラブルシューティングのために、アプリのログをエクスポートして共有してください。この操作が失敗した場合は、{app_name} を再インストールし、アカウントを復元してください。" + "value" : "データベースエラーが発生しました。

    トラブルシューティングのために、アプリのログをエクスポートして共有してください。この操作が失敗した場合は、{app_name} を再インストールし、アカウントを復元してください。" } }, "ko" : { @@ -238366,6 +237942,17 @@ } } }, + "important" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Important" + } + } + } + }, "incognitoKeyboard" : { "extractionState" : "manual", "localizations" : { @@ -242910,6 +242497,39 @@ } } }, + "launchOnStartDescriptionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch Session automatically when your computer starts up." + } + } + } + }, + "launchOnStartDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch on Startup" + } + } + } + }, + "launchOnStartupDisabledDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This setting is managed by your system on Linux. To enable automatic startup, add Session to your startup applications in system settings." + } + } + } + }, "learnMore" : { "extractionState" : "manual", "localizations" : { @@ -252864,6 +252484,17 @@ } } }, + "links" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links" + } + } + } + }, "loadAccount" : { "extractionState" : "manual", "localizations" : { @@ -258624,6 +258255,17 @@ } } }, + "logs" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logs" + } + } + } + }, "manageMembers" : { "extractionState" : "manual", "localizations" : { @@ -258785,6 +258427,17 @@ } } }, + "managePro" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage {pro}" + } + } + } + }, "max" : { "extractionState" : "manual", "localizations" : { @@ -293816,6 +293469,17 @@ } } }, + "newPassword" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New Password" + } + } + } + }, "next" : { "extractionState" : "manual", "localizations" : { @@ -294295,6 +293959,17 @@ } } }, + "nextSteps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Steps" + } + } + } + }, "nicknameDescription" : { "extractionState" : "manual", "localizations" : { @@ -324740,6 +324415,28 @@ } } }, + "onDevice" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On your {device_type} device" + } + } + } + }, + "onDeviceDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the {app_pro} settings." + } + } + } + }, "onionRoutingPath" : { "extractionState" : "manual", "localizations" : { @@ -329069,6 +328766,17 @@ } } }, + "openStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open {platform_store} Website" + } + } + } + }, "openSurvey" : { "extractionState" : "manual", "localizations" : { @@ -330169,967 +329877,25 @@ } } }, - "passwordChangedDescription" : { + "passwordChangedDescriptionToast" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou wagwoord is verander. Hou dit asseblief veilig." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم تغيير كلمة المرور الخاصة بك. احفظها في مامن." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolunuz dəyişdirildi. Lütfən, onu güvəndə saxlayın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک اپلیکیشن پاسکوڈ ناقض کردی. براہپس محفوظے کہ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль быў зменены. Захавайце яго ў бяспецы." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата парола беше променена. Моля, пазете я безопасно." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড পরিবর্তন করা হয়েছে। দয়া করে এটি নিরাপদ রাখুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra contrasenya s'ha definit. Mantingueu-la segura." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvé heslo bylo změněno. Pečlivě si ho odlož." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i newid. Cadwch ef yn ddiogel." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din adgangskode er blevet ændret. Venligst hold den sikker." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dein Passwort wurde geändert. Bitte bewahre es sicher auf." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει αλλάξει. Παρακαλώ κρατήστε τον ασφαλή." - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been changed. Please keep it safe." } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via pasvorto estas ŝanĝita. Bonvolu konservi ĝin sekura." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido cambiada. Por favor, manténla segura." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido cambiada. Por favor, manténla segura." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie parool on muudetud. Hoidke seda turvaliselt." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure pasahitza aldatu da. Gorde seguru batean." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور شما تغییر کرد. لطفا آن را در جای امنی نگهداری کنید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanasi on vaihdettu. Pidä se turvassa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nabago na ang iyong password. Pakisuyong itago ito." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre mot de passe a été changé. Veuillez le conserver en sécurité." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu contrasinal foi cambiado. Por favor, mantéñeo seguro." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "An canza kalmar sirrinku. Da fatan za a kiyaye shi lafiya." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמה שלך השתנתה. שמור עליה בבטחה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपका पासवर्ड बदल दिया गया है। कृपया इसे सुरक्षित रखें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je promijenjena. Molimo, čuvajte je na sigurnom." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszó megváltozott. Tartsd biztonságos helyen!" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր գաղտնաբառը փոխվել է։ Խնդրում ենք անվտանգ պահել։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata sandi anda telah diubah. Harap untuk menjaganya." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La tua password è stata modificata. Per favore tienila al sicuro." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードが変更されました。安全に保管してください。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი პაროლი შეცვლილია. გთხოვთ, შეინახეთ იგი უსაფრთხოდ." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យសម្ងាត់ របស់អ្នកត្រូវ​បាន​ប្តូរ។ សូមរក្សា​វា​ឲ្យ​មាន​សុវត្ថិភាព។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ. ಅದು ಸುರಕ್ಷಿತವಾಗಿರಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 변경이 완료되었습니다. 안전히 관리하시기 바랍니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "وشەی پەرەسەدت گۆڕدرا. تکایە ئەوە بەندەن پارێزەر بێت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te jîrêbandeya we yê danîn Muhafize mane sihîn bike." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yo ekabatiddwa. Kaakasa nti bagutemye mu kifo ekitalemerera." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsų slaptažodis buvo pakeistas. Prašome saugoti jį saugiai." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsu parole tika nomainīta. Lūdzu, saglabājiet to drošībā." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата лозинка е променета. Ве молиме чувајте ја безбедно." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны нууц үг солигдож байна. Нууц үгээ хамгаалж байгаарай." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan anda telah ditukar. Sila simpan dengan selamat." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်၏ စကားဝှက် ပြောင်းလဲ ပြီးပါပြီ။ ထိန်းသိမ်းပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er endret. Vennligst oppbevar det trygt." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er endret. Vennligst oppbevar det trygt." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईँको पासवर्ड परिवर्तन भयो। कृपया यसलाई सुरक्षित राख्नुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uw wachtwoord is gewijzigd. Hou het veilig." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er blitt endra. Vennligst oppbevar det trygt." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yanu yasinthidwa. Chonde sungani mosamala." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਸੁਰੱਖਿਅਤ ਰੱਖੋ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zmieniono hasło. Zachowaj je w bezpiecznym miejscu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو پاسورډ بدل شوی. مهرباني وکړۍ، دا خوندي وساتئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sua senha foi alterada. Por favor, mantenha-a segura." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A sua palavra-passe foi alterada. Por favor, mantenha-a segura." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parola ta a fost schimbată. Te rugăm să o păstrezi în siguranță." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль был изменен. Пожалуйста, храните его в безопасном месте." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoja šifra je promijenjena. Molimo, čuvaj je na sigurnom." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය වෙනස් කර ඇත. කරුණාකර එය ආරක්ෂිතව තබා ගන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bolo zmenené. Uchovajte ho prosím v bezpečí." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše geslo je bilo spremenjeno. Prosim, hranite ga na varnem mestu." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi juaj është ndryshuar. Ju lutemi ta mbani të sigurt." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша лозинка је промењена. Молимо вас да је сачувате." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je promenjena. Čuvajte je na sigurnom mestu." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ditt lösenord har ändrats. Håll det säkert." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenosiri lako limebadilishwa. Tafadhali lihifadhi salama." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் மாற்றப்பட்டுள்ளது. தயவுசெய்து அதை பாதுகாப்பாக வைத்திருங்கள்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ మార్పు జరిగింది. దయచేసి దాన్ని సురక్షితంగా ఉంచండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านของคุณได้รับการเปลี่ยนแปลงแล้ว กรุณารักษาเอาไว้ให้ปลอดภัย" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şifreniz değiştirildi. Lütfen güvende tutunuz." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль змінено. Будь ласка, збережіть його в безпеці." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ کا پاس ورڈ تبدیل ہو گیا ہے۔ براہ کرم اسے محفوظ رکھیں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xabar so'rovingiz hozirda kutilmoqda." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu của bạn đã được đổi. Hãy giữ nó cẩn thận." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi yakho itshintshiwe. Nceda uyigcine ikhuselekile." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密码已经设定。请妥善保管。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密碼變更完成。請注意保管。" - } } } }, - "passwordChangeDescription" : { + "passwordChangeShortDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verander die wagwoord wat benodig word om {app_name} te ontsluit." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغيير كلمة السر المطلوبة لفتح {app_name}." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu dəyişdir." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے درکار پاس ورڈ تبدیل کریں۔" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Змяніць пароль для разблакоўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сменете паролата, изисквана за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে প্রয়োজনীয় পাসওয়ার্ড পরিবর্তন করুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canvia la contrasenya requerida per desblocar {app_name}." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Změňte heslo pro odemykání {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Newid y cyfrinair sy'n angenrheidiol i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skift adgangskoden, der kræves for at låse {app_name} op." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Passwort zum Entsperren von {app_name} ändern." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αλλαγή του κωδικού πρόσβασης που απαιτείται για το ξεκλείδωμα του {app_name}." - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change the password required to unlock {app_name}." } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ŝanĝi la pasvorton, kiu necesas por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambiar la contraseña necesaria para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambiar la contraseña requerida para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Muuda parooli, mida on vaja {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور مورد نیاز برای باز کردن {app_name} را تغییر بده." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaihda {app_name} in avaukseen käytettävä salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Palitan ang password na kinakailangan para i-unlock ang {app_name}." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifier le mot de passe requis pour déverrouiller {app_name}" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia o contrasinal necesario para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canza kalmar sirrin da ake bukata don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שנה את הסיסמה הנדרשת לפתיחת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} को अनलॉक करने के लिए आवश्यक पासवर्ड बदलें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promijenite lozinku potrebnu za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} alkalmazás jelszavának megváltoztatása." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Փոխեք {app_name}-ն ապակողպելու համար պահանջվող գաղտնաբառը:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubah kata sandi yang diperlukan untuk membuka kunci {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia la password richiesta per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}のロック解除に必要なパスワードを変更します" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის შეცვლა აუცილებელია {app_name}-ის გახსნისთვის." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្ដូរពាក្យសម្ងាត់ដែលបានតម្រូវឲ្យមានដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ತೆಗೆಯಲು ಬೇಕಾದ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제 시 사용되는 비밀번호를 변경합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} وشە نهێنی بگۆڕە بۆ کردنەوەی" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "şîfreyê ku ji bo vekirina {app_name} lazim e biguherîne." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ປ່ຽນລະຫັດຕົກທາງທີ່ຈະເຜີດ {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pakeisti slaptažodį, reikalingą atrakinti {app_name}." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mainīt paroli, kas nepieciešama {app_name} atbloķēšanai." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Смени ја лозинката што е потребна за отклучување {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} -г нээхийн тулд шаардлагатай нууц үгийг өөрчлөх." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tukar kata laluan yang diperlukan untuk membuka kunci {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ဖြင့် လော့ခ်ဖွင့်ရန် လျှို့ဝှက် စကားဝှက် ပြောင်းပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som kreves for å låse opp {app_name}." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som kreves for å låse opp {app_name}." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} अनलक गर्न आवश्यक पासवर्ड परिवर्तन गर्नुहोस्।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wijzig het wachtwoord dat nodig is om {app_name} te ontgrendelen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som krevst for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਨਲੌਕ ਕਰਨ ਲਈ ਲੋੜੀਂਦੇ ਪਾਸਵਰਡ ਨੂੰ ਬਦਲੋ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zmień hasło wymagane do odblokowania aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د {app_name} خلاصولو لپاره اړین پاسورډ بدل کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altere a senha necessária para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altere a palavra-passe, necessária para desbloquear {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schimbați parola necesară pentru a debloca {app_name}." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Измените пароль, необходимый для разблокировки {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promeni lozinku potrebnu za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීමට අවශ්‍ය මුරපදය වෙනස් කරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zmeňte heslo potrebné na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spremeni geslo potrebno za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndryshoni fjalëkalimin e kërkuar për të zhbllokuar {app_name}." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Измените лозинку потребну за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promenite lozinku koja je potrebna za otključavanje {app_name}." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ändra lösenordet som krävs för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badilisha nywila inayohitajika kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ேத்தUnlock ச்சபட செய்ய வேண்டிய கடவுச்சொல்லை மாற்றவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ని అన్‌లాక్ చేయడానికి అవసరమైన పాస్‌వర్డ్ మార్చండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "เปลี่ยนรหัสผ่านที่ใช้ปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmak için gereken parolayı değiştirin." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Змінити пароль, необхідний для розблокування {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لیے مطلوبہ پاس ورڈ تبدیل کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "O'zingizga {app_name}ni ochish uchun zarur parolni o'zgartiring." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đổi mật khẩu cần thiết để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tshintsha i-password efunekayo ukusikhulula {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改{app_name}的解锁密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改解鎖 {app_name} 所需的密碼。" - } } } }, @@ -332108,17 +330874,6 @@ } } }, - "passwordDescription" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Require password to unlock {app_name} on startup." - } - } - } - }, "passwordEnter" : { "extractionState" : "manual", "localizations" : { @@ -336075,492 +334830,24 @@ } } }, - "passwordRemovedDescription" : { + "passwordRemovedDescriptionToast" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou wagwoord is verwyder." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تمت إزالة كلمة السر الخاصة بك." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolunuz silindi." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک پاسکوڈ ہٹاٹی." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль быў выдалены." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата парола беше премахната." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড সরানো হয়েছে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra contrasenya s'ha eliminat." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bylo odstraněno." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i dynnu." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din adgangskode er blevet fjernet." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dein Passwort wurde entfernt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει αφαιρεθεί." - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been removed." } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via pasvorto estas forigita." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido eliminada." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Has eliminado tu contraseña." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie parool on eemaldatud." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure pasahitza kendu da." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "گذرواژه شما حذف شده است." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanasi on on poistettu." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ang iyong password ay naalis na." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre mot de passe a été supprimé." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu contrasinal foi eliminado." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "An cire kalmar sirrinku." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמה שלך הוסרה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपका पासवर्ड हटा दिया गया है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je uklonjena." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszavadat eltávolítottuk." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր գաղտնաբառը հեռացվել է։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata sandi Anda telah dihapus." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La tua password è stata rimossa." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを削除しました。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი პაროლი წაშლილია." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យសម្ងាត់ របស់អ្នកត្រូវបានលុបចេញ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "당신의 비밀번호가 제거되었습니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "وشەی پەرەسەدت وەکبێژاند." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yo ekatutuzzibwa." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsų slaptažodis buvo pašalintas." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsu parole tika noņemta." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата лозинка е отстранета." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны нууц үг устгагдсан." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan anda telah dibuang." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်၏ စကားဝှက် ဖယ်ရှားပြီးပါပြီ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er fjernet." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt har blitt fjernet." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईँको पासवर्ड हटाइएको छ।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uw wachtwoord is verwijderd." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er blitt fjerna." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yanu yachotsedwa." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usunięto hasło" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو پاسورډ لرې شوی دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sua senha foi removida." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A sua palavra-passe foi removida." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parola ta a fost eliminată." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль удален." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoja šifra je uklonjena." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය ඉවත් කර ඇත." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bolo odstránené." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše geslo je bilo odstranjeno." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi juaj është hequr." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша лозинка је уклоњена." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je uklonjena." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ditt lösenord har tagits bort." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenosiri lako limeondolewa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் நீக்கப்பட்டது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ తొలగించబడింది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านของคุณถูกลบแล้ว" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolanız kaldırıldı." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль був видалений." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ کا پاس ورڈ ہٹا دیا گیا ہے۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolingiz saqlandi. Iltimos, uni xavfsiz joyda saqlang." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu của bạn đã được gỡ bỏ." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi yakho isusiwe." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密码已被移除。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "已移除密碼。" - } } } }, - "passwordRemoveDescription" : { + "passwordRemoveShortDescription" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." + "value" : "Remove the password required to unlock {app_name}" } } } @@ -337055,13 +335342,24 @@ } } }, - "passwordSetDescription" : { + "passwordSetDescriptionToast" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." + "value" : "Your password has been set. Please keep it safe." + } + } + } + }, + "passwordSetShortDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Require password to unlock {app_name} on startup." } } } @@ -337088,24 +335386,24 @@ } } }, - "passwordStrengthIncludesLetter" : { + "passwordStrengthIncludesLowercase" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Includes a letter" + "value" : "Includes a lowercase letter" } } } }, - "passwordStrengthIncludesLowercase" : { + "passwordStrengthIncludesSymbol" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Includes a lowercase letter" + "value" : "Includes a symbol" } } } @@ -351180,6 +349478,28 @@ } } }, + "plusLoadsMore" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus Loads More..." + } + } + } + }, + "plusLoadsMoreDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New features coming soon to {pro}. Discover what's next on the {pro} Roadmap {icon}" + } + } + } + }, "preferences" : { "extractionState" : "manual", "localizations" : { @@ -351806,6 +350126,28 @@ } } }, + "proAllSet" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You're all set!" + } + } + } + }, + "proAllSetDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan was updated! You will be billed when your current {pro} plan is automatically renewed on {date}." + } + } + } + }, "proAlreadyPurchased" : { "extractionState" : "manual", "localizations" : { @@ -352449,6 +350791,28 @@ } } }, + "proAnimatedDisplayPictures" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animated Display Pictures" + } + } + } + }, + "proAnimatedDisplayPicturesDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set animated GIFs and WebP images as your display picture." + } + } + } + }, "proAnimatedDisplayPicturesNonProModalDescription" : { "extractionState" : "manual", "localizations" : { @@ -352580,121 +350944,113 @@ } } }, - "proBadge" : { + "proAutoRenewTime" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro} Nişanı" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro} Insígnia" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odznak {app_pro}" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro}-Abzeichen" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} Badge" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insignia de {app_pro}" + "value" : "{pro} auto-renewing in {time}" } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insignia de {app_pro}" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badge {app_pro}" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro} बैज" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badge {app_pro}" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro} バッジ" - } - }, - "nl" : { + } + } + }, + "proBadge" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro}-badge" + "value" : "{pro} Badge" } - }, - "pl" : { + } + } + }, + "proBadges" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Odznaka {app_pro}" + "value" : "Badges" } - }, - "pt-PT" : { + } + } + }, + "proBadgesDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Distintivo {app_pro}" + "value" : "Show your support for {app_name} with an exclusive badge next to your display name." } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insigna {app_pro}" + } + } + }, + "proBadgesSent" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld {pro} Badge Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld {pro} Badges Sent" + } + } + } } - }, - "sv-SE" : { + } + } + }, + "proBadgeVisible" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro}-märke" + "value" : "Show {app_pro} badge to other users" } - }, - "uk" : { + } + } + }, + "proBilledAnnually" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} значок" + "value" : "{price} Billed Annually" } - }, - "zh-CN" : { + } + } + }, + "proBilledMonthly" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} 徽章" + "value" : "{price} Billed Monthly" } - }, - "zh-TW" : { + } + } + }, + "proBilledQuarterly" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} 徽章" + "value" : "{price} Billed Quarterly" } } } @@ -353098,6 +351454,105 @@ } } }, + "processingRefundRequest" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} is processing your refund request" + } + } + } + }, + "proDiscountTooltip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your current plan is already discounted by {percent}% of the full {app_pro} price." + } + } + } + }, + "proExpired" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expired" + } + } + } + }, + "proExpiredDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unfortunately, your {pro} plan has expired. Renew to keep accessing the exclusive perks and features of {app_pro}." + } + } + } + }, + "proExpiringSoon" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expiring Soon" + } + } + } + }, + "proExpiringSoonDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of {app_pro}." + } + } + } + }, + "proExpiringTime" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} expiring in {time}" + } + } + } + }, + "proFaq" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} FAQ" + } + } + } + }, + "proFaqDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Find answers to common questions in the {app_name} FAQ." + } + } + } + }, "proFeatureListAnimatedDisplayPicture" : { "extractionState" : "manual", "localizations" : { @@ -353765,6 +352220,17 @@ } } }, + "proFeatures" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Features" + } + } + } + }, "profile" : { "extractionState" : "manual", "localizations" : { @@ -356883,6 +355349,40 @@ } } }, + "proGroupsUpgraded" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Group Upgraded" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Groups Upgraded" + } + } + } + } + } + } + }, + "proImportantDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requesting a refund is final. If approved, your {pro} plan will be canceled immediately and you will lose access to all {pro} features." + } + } + } + }, "proIncreasedAttachmentSizeFeature" : { "extractionState" : "manual", "localizations" : { @@ -357121,6 +355621,73 @@ } } }, + "proLargerGroups" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Larger Groups" + } + } + } + }, + "proLargerGroupsDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groups you are an admin in are automatically upgraded to support 300 members." + } + } + } + }, + "proLongerMessages" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longer Messages" + } + } + } + }, + "proLongerMessagesDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can send messages up to 10,000 characters in all conversations." + } + } + } + }, + "proLongerMessagesSent" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Longer Message Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Longer Messages Sent" + } + } + } + } + } + } + }, "proMessageInfoFeatures" : { "extractionState" : "manual", "localizations" : { @@ -359333,6 +357900,359 @@ } } }, + "proPercentOff" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% Off" + } + } + } + }, + "proPinnedConversations" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Pinned Conversation" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Pinned Conversations" + } + } + } + } + } + } + }, + "proPlanActivatedAuto" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when {pro} is next renewed." + } + } + } + }, + "proPlanActivatedAutoShort" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}." + } + } + } + }, + "proPlanActivatedNotAuto" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan will expire on {date}.

    Update your plan now to ensure uninterrupted access to exclusive Pro features." + } + } + } + }, + "proPlanExpireDate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan will expire on {date}." + } + } + } + }, + "proPlanNotFound" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Not Found" + } + } + } + }, + "proPlanNotFoundDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No active plan was found for your account. If you believe this is a mistake, please reach out to {app_name} support for assistance." + } + } + } + }, + "proPlanPlatformRefund" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use the same {platform_account} to request a refund." + } + } + } + }, + "proPlanPlatformRefundLong" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, your refund request will be processed by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." + } + } + } + }, + "proPlanRecover" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recover {pro} Plan" + } + } + } + }, + "proPlanRenew" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew {pro} Plan" + } + } + } + }, + "proPlanRenewDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store} Stores. Because you are using {app_name} Desktop, you're not able to renew your plan here.

    {app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store} Stores. {pro} Roadmap" + } + } + } + }, + "proPlanRenewDesktopLinked" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your plan in the {app_pro} settings on a linked device with {app_name} installed via the {platform_store} or {platform_store} Store." + } + } + } + }, + "proPlanRenewDesktopStore" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your plan on the {platform_store} website using the {platform_account} you signed up for {pro} with." + } + } + } + }, + "proPlanRenewStart" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your {app_pro} plan to start using powerful {app_pro} features again." + } + } + } + }, + "proPlanRenewSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan has been renewed! Thank you for supporting the {network_name}." + } + } + } + }, + "proPlanRestored" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Restored" + } + } + } + }, + "proPlanRestoredDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A valid plan for {app_pro} was detected and your {pro} status has been restored!" + } + } + } + }, + "proPlanSignUp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use your {platform_account} to update your plan." + } + } + } + }, + "proPriceOneMonth" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Month - {monthly_price} / Month" + } + } + } + }, + "proPriceThreeMonths" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 Months - {monthly_price} / Month" + } + } + } + }, + "proPriceTwelveMonths" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 Months - {monthly_price} / Month" + } + } + } + }, + "proRefundDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We’re sorry to see you go. Here's what you need to know before requesting a refund." + } + } + } + }, + "proRefunding" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refunding {pro}" + } + } + } + }, + "proRefundingDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refunds for {app_pro} plans are handled exclusively by {platform_account} through the {platform_store} Store.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." + } + } + } + }, + "proRefundNextSteps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your {pro} status change in {app_name}." + } + } + } + }, + "proRefundRequestSessionSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your refund request will be handled by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." + } + } + } + }, + "proRefundRequestStorePolicies" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your refund request will be handled exclusively by {platform_account} through the {platform_account} website.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." + } + } + } + }, + "proRefundSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please contact {platform_account} for further updates on your refund request. Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests.

    {platform_store} Refund Support" + } + } + } + }, + "proRequestedRefund" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refund Requested" + } + } + } + }, "proSendMore" : { "extractionState" : "manual", "localizations" : { @@ -359470,6 +358390,105 @@ } } }, + "proSettings" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Settings" + } + } + } + }, + "proStats" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} Stats" + } + } + } + }, + "proStatsTooltip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} stats reflect usage on this device and may appear differently on linked devices" + } + } + } + }, + "proSupportDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Need help with your {pro} plan? Submit a request to the support team." + } + } + } + }, + "proTosPrivacy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By updating, you agree to the {app_pro} Terms of Service {icon} and Privacy Policy {icon}" + } + } + } + }, + "proUnlimitedPins" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlimited Pins" + } + } + } + }, + "proUnlimitedPinsDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organize all your chats with unlimited pinned conversations." + } + } + } + }, + "proUpdatePlanDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of {pro} access." + } + } + } + }, + "proUpdatePlanExpireDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your plan will expire on {date}.

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access." + } + } + } + }, "proUserProfileModalCallToAction" : { "extractionState" : "manual", "localizations" : { @@ -373129,478 +372148,10 @@ "recoveryPasswordView" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kyk Wagwoord" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "عرض كلمة المرور" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolu göstər" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ دیکھیں" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паказаць пароль" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Преглед на паролата" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "পাসওয়ার্ড দেখুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra la contrasenya" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zobrazit heslo" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gweld Cyfrinair" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis adgangskode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passwort anzeigen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Προβολή Κωδικού" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "View Password" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vidi Pasvorton" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contraseña" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contraseña" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Näita parooli" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikusi pasahitza" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "نمایش گذرواژه" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Näytä salasana" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "View Password" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher le mot de passe" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contrasinal" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Duba kalmar wucewa" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הצג סיסמא" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड देखें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pogledaj lozinku" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó megtekintése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Դիտել Գաղտնաբառը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lihat Kata Sandi" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Visualizza password" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを見る" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის პარამეტრების ჩვენება" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "មើលពាក្យសម្ងាត់" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಹುಡುಕಿ ನೋಡಿ ಪಾಸ್ವರ್ಡ್" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 보기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "بینینی وشەی نهێنی" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaziniya Password" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Laba Password" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rodyti slaptažodį" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parādīt paroli" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Преглед на лозинка" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нууц үгийг харах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lihat Kata Laluan" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password ကြည့်ပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis Password" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis passord" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड हेर्नुहोस्" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bekijk wachtwoord" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis passord" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onani Chinsinsi" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਪਾਸਵਰਡ ਵੇਖੋ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zobacz hasło" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورډ وګورئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver Senha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver Palavra-passe" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vizualizați parola" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Посмотреть пароль" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prikaži lozinku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මුරපදය පෙන්වන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zobraziť heslo" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ogled gesla" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shihni Fjalëkalimin" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Погледај лозинку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pregled lozinke" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Visa lösenord" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tazama Nywila" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கடவுச்சொல்லைக் காண்பிக்கவும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "పాస్‌వర్డ్ చూడండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ดูรหัสผ่าน." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şifreyi Görüntüle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показати пароль" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ دیکھیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolni ko‘rish" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xem Mật khẩu" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jonga Iphasiwedi" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "查看密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢視密碼" + "value" : "View Recovery Password" } } } @@ -374753,6 +373304,17 @@ } } }, + "refundPlanNonOriginatorApple" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via a different {platform_account}, you'll need to use that {platform_account} to update your plan." + } + } + } + }, "remainingCharactersOverTooltip" : { "extractionState" : "manual", "localizations" : { @@ -376291,6 +374853,28 @@ } } }, + "removePasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." + } + } + } + }, + "renew" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew" + } + } + } + }, "reply" : { "extractionState" : "manual", "localizations" : { @@ -376770,6 +375354,17 @@ } } }, + "requestRefund" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request Refund" + } + } + } + }, "resend" : { "extractionState" : "manual", "localizations" : { @@ -397802,6 +396397,17 @@ } } }, + "sessionProBeta" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + } + } + }, "sessionRecoveryPassword" : { "extractionState" : "manual", "localizations" : { @@ -399400,6 +398006,28 @@ } } }, + "setPasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." + } + } + } + }, + "settingsCannotChangeDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot Update Setting" + } + } + } + }, "settingsRestartDescription" : { "extractionState" : "manual", "localizations" : { @@ -399879,6 +398507,17 @@ } } }, + "settingsStartCategoryDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Startup" + } + } + } + }, "share" : { "extractionState" : "manual", "localizations" : { @@ -407279,6 +405918,17 @@ } } }, + "theReturn" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Return" + } + } + } + }, "tooltipAccountIdVisible" : { "extractionState" : "manual", "localizations" : { @@ -414274,6 +412924,28 @@ } } }, + "updatePlan" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Plan" + } + } + } + }, + "updatePlanTwo" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Two ways to update your plan:" + } + } + } + }, "updateProfileInformation" : { "extractionState" : "manual", "localizations" : { @@ -418293,6 +416965,17 @@ } } }, + "urlOpenDescriptionAlternative" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links will open in your browser." + } + } + } + }, "useFastMode" : { "extractionState" : "manual", "localizations" : { @@ -418772,6 +417455,28 @@ } } }, + "viaStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via the {platform_store} website" + } + } + } + }, + "viaStoreWebsiteDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website." + } + } + } + }, "video" : { "extractionState" : "manual", "localizations" : { diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index f8cd66b791..85ccadd231 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -16,4 +16,5 @@ public enum Constants { public static let session_network_data_price: String = "Price data powered by CoinGecko
    Accurate at {date_time}" public static let app_pro: String = "Session Pro" public static let session_foundation: String = "Session Foundation" + public static let pro: String = "Pro" } From bce7330fdade4613bf7ee8838a1a95f369f64e6d Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 29 Aug 2025 10:15:37 +0800 Subject: [PATCH 136/244] Additional code clean ups --- .../App Review/AppReviewPromptModel.swift | 34 ++++++++----------- Session/Home/HomeViewModel.swift | 25 +++++++------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 26332b1b64..aa5debf039 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -16,8 +16,12 @@ struct AppReviewPromptModel { } extension AppReviewPromptModel { + // Base version where app review prompt became available + // TODO: Update this once a version to include app review prompt is decided + static private let reviewPromptAvailabilityVersion = "2.14.1" // stringlint:ignore + /// Determines the initial state of the app review prompt. - static func loadInitialAppReviewPromptState(_ dependencies: Dependencies) -> (promptState: AppReviewPromptState?, wasInstalledPriorToAppReviewRelease: Bool) { + static func loadInitialAppReviewPromptState(using dependencies: Dependencies) -> AppReviewPromptState? { /// Check if incomplete app review can be shown again to user on next app launch let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] @@ -41,30 +45,22 @@ extension AppReviewPromptModel { promptState = .enjoyingSession } - let wasInstalledPriorToAppReviewRelease = checkIfAppWasInstalledPriorToAppReviewRelease(dependencies) - - return (promptState: promptState, wasInstalledPriorToAppReviewRelease: wasInstalledPriorToAppReviewRelease) + return promptState } /// Checks if version was from install or from update - static private func checkIfAppWasInstalledPriorToAppReviewRelease(_ dependencies: Dependencies) -> Bool { - // Base version where app review prompt became available - // TODO: Update this once a version to include app review prompt is decided - let reviewPromptAvailabilityVersion = "2.14.1" // stringlint:ignore - + static func checkIfAppWasInstalledPriorToAppReviewRelease(using dependencies: Dependencies) -> Bool { guard let firstAppVersion = dependencies[cache: .appVersion].firstAppVersion else { - return true - } - - let comparisonResult = firstAppVersion.compare(reviewPromptAvailabilityVersion, options: .numeric) - - if comparisonResult == .orderedAscending { - // App was updated to the latest version with app review prompt return false - } else { - // App was installed not updated to new version - return true } + + let comparisonResult = firstAppVersion.compare( + reviewPromptAvailabilityVersion, + options: .numeric + ) + + // App was updated to the latest version with app review prompt + return comparisonResult == .orderedAscending } } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index b47f0e6918..441f5e8a90 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -48,15 +48,12 @@ public class HomeViewModel: NavigatableStateHolder { self.dependencies = dependencies self.userSessionId = dependencies[cache: .general].sessionId - // The `appReview` variable is a tuple with the following elements: - // .promptState: AppReviewPromptState? - // .wasInstalledPriorToAppReviewRelease: Bool - let appReview = AppReviewPromptModel.loadInitialAppReviewPromptState(dependencies) - self.state = State.initialState( using: dependencies, - appReviewPromptState: appReview.promptState, - appWasInstalledPriorToAppReviewRelease: appReview.wasInstalledPriorToAppReviewRelease + appReviewPromptState: AppReviewPromptModel + .loadInitialAppReviewPromptState(using: dependencies), + appWasInstalledPriorToAppReviewRelease: AppReviewPromptModel + .checkIfAppWasInstalledPriorToAppReviewRelease(using: dependencies) ) /// Bind the state @@ -400,15 +397,17 @@ public class HomeViewModel: NavigatableStateHolder { pendingAppReviewPromptState = nil } else { groupedOtherEvents?[.userDefault]?.forEach { event in - switch (event.key, event.value) { - case (.userDefault(.hasVisitedPathScreen), let value as Bool) where value == true: - if appWasInstalledPriorToAppReviewRelease { pendingAppReviewPromptState = .enjoyingSession } + guard let value: Bool = event.value as? Bool else { return } + + switch (event.key, value, appWasInstalledPriorToAppReviewRelease) { + case (.userDefault(.hasVisitedPathScreen), true, false): + pendingAppReviewPromptState = .enjoyingSession - case (.userDefault(.hasPressedDonateButton), let value as Bool) where value == true: + case (.userDefault(.hasPressedDonateButton), true, _): pendingAppReviewPromptState = .enjoyingSession - case (.userDefault(.hasChangedTheme), let value as Bool) where value == true: - if appWasInstalledPriorToAppReviewRelease { pendingAppReviewPromptState = .enjoyingSession } + case (.userDefault(.hasChangedTheme), true, false): + pendingAppReviewPromptState = .enjoyingSession default: break } From 6fb9e3534292e451c33c93e2d89b5ae7eb7bd006 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 29 Aug 2025 15:36:44 +1000 Subject: [PATCH 137/244] Removed a database call from onboarding --- Session/Onboarding/Onboarding.swift | 26 ++-- .../_TestUtilities/MockLibSessionCache.swift | 1 + SessionTests/Onboarding/OnboardingSpec.swift | 115 +++++++++++------- 3 files changed, 89 insertions(+), 53 deletions(-) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 52da3682fa..00eb1600a2 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -106,17 +106,21 @@ extension Onboarding { self.id = dependencies.randomUUID() self.initialFlow = flow - /// Try to load the users `ed25519KeyPair` from the database and generate the `x25519KeyPair` from it - var ed25519KeyPair: KeyPair = .empty - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - dependencies[singleton: .storage].readAsync( - retrieve: { db in Identity.fetchUserEd25519KeyPair(db) }, - completion: { result in - ed25519KeyPair = ((try? result.successOrThrow()) ?? .empty) - semaphore.signal() - } - ) - semaphore.wait() + /// Try to load the users `ed25519SecretKey` from the general cache and generate the key pairs from it + let ed25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey + let ed25519KeyPair: KeyPair = { + guard + !ed25519SecretKey.isEmpty, + let ed25519Seed: Data = dependencies[singleton: .crypto].generate( + .ed25519Seed(ed25519SecretKey: ed25519SecretKey) + ), + let ed25519KeyPair: KeyPair = dependencies[singleton: .crypto].generate( + .ed25519KeyPair(seed: Array(ed25519Seed)) + ) + else { return .empty } + + return ed25519KeyPair + }() let x25519KeyPair: KeyPair = { guard ed25519KeyPair != .empty, diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index e27b816544..d6d04cce23 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -355,6 +355,7 @@ extension Mock where T == LibSessionCacheType { .when { try $0.pendingPushes(swarmPublicKey: .any) } .thenReturn(LibSession.PendingPushes()) self.when { $0.configNeedsDump(.any) }.thenReturn(false) + self.when { $0.activeHashes(for: .any) }.thenReturn([]) self .when { try $0.createDump(config: .any, for: .any, sessionId: .any, timestampMs: .any) } .thenReturn(nil) diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e6a4e41559..a9e37a2154 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -43,6 +43,14 @@ class OnboardingSpec: AsyncSpec { crypto .when { $0.generate(.randomBytes(.any)) } .thenReturn(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8])) + crypto + .when { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenReturn(Data([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2 + ])) crypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn( @@ -73,7 +81,8 @@ class OnboardingSpec: AsyncSpec { ) @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in - defaults.when { $0.bool(forKey: .any) }.thenReturn(true) + defaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) + defaults.when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) }.thenReturn(false) defaults.when { $0.integer(forKey: .any) }.thenReturn(2) defaults.when { $0.set(true, forKey: .any) }.thenReturn(()) defaults.when { $0.set(false, forKey: .any) }.thenReturn(()) @@ -190,9 +199,10 @@ class OnboardingSpec: AsyncSpec { } } - // MARK: -- without a stored key pair - context("without a stored key pair") { + // MARK: -- without a stored secret key + context("without a stored secret key") { beforeEach { + mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) @@ -214,19 +224,20 @@ class OnboardingSpec: AsyncSpec { } } - // MARK: -- with a stored key pair - context("with a stored key pair") { + // MARK: -- with a stored secret key + context("with a stored secret key") { beforeEach { - mockStorage.write { db in - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: TestConstants.edPublicKey) - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockGeneralCache + .when { $0.ed25519SecretKey } + .thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) } // MARK: ---- does not generate a seed @@ -266,22 +277,23 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and failing to generate an x25519KeyPair context("and failing to generate an x25519KeyPair") { beforeEach { - mockStorage.write { db in - try Identity.deleteAll(db) - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: "090807") - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto - .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } - .thenReturn(nil) + .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenThrow(MockError.mockedData) mockCrypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .when { + $0.generate(.ed25519KeyPair( + seed: Array(Data(hex: TestConstants.edSecretKey)) + )) + } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] + )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } @@ -289,6 +301,9 @@ class OnboardingSpec: AsyncSpec { mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) + mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } + .thenReturn(nil) } // MARK: ------ generates new credentials @@ -321,6 +336,9 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and an existing display name context("and an existing display name") { beforeEach { + mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } + .thenReturn(true) mockLibSession .when { $0.profile( @@ -359,26 +377,33 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ after generating new credentials context("after generating new credentials") { beforeEach { - mockStorage.write { db in - try Identity.deleteAll(db) - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: "090807") - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto - .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } - .thenReturn(nil) + .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenThrow(MockError.mockedData) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: Array(Data(hex: TestConstants.edSecretKey)) + )) + } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] + )) } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } .thenReturn([4, 3, 2, 1]) mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) + mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } + .thenReturn(nil) } // MARK: -------- has an empty display name @@ -390,6 +415,12 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and a missing display name context("and a missing display name") { + beforeEach { + mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } + .thenReturn(true) + } + // MARK: ------ has an empty display name it("has an empty display name") { expect(cache.displayName).to(equal("")) From b79aedd0a47894d7eb17295107df9ad790925cee Mon Sep 17 00:00:00 2001 From: mpretty-cyro <15862619+mpretty-cyro@users.noreply.github.com> Date: Mon, 1 Sep 2025 00:43:10 +0000 Subject: [PATCH 138/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 138 +++++++++++------- 1 file changed, 89 insertions(+), 49 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 4b0b2bdd5b..48296c8587 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -350992,18 +350992,28 @@ "extractionState" : "manual", "localizations" : { "en" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld {pro} Badge Sent" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld {pro} Badges Sent" + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Badge Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Badges Sent" + } + } } } } @@ -355353,18 +355363,28 @@ "extractionState" : "manual", "localizations" : { "en" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Group Upgraded" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Groups Upgraded" + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Group Upgraded" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Groups Upgraded" + } + } } } } @@ -355669,18 +355689,28 @@ "extractionState" : "manual", "localizations" : { "en" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Longer Message Sent" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Longer Messages Sent" + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Longer Message Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Longer Messages Sent" + } + } } } } @@ -357915,18 +357945,28 @@ "extractionState" : "manual", "localizations" : { "en" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Pinned Conversation" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Pinned Conversations" + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Pinned Conversation" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Pinned Conversations" + } + } } } } @@ -358127,7 +358167,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use your {platform_account} to update your plan." + "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use your {platform_account} to update your plan." } } } From 337da68cd6514c13620db7ebf76663fbdacad1bc Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 10:57:40 +0800 Subject: [PATCH 139/244] Added app state observer to check for review retry state --- .../App Review/AppReviewPromptModel.swift | 28 ++++++++++++------- Session/Home/HomeVC.swift | 18 ++++++++++++ Session/Home/HomeViewModel.swift | 25 ++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index aa5debf039..dd2dbea739 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -22,30 +22,38 @@ extension AppReviewPromptModel { /// Determines the initial state of the app review prompt. static func loadInitialAppReviewPromptState(using dependencies: Dependencies) -> AppReviewPromptState? { - /// Check if incomplete app review can be shown again to user on next app launch - let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - var promptState: AppReviewPromptState? + if shouldShowAppReviewAgain(using: dependencies) { + promptState = .rateSession + + } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && !dependencies[defaults: .standard, key: .didActionAppReviewPrompt] { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .enjoyingSession + } + + return promptState + } + + static func shouldShowAppReviewAgain(using dependencies: Dependencies) -> Bool { + /// Check if incomplete app review can be shown again to user on next app launch + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + // A buffer of 24 hours let buffer: TimeInterval = 24 * 60 * 60 if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { // This block will execute if the current time is within 24 hours of the retryDate // or if the current time is past the retryDate. - dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - promptState = .rateSession - } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && !dependencies[defaults: .standard, key: .didActionAppReviewPrompt] { - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false - - promptState = .enjoyingSession + return true } - return promptState + return false } /// Checks if version was from install or from update diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 811a4d5796..ecab5c8318 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -361,6 +361,14 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi bindViewModel() viewModel.navigatableState.setupBindings(viewController: self, disposables: &disposables) + + // Notification + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) } public override func viewDidAppear(_ animated: Bool) { @@ -371,6 +379,10 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi viewModel.viewDidAppear() } + deinit { + NotificationCenter.default.removeObserver(self) + } + // MARK: - Updating @MainActor public func afterInitialConversationsLoaded(_ closure: @escaping () -> Void) { @@ -805,6 +817,12 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi viewModel.dependencies[singleton: .app].createNewConversation() } + @objc func applicationDidBecomeActive(_ notification: Notification) { + DispatchQueue.main.async { [weak self] in + self?.viewModel.didReturnFromBackground() + } + } + func createNewDMFromDeepLink(sessionId: String) { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: NewMessageScreen(accountId: sessionId, using: viewModel.dependencies) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 441f5e8a90..829e2ef466 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -47,7 +47,7 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor init(using dependencies: Dependencies) { self.dependencies = dependencies self.userSessionId = dependencies[cache: .general].sessionId - + self.state = State.initialState( using: dependencies, appReviewPromptState: AppReviewPromptModel @@ -537,13 +537,17 @@ public class HomeViewModel: NavigatableStateHolder { } // MARK: - Handle App review - @MainActor func viewDidAppear() { + @MainActor + func viewDidAppear() { guard state.pendingAppReviewPromptState != nil else { return } DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in // Set flag that review prompt was already presented dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false + + if state.appReviewPromptState != .rateSession { + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false + } dependencies.notifyAsync( priority: .immediate, @@ -555,7 +559,7 @@ public class HomeViewModel: NavigatableStateHolder { ) } } - + func scheduleAppReviewRetry() { /// Wait 2 weeks before trying again dependencies[defaults: .standard, key: .rateAppRetryDate] = dependencies.dateNow @@ -693,6 +697,19 @@ public class HomeViewModel: NavigatableStateHolder { default: break } } + + @MainActor + @objc func didReturnFromBackground() { + // Observe changes to app state retry and flags when app goes to bg to fg + if AppReviewPromptModel.shouldShowAppReviewAgain(using: dependencies) { + // Handles scenario where app is in background -> foreground when the retry date is hit + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in + // Set flag that review prompt was already presented + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + self?.handlePromptChangeState(.rateSession) + } + } + } // MARK: - Functions From e1b1416a36f83e5ca6f62c4e52b91afbcefbca30 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 10:58:31 +0800 Subject: [PATCH 140/244] Updated feedback url --- SessionUIKit/Style Guide/Constants+Apple.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionUIKit/Style Guide/Constants+Apple.swift b/SessionUIKit/Style Guide/Constants+Apple.swift index 17b9f02c72..5accc01675 100644 --- a/SessionUIKit/Style Guide/Constants+Apple.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -7,7 +7,7 @@ public extension Constants { static let session_staking_url = "https://docs.getsession.org/session-network/staking" static let session_token_url = "https://token.getsession.org" static let session_donations_url = "https://session.foundation/donate#app" - static let session_feedback_url = "https://www.surveymonkey.com/r/YLDZJR8" + static let session_feedback_url = "https://getsession.org/feedback" // MARK: - Names static let store_name = "App Store" From 14df9e0eabecab7f912f96ec0a28fa02f67e28e0 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 11:10:30 +0800 Subject: [PATCH 141/244] Fix bottom padding when app review prompt has no buttons --- Session/Home/App Review/View/AppReviewPromptDialog.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index e37cd44601..88d40bbe78 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -149,6 +149,15 @@ class AppReviewPromptDialog: UIView { secondaryButton.isHidden = prompt?.promptContent.secondaryButtonTitle == nil secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) secondaryButton.accessibilityIdentifier = prompt?.promptContent.secondaryButtonAccessibilityIdentifier + + let isButtonsHidden = primaryButton.isHidden && secondaryButton.isHidden + + buttonStack.layoutMargins = .init( + top: isButtonsHidden ? 0: Values.mediumSpacing, + left: 0, + bottom: Values.mediumSpacing, + right: 0 + ) } @objc From f7b458cf92f63d90ee4d5804ebee3bc4d429d378 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 11:12:45 +0800 Subject: [PATCH 142/244] Changed retry buffer to from 24hrs to 1hr --- Session/Home/App Review/AppReviewPromptModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index dd2dbea739..bc3babc608 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -40,9 +40,9 @@ extension AppReviewPromptModel { /// Check if incomplete app review can be shown again to user on next app launch let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] - // A buffer of 24 hours - let buffer: TimeInterval = 24 * 60 * 60 - + // A buffer of 1 hour + let buffer: TimeInterval = 60 * 60 + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { // This block will execute if the current time is within 24 hours of the retryDate // or if the current time is past the retryDate. From df036896b90f190c5768223943d4bb80b1890aed Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 13:20:00 +0800 Subject: [PATCH 143/244] Code clean ups and improvements --- .../App Review/AppReviewPromptModel.swift | 6 ++-- .../View/AppReviewPromptDialog.swift | 6 ---- Session/Home/HomeViewModel.swift | 35 ++++++++----------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index bc3babc608..9d9d82e2c8 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -24,7 +24,7 @@ extension AppReviewPromptModel { static func loadInitialAppReviewPromptState(using dependencies: Dependencies) -> AppReviewPromptState? { var promptState: AppReviewPromptState? - if shouldShowAppReviewAgain(using: dependencies) { + if checkAndRefreshAppReviewState(using: dependencies) { promptState = .rateSession } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && !dependencies[defaults: .standard, key: .didActionAppReviewPrompt] { @@ -36,7 +36,7 @@ extension AppReviewPromptModel { return promptState } - static func shouldShowAppReviewAgain(using dependencies: Dependencies) -> Bool { + static func checkAndRefreshAppReviewState(using dependencies: Dependencies) -> Bool { /// Check if incomplete app review can be shown again to user on next app launch let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] @@ -44,7 +44,7 @@ extension AppReviewPromptModel { let buffer: TimeInterval = 60 * 60 if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { - // This block will execute if the current time is within 24 hours of the retryDate + // This block will execute if the current time is within 1 hour of the retryDate // or if the current time is past the retryDate. dependencies[defaults: .standard, key: .rateAppRetryDate] = nil dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift index 88d40bbe78..f1878753ea 100644 --- a/Session/Home/App Review/View/AppReviewPromptDialog.swift +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -84,12 +84,6 @@ class AppReviewPromptDialog: UIView { result.distribution = .fillEqually result.alignment = .fill result.isLayoutMarginsRelativeArrangement = true - result.layoutMargins = .init( - top: Values.mediumSpacing, - left: 0, - bottom: Values.mediumSpacing, - right: 0 - ) return result }() diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 829e2ef466..7239cc427e 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -541,22 +541,12 @@ public class HomeViewModel: NavigatableStateHolder { func viewDidAppear() { guard state.pendingAppReviewPromptState != nil else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [self, dependencies] in - // Set flag that review prompt was already presented - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in + guard let updatedState: AppReviewPromptState = self?.state.pendingAppReviewPromptState else { return } - if state.appReviewPromptState != .rateSession { - dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false - } + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false - dependencies.notifyAsync( - priority: .immediate, - key: .updateScreen(HomeViewModel.self), - value: HomeViewModelEvent( - pendingAppReviewPromptState: nil, - appReviewPromptState: state.pendingAppReviewPromptState - ) - ) + self?.handlePromptChangeState(updatedState) } } @@ -567,9 +557,13 @@ public class HomeViewModel: NavigatableStateHolder { } func handlePromptChangeState(_ state: AppReviewPromptState?) { - // Prompt closed from `x` button of prompt + // Set`didActionAppReviewPrompt` to true when closed from `x` button of prompt + // or in show rate limit prompt so it does not show again on relaunch if state == nil || state == .rateLimit { dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true } + // Set `didShowAppReviewPrompt` when a new state is presented + if state != nil { dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true } + dependencies.notifyAsync( priority: .immediate, key: .updateScreen(HomeViewModel.self), @@ -701,12 +695,13 @@ public class HomeViewModel: NavigatableStateHolder { @MainActor @objc func didReturnFromBackground() { // Observe changes to app state retry and flags when app goes to bg to fg - if AppReviewPromptModel.shouldShowAppReviewAgain(using: dependencies) { + if AppReviewPromptModel.checkAndRefreshAppReviewState(using: dependencies) { + // state.appReviewPromptState check so it does not replace existing prompt if there is any + let updatedState = state.appReviewPromptState ?? .rateSession + // Handles scenario where app is in background -> foreground when the retry date is hit - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in - // Set flag that review prompt was already presented - dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true - self?.handlePromptChangeState(.rateSession) + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in + self?.handlePromptChangeState(updatedState) } } } From e5fc9f20c7095c0c120a7acce0f554444eeb65e7 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 1 Sep 2025 09:07:29 +0800 Subject: [PATCH 144/244] Added skip authentication flags for some group api calls --- .../Jobs/DisplayPictureDownloadJob.swift | 20 ++++---- .../RetrieveDefaultOpenGroupRoomsJob.swift | 6 ++- .../Open Groups/OpenGroupAPI.swift | 48 +++++++++++-------- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 6 ++- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index ec9ee87bc3..d379337d09 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -48,7 +48,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { using: dependencies ) - case .community(let fileId, let roomToken, let server): + case .community(let fileId, let roomToken, let server, let skipAuthentication): guard let info: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) @@ -58,7 +58,8 @@ public enum DisplayPictureDownloadJob: JobExecutor { fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), - using: dependencies + using: dependencies, + skipAuthentication: skipAuthentication ) } } @@ -206,7 +207,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { ) db.addConversationEvent(id: id, type: .updated(.displayPictureUrl(url))) - case .community(_, let roomToken, let server): + case .community(_, let roomToken, let server, _): _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: server)) .updateAllAndConfig( @@ -228,7 +229,7 @@ extension DisplayPictureDownloadJob { public enum Target: Codable, Hashable, CustomStringConvertible { case profile(id: String, url: String, encryptionKey: Data) case group(id: String, url: String, encryptionKey: Data) - case community(imageId: String, roomToken: String, server: String) + case community(imageId: String, roomToken: String, server: String, skipAuthentication: Bool = false) var isValid: Bool { switch self { @@ -239,7 +240,7 @@ extension DisplayPictureDownloadJob { encryptionKey.count == DisplayPictureManager.aes256KeyByteLength ) - case .community(let imageId, _, _): return !imageId.isEmpty + case .community(let imageId, _, _, _): return !imageId.isEmpty } } @@ -249,7 +250,7 @@ extension DisplayPictureDownloadJob { switch self { case .profile(let id, _, _): return "profile: \(id)" case .group(let id, _, _): return "group: \(id)" - case .community(_, let roomToken, let server): return "room: \(roomToken) on server: \(server)" + case .community(_, let roomToken, let server, _): return "room: \(roomToken) on server: \(server)" } } } @@ -274,11 +275,12 @@ extension DisplayPictureDownloadJob { self.target = { switch target { - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, let skipAuthentication): return .community( imageId: imageId, roomToken: roomToken, - server: server.lowercased() // Always in lowercase on `OpenGroup` + server: server.lowercased(), // Always in lowercase on `OpenGroup` + skipAuthentication: skipAuthentication ) default: return target @@ -358,7 +360,7 @@ extension DisplayPictureDownloadJob { return (url == latestDisplayPictureUrl) - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, _): guard let latestImageId: String = try? OpenGroup .select(.imageId) diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index a864b6c2fe..5ec27d9348 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -72,7 +72,8 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in try OpenGroupAPI.preparedCapabilitiesAndRooms( authMethod: authMethod, - using: dependencies + using: dependencies, + skipAuthentication: true ).send(using: dependencies) } .subscribe(on: scheduler, using: dependencies) @@ -157,7 +158,8 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { target: .community( imageId: imageId, roomToken: room.token, - server: OpenGroupAPI.defaultServer + server: OpenGroupAPI.defaultServer, + skipAuthentication: true ), timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 78b97e3f08..506821f670 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -165,9 +165,10 @@ public enum OpenGroupAPI { private static func preparedSequence( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, - using dependencies: Dependencies + using dependencies: Dependencies, + skipAuthentication: Bool = false ) throws -> Network.PreparedRequest> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( method: .post, endpoint: Endpoint.sequence, @@ -178,7 +179,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Capabilities @@ -192,9 +193,10 @@ public enum OpenGroupAPI { /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` public static func preparedCapabilities( authMethod: AuthenticationMethod, - using dependencies: Dependencies + using dependencies: Dependencies, + skipAuthentication: Bool = false ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .capabilities, authMethod: authMethod @@ -203,7 +205,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Room @@ -213,9 +215,10 @@ public enum OpenGroupAPI { /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included public static func preparedRooms( authMethod: AuthenticationMethod, - using dependencies: Dependencies + using dependencies: Dependencies, + skipAuthentication: Bool = false ) throws -> Network.PreparedRequest<[Room]> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .rooms, authMethod: authMethod @@ -224,7 +227,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) } /// Returns the details of a single room @@ -326,20 +329,25 @@ public enum OpenGroupAPI { /// methods for the documented behaviour of each method public static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, - using dependencies: Dependencies + using dependencies: Dependencies, + skipAuthentication: Bool = false ) throws -> Network.PreparedRequest { - return try OpenGroupAPI + let preparedRequest = try OpenGroupAPI .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies), - preparedRooms(authMethod: authMethod, using: dependencies) + preparedCapabilities(authMethod: authMethod, using: dependencies, skipAuthentication: skipAuthentication), + preparedRooms(authMethod: authMethod, using: dependencies, skipAuthentication: skipAuthentication) ], authMethod: authMethod, - using: dependencies + using: dependencies, + skipAuthentication: skipAuthentication ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + let finalRequest = skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return finalRequest .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data @@ -364,7 +372,7 @@ public enum OpenGroupAPI { capabilities: (info: capabilitiesInfo, data: capabilities), rooms: (info: roomsInfo, data: (roomsResponse.body ?? [])) ) - } + } } // MARK: - Messages @@ -839,9 +847,10 @@ public enum OpenGroupAPI { fileId: String, roomToken: String, authMethod: AuthenticationMethod, - using dependencies: Dependencies + using dependencies: Dependencies, + skipAuthentication: Bool = false ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), authMethod: authMethod @@ -851,7 +860,8 @@ public enum OpenGroupAPI { requestTimeout: Network.fileDownloadTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Inbox/Outbox (Message Requests) diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 39d2bff89d..d4f58196be 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -438,7 +438,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: OpenGroupAPI.defaultServer, + skipAuthentication: true ), timestamp: 1234567890 ) @@ -485,7 +486,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: OpenGroupAPI.defaultServer, + skipAuthentication: true ), timestamp: 1234567890 ) From 75ec07ceb0b806c225b810cc2055395731d6d096 Mon Sep 17 00:00:00 2001 From: Bilb <1544279+Bilb@users.noreply.github.com> Date: Mon, 1 Sep 2025 09:52:08 +0000 Subject: [PATCH 145/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 470 +----------------- 1 file changed, 1 insertion(+), 469 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 48296c8587..11c67635b7 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -118923,478 +118923,10 @@ "conversationsEnterDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funksie van die Enter-sleutel wanneer u 'n gesprek tik." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "وظيفة مفتاح الإدخال عند الكتابة في محادثة." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bir danışıqda yazarkən Enter düyməsinin funksiyası." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "واژئے کُنجیء کارکردگی ہدباتءَ نیںّ گفتگوئے شتگ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функцыя клавішы Enter пры наборы тэксту ў гутарцы." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функция на клавиша Enter, когато пишете в разговор." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "কথোপকথনে টাইপ করার সময় এন্টার কী এর ফাংশন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funció de la tecla d'introducció durant l'escriptura en una conversa." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkce klávesy Enter při psaní v konverzaci." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Swyddogaeth allwedd enter wrth deipio mewn sgwrs." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funktion af enter-tasten, når du skriver i en samtale." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funktion der Eingabetaste beim Tippen in einer Unterhaltung." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Λειτουργία του πλήκτρου εισαγωγής κατά την πληκτρολόγηση σε συνομιλία." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Function of the enter key when typing in a conversation." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcio de la eniga klavo dum tajpado en konversacio." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Función de la tecla enter al escribir en una conversación." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Función de la tecla Enter al escribir en una conversación." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter-klahvi funktsioon vestluse ajal tippimisel." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sartu tekla funtzioa elkarrizketan idazten ari zarenean." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "ویژگی کلید Enter در هنگام تایپ در یک مکالمه." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter-näppäimen toiminto keskustelussa kirjoittaessa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gamit ng enter key kapag nagta-type sa isang usapan." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fonction de la touche Entrée lors de la saisie dans une conversation." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Función da tecla Enter ao escribir nunha conversa." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aikin maɓallin shigar yayin rubutu a cikin tattaunawa." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "פונקציה של מקש ה-Enter בעת הקלדה בשיחה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "बातचीत में टाइप करते समय एंटर कुंजी का कार्य।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcija tipke enter pri pisanju u razgovoru." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Az Enter billentyű funkciója beszélgetés közben." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter ստեղնի գործառույթը զրույցի ժամանակ:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fungsi tombol enter saat mengetik dalam percakapan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funzione del tasto invio quando si digita in una chat." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "会話中のEnterキーの機能" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ფუნქცია, როცა კლავიატურაზე იყენებთ Enter ღილაკს საუბრის დროს." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "មុខងាររបស់ឃី enter នៅពេលវាយក្នុងការសន្ទនា។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸಂಭಾಷಣೆಯಲ್ಲಿ ಟೈಪಿಂಗ್‌ನಲ್ಲಿ ಎಂಟರ್ ಕೀಯ ತಂತ್ರಜ್ಞಾನ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "대화에서 Enter 키 기능." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "فەرمیۆنی لەگەڵ داخستنی کلیلەکەرە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Karwilaşa tuşî zimanê gava niqaşe." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enkola ya ki Enter okunyiga kulwokola message nga wandikira mu kwogereza." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Įveskite pokalbio metu naudojamo enter klavišo funkciją." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter taustiņa funkcija, rakstot sarunā." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функција на Enter копчето при пишување во разговор." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Товчилсон түлхүүрийн үүрэг нь харилцан ярианы үед." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fungsi kekunci enter semasa menaip dalam perbualan." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စကားပြောမှာ enter key ရဲ့လုပ်ဆောင်ချက်" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funksjon til enter-tasten når man skriver i en samtale." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funksjon av enter-tasten når du skriver i en samtale." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "समूह निमन्त्रणा सफल" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Functie van de entertoets bij het typen in een gesprek." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funksjon for enter-tasten ved skriving i samtale." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ntchito ya kiyi ya enter mukalemba mu kukambirana." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਗੱਲਬਾਤ ਵਿੱਚ ਲਿਖਣ ਵੇਲੇ ਐਂਟਰ ਕੀ ਦਾ ਫੰਕਸ਼ਨ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcja klawisza Enter podczas pisania wiadomości w konwersacji." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د خبرو اترو په دوران کې د انټر کیلي دنده." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Função da tecla Enter ao digitar em uma conversa." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Função da tecla Enter numa conversa." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funcția tastei Enter când scrii într-o conversație." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функция клавиши Enter при вводе текста в беседе." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcija tastera enter tokom tipkanja u razgovoru." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "සංවාදයේ, Enter යතුරේ ක්‍රියාකාරිත්වය." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcia klávesy Enter pri písaní v konverzácii." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcija tipke enter pri tipkanju v pogovoru." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funksioni i tastit enter kur shkrini mesazhe." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функција тастера ентер при куцању у разговору." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkcija tastera enter prilikom kucanja poruke." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funktionen av Enter-tangenten när du skriver i en konversation." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kazi ya kitufe cha kuingiza wakati wa kuandika katika mazungumzo." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உரையாடலில் தட்டச்சு செய்வதற்கான எந்திரக் கை நுட்பம்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సంభాషణలో టైపింగ్ చేస్తున్నప్పుడు ఎంటర్ కీ యొక్క విధానం." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ฟังก์ชันของปุ่ม Enter เมื่อพิมพ์ในบทสนทนา" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sohbette yazarken enter tuşunun fonksiyonu." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функція клавіші Enter під час набору повідомлення." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "گفتگو میں ٹائپ کرتے وقت انٹر کی کلید کا فنکشن" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suhbatda enter tugmasining vazifasi." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chức năng của phím enter khi nhập nội dung trong cuộc trò chuyện." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Umsebenzi weqhosha loku ngenisa xa uchwetheza kwincoko." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "在对话中输入回车键时的功能。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "在對話中鍵入時回車鍵的功能。" + "value" : "Define how the Enter and Shift+Enter keys function in conversations." } } } From d5a3eb8f2303975942031f25b33f932ef0443167 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 2 Sep 2025 08:45:33 +0800 Subject: [PATCH 146/244] Code clean ups --- .../Jobs/DisplayPictureDownloadJob.swift | 4 +-- .../RetrieveDefaultOpenGroupRoomsJob.swift | 4 +-- .../Open Groups/OpenGroupAPI.swift | 30 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index d379337d09..b6323ce744 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -58,8 +58,8 @@ public enum DisplayPictureDownloadJob: JobExecutor { fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), - using: dependencies, - skipAuthentication: skipAuthentication + skipAuthentication: skipAuthentication, + using: dependencies ) } } diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index 5ec27d9348..66252af6f5 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -72,8 +72,8 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in try OpenGroupAPI.preparedCapabilitiesAndRooms( authMethod: authMethod, - using: dependencies, - skipAuthentication: true + skipAuthentication: true, + using: dependencies ).send(using: dependencies) } .subscribe(on: scheduler, using: dependencies) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 506821f670..548a5ebd07 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -165,8 +165,8 @@ public enum OpenGroupAPI { private static func preparedSequence( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, - using dependencies: Dependencies, - skipAuthentication: Bool = false + skipAuthentication: Bool = false, + using dependencies: Dependencies ) throws -> Network.PreparedRequest> { let preparedRequest = try Network.PreparedRequest( request: Request( @@ -193,8 +193,8 @@ public enum OpenGroupAPI { /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` public static func preparedCapabilities( authMethod: AuthenticationMethod, - using dependencies: Dependencies, - skipAuthentication: Bool = false + skipAuthentication: Bool = false, + using dependencies: Dependencies ) throws -> Network.PreparedRequest { let preparedRequest = try Network.PreparedRequest( request: Request( @@ -215,8 +215,8 @@ public enum OpenGroupAPI { /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included public static func preparedRooms( authMethod: AuthenticationMethod, - using dependencies: Dependencies, - skipAuthentication: Bool = false + skipAuthentication: Bool = false, + using dependencies: Dependencies ) throws -> Network.PreparedRequest<[Room]> { let preparedRequest = try Network.PreparedRequest( request: Request( @@ -329,20 +329,20 @@ public enum OpenGroupAPI { /// methods for the documented behaviour of each method public static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, - using dependencies: Dependencies, - skipAuthentication: Bool = false + skipAuthentication: Bool = false, + using dependencies: Dependencies ) throws -> Network.PreparedRequest { let preparedRequest = try OpenGroupAPI .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies, skipAuthentication: skipAuthentication), - preparedRooms(authMethod: authMethod, using: dependencies, skipAuthentication: skipAuthentication) + preparedCapabilities(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies), + preparedRooms(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies) ], authMethod: authMethod, - using: dependencies, - skipAuthentication: skipAuthentication + skipAuthentication: skipAuthentication, + using: dependencies ) let finalRequest = skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) @@ -372,7 +372,7 @@ public enum OpenGroupAPI { capabilities: (info: capabilitiesInfo, data: capabilities), rooms: (info: roomsInfo, data: (roomsResponse.body ?? [])) ) - } + } } // MARK: - Messages @@ -847,8 +847,8 @@ public enum OpenGroupAPI { fileId: String, roomToken: String, authMethod: AuthenticationMethod, - using dependencies: Dependencies, - skipAuthentication: Bool = false + skipAuthentication: Bool = false, + using dependencies: Dependencies ) throws -> Network.PreparedRequest { let preparedRequest = try Network.PreparedRequest( request: Request( From 0543c7d5fbe610d4c449123b22b76ad466766800 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 2 Sep 2025 09:47:18 +0800 Subject: [PATCH 147/244] Updated unit tests to verify `skipAuthentication` flag --- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 3 + .../Open Groups/OpenGroupAPISpec.swift | 97 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index d4f58196be..7a046e4266 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -263,6 +263,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ), forceBlinded: false ), + skipAuthentication: true, using: dependencies ) } @@ -284,6 +285,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) + + expect(expectedRequest?.headers).to(beEmpty()) } // MARK: -- will retry 8 times before it fails diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index fa13eeea02..a4ffc9f523 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -770,6 +770,44 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).toNot(beEmpty()) + expect(preparedRequest?.headers).to(equal([ + HTTPHeader.sogsNonce: "pK6YRtQApl4NhECGizF0Cg==", + HTTPHeader.sogsTimestamp: "1234567890", + HTTPHeader.sogsSignature: "VGVzdFNvZ3NTaWduYXR1cmU=", + HTTPHeader.sogsPubKey: "1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" + ])) + } + + // MARK: ---- generates the request correctly and skips adding request headers + it("generates the request correctly and skips adding request headers") { + expect { + preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.batchEndpoints.count).to(equal(2)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + .to(equal(.capabilities)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + .to(equal(.rooms)) + + expect(preparedRequest?.path).to(equal("/sequence")) + expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).to(beEmpty()) } // MARK: ---- processes a valid response correctly @@ -1626,6 +1664,31 @@ class OpenGroupAPISpec: QuickSpec { ])) } + // MARK: ---- generates the download destination correctly when given an id and skips adding request headers + it("generates the download destination correctly when given an id and skips adding request headers") { + expect { + preparedRequest = try OpenGroupAPI.preparedDownload( + fileId: "1", + roomToken: "roomToken", + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.path).to(equal("/room/roomToken/file/1")) + expect(preparedRequest?.method.rawValue).to(equal("GET")) + expect(preparedRequest?.headers).to(beEmpty()) + } + // MARK: ---- generates the download request correctly when given a URL it("generates the download request correctly when given a URL") { expect { @@ -2274,6 +2337,40 @@ class OpenGroupAPISpec: QuickSpec { .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).toNot(beEmpty()) + + expect(response).toNot(beNil()) + expect(error).to(beNil()) + } + + // MARK: ---- triggers sending correctly without headers + it("triggers sending correctly without headers") { + var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? + + expect { + preparedRequest = try OpenGroupAPI.preparedRooms( + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + preparedRequest? + .send(using: dependencies) + .handleEvents(receiveOutput: { result in response = result }) + .mapError { error.setting(to: $0) } + .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).to(beEmpty()) expect(response).toNot(beNil()) expect(error).to(beNil()) From 66b22b2dfa39e7bb5106800757082e2d1116c301 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 1 Sep 2025 10:11:33 +1000 Subject: [PATCH 148/244] Increased version number --- Session.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 157c149838..3fa3a7d904 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8180,7 +8180,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 626; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8220,7 +8220,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.1; + MARKETING_VERSION = 2.14.2; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8261,7 +8261,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 626; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8296,7 +8296,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.1; + MARKETING_VERSION = 2.14.2; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8742,7 +8742,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 626; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8781,7 +8781,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.1; + MARKETING_VERSION = 2.14.2; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9329,7 +9329,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 626; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9362,7 +9362,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.1; + MARKETING_VERSION = 2.14.2; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", From 067d4dddafa83a9f9b7226632bdbf8bead4ad3b1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 1 Sep 2025 15:36:55 +1000 Subject: [PATCH 149/244] Fixed an issue where disappearing attachments wouldn't disappear --- .../ConversationVC+Interaction.swift | 2 +- ...isappearingMessagesSettingsViewModel.swift | 2 +- .../Database/Models/Interaction.swift | 20 +++++++++++++++++++ .../Jobs/CheckForAppUpdatesJob.swift | 4 ++-- .../LibSession+GroupInfo.swift | 2 +- .../MessageSender+Groups.swift | 2 +- .../MessageSender+Convenience.swift | 10 ++++++---- .../Database/Models/KeyValueStore.swift | 4 ++-- .../LibSession/Types/ObservingDatabase.swift | 12 ----------- 9 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2bb293a909..1330cba707 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1180,7 +1180,7 @@ extension ConversationVC: let currentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try messageDisappearingConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: cellViewModel.threadVariant, diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index b7df60ec3c..1f780be902 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -359,7 +359,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga let currentOffsetTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try updatedConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: threadVariant, diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index ab50549c0b..06ca54b819 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -406,6 +406,26 @@ public struct Interaction: Codable, Identifiable, Equatable, Hashable, Fetchable } } + public func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { + _ = try update() + + // Start the disappearing messages timer if needed + guard columns.contains(Columns.expiresStartedAtMs.name) else { return } + + switch ObservationContext.observingDb { + case .none: Log.error("[Interaction] Could not process 'aroundUpdate' due to missing observingDb.") + case .some(let observingDb): + observingDb.dependencies[singleton: .jobRunner].upsert( + observingDb, + job: DisappearingMessagesJob.updateNextRunIfNeeded( + observingDb, + using: observingDb.dependencies + ), + canStartJob: true + ) + } + } + public mutating func didInsert(_ inserted: InsertionSuccess) { self.id = inserted.rowID } diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index aba703204e..c3f4583f59 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -40,7 +40,7 @@ public enum CheckForAppUpdatesJob: JobExecutor { nextRunTimestamp: (dependencies.dateNow.timeIntervalSince1970 + updateCheckFrequency) ) dependencies[singleton: .storage].write { db in - try updatedJob.save(db) + try updatedJob.upsert(db) } Log.info(.cat, "Deferred due to test/simulator build.") @@ -59,7 +59,7 @@ public enum CheckForAppUpdatesJob: JobExecutor { ) dependencies[singleton: .storage].write { db in - try updatedJob.save(db) + try updatedJob.upsert(db) } success(updatedJob, false) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index ee77869c1e..b5b3f0d65e 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -174,7 +174,7 @@ internal extension LibSessionCacheType { if localConfig != updatedConfig { try updatedConfig - .saved(db) + .upserted(db) .clearUnrelatedControlMessages( db, threadVariant: .group, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 58caa31955..60e9665c93 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -479,7 +479,7 @@ extension MessageSender { /// Add a record of the change to the conversation _ = try updatedConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: .group, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index cb7a63ffc6..f39f39ee2f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -199,10 +199,10 @@ extension MessageSender { } else { // Otherwise we do want to try and update the referenced interaction - let interaction: Interaction? = try interaction(db, for: message, interactionId: interactionId) + let maybeInteraction: Interaction? = try interaction(db, for: message, interactionId: interactionId) // Get the visible message if possible - if let interaction: Interaction = interaction { + if var interaction: Interaction = maybeInteraction { // Only store the server hash of a sync message if the message is self send valid switch (message.isSelfSendValid, destination) { case (false, .syncMessage): @@ -218,7 +218,8 @@ extension MessageSender { return sentTimestampMs } - try interaction.with( + // Update the interaction so we have the correct `expiresStartedAtMs` value + interaction = interaction.with( serverHash: message.serverHash, // Track the open group server message ID and update server timestamp (use server // timestamp for open group messages otherwise the quote messages may not be able @@ -230,7 +231,8 @@ extension MessageSender { expiresStartedAtMs: scheduledTimestampForDeletion, // Updates the expiresStartedAtMs value when message is marked as sent openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }, state: .sent - ).update(db) + ) + try interaction.update(db) if interaction.isExpiringMessage { // Start disappearing messages job after a message is successfully sent. diff --git a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift index d7bac86112..539f2c15c2 100644 --- a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift +++ b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift @@ -150,7 +150,7 @@ public extension ObservingDatabase { return nil } - return try? KeyValueStore(key: key, value: value)?.saved(self) + return try? KeyValueStore(key: key, value: value)?.upserted(self) } private subscript(key: String) -> KeyValueStore? { @@ -161,7 +161,7 @@ public extension ObservingDatabase { return } - try? newValue.save(self) + try? newValue.upsert(self) } } diff --git a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift index 6a30c902fc..34a9bb0582 100644 --- a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift +++ b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift @@ -167,10 +167,6 @@ public extension PersistableRecord { func upsert(_ db: ObservingDatabase) throws { return try self.upsert(db.originalDb) } - - func save(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws { - try self.save(db.originalDb, onConflict: conflictResolution) - } } public extension SQLRequest { @@ -196,14 +192,6 @@ public extension MutablePersistableRecord { try self.update(db.originalDb, onConflict: conflictResolution) } - mutating func save(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws { - try self.save(db.originalDb, onConflict: conflictResolution) - } - - func saved(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws -> Self { - return try self.saved(db.originalDb, onConflict: conflictResolution) - } - @discardableResult func delete(_ db: ObservingDatabase) throws -> Bool { if LoggingDatabaseRecordContext.suppressLogs != true { From f15b5e614a5aaa3dea4c93f05557968be51b8ea6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 1 Sep 2025 17:01:47 +1000 Subject: [PATCH 150/244] Fixed a crash related to async/await that would occur on iOS 15 --- Session.xcodeproj/project.pbxproj | 8 +++ .../Dependency Injection/Dependencies.swift | 23 +++--- .../Types/CancellationAwareAsyncStream.swift | 72 +++++++++++++++++++ .../Types/CurrentValueAsyncStream.swift | 38 +++++----- .../Types/StreamLifecycleManager.swift | 61 ++++++++++++++++ _SharedTestUtilities/TestDependencies.swift | 2 +- 6 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift create mode 100644 SessionUtilitiesKit/Types/StreamLifecycleManager.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 3fa3a7d904..c3c86fd017 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -761,6 +761,8 @@ FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; + FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */; }; + FD6F5B602E657A33009A8D01 /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; @@ -2056,6 +2058,8 @@ FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; + FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; + FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _026_MessageDeduplicationTable.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; @@ -4186,10 +4190,12 @@ FDE755042C9BB4ED002A2623 /* Bencode.swift */, FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */, FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */, + FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */, FDB11A5A2DD1900B00BEF49F /* CurrentValueAsyncStream.swift */, FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */, FD6A38F02C2A66B100762359 /* KeychainStorage.swift */, FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */, + FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */, FD2272E92C351CA7004D8A6C /* Threading.swift */, FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */, ); @@ -6434,11 +6440,13 @@ FD74434A2D07CA9F00862443 /* Codable+Utilities.swift in Sources */, FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */, FD74434B2D07CA9F00862443 /* CGFloat+Utilities.swift in Sources */, + FD6F5B602E657A33009A8D01 /* StreamLifecycleManager.swift in Sources */, FD52CB632E13B61700A4DA70 /* ObservableKey.swift in Sources */, FD74434C2D07CA9F00862443 /* CGSize+Utilities.swift in Sources */, FD74434D2D07CA9F00862443 /* CGPoint+Utilities.swift in Sources */, FD74434E2D07CA9F00862443 /* CGRect+Utilities.swift in Sources */, FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */, + FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, FDE7551C2C9BC169002A2623 /* UIBezierPath+Utilities.swift in Sources */, diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index b60081b0c8..4c6578106a 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -14,9 +14,8 @@ public class Dependencies { @ThreadSafeObject private var storage: DependencyStorage = DependencyStorage() private typealias DependencyChange = (Dependencies.DependencyStorage.Key, DependencyStorage.Value?) - private let dependecyChangeStream: AsyncStream - private let dependecyChangeContinuation: AsyncStream.Continuation - + private let dependencyChangeStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() + // MARK: - Subscript Access public subscript(singleton singleton: SingletonConfig) -> S { getOrCreate(singleton) } @@ -40,14 +39,7 @@ public class Dependencies { // MARK: - Initialization - public static func createEmpty() -> Dependencies { return Dependencies(forTesting: false) } - - /// This constructor should not be used directly (except for `TestDependencies`), use `Dependencies.createEmpty()` instead - internal init(forTesting: Bool) { - let (stream, continuation) = AsyncStream.makeStream(of: DependencyChange.self) - dependecyChangeStream = stream - dependecyChangeContinuation = continuation - } + public static func createEmpty() -> Dependencies { return Dependencies() } // MARK: - Functions @@ -150,7 +142,7 @@ public class Dependencies { /// If we already have an instance (which isn't a `NoopDependency`) then no need to observe the stream guard !_storage.performMap({ $0.instances[targetKey]?.isNoop == false }) else { return } - for await (key, instance) in dependecyChangeStream { + for await (key, instance) in dependencyChangeStream.stream { /// If the target instance has been set (and isn't a `NoopDependency`) then we can stop waiting (observing the stream) if key == targetKey && instance?.isNoop == false { break @@ -227,7 +219,8 @@ public extension Dependencies { removeValue(feature.identifier, of: .feature) /// Notify observers - dependecyChangeContinuation.yield((key, nil)) + + Task { await dependencyChangeStream.send((key, nil)) } notifyAsync(events: [ ObservedEvent(key: .feature(feature), value: nil), ObservedEvent(key: .featureGroup(feature), value: nil) @@ -396,7 +389,7 @@ private extension Dependencies { Log.warn("Setting noop dependency for \(key)") } - dependecyChangeContinuation.yield((finalKey, typedStorage)) + Task { await dependencyChangeStream.send((finalKey, typedStorage)) } return result } @@ -408,7 +401,7 @@ private extension Dependencies { return storage } - dependecyChangeContinuation.yield((finalKey, nil)) + Task { await dependencyChangeStream.send((finalKey, nil)) } } } diff --git a/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift new file mode 100644 index 0000000000..42a56bace5 --- /dev/null +++ b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift @@ -0,0 +1,72 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +// MARK: - CancellationAwareAsyncStream + +public actor CancellationAwareAsyncStream: CancellationAwareStreamType { + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + // MARK: - Initialization + + public init() {} + + // MARK: - Functions + + public func send(_ newValue: Element) async { + lifecycleManager.send(newValue) + } + + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() + } + + public func beforeYield(to continuation: AsyncStream.Continuation) async { + // No-op - no initial value + } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream + } +} + +// MARK: - CancellationAwareStreamType + +public protocol CancellationAwareStreamType: Actor { + associatedtype Element: Sendable + + func send(_ newValue: Element) async + func finishCurrentStreams() async + + /// This function gets called when a stream is initially created but before the inner stream is created, it shouldn't be called directly + func beforeYield(to continuation: AsyncStream.Continuation) async + + /// This is an internal function which shouldn't be called directly + func makeTrackedStream() async -> AsyncStream +} + +public extension CancellationAwareStreamType { + /// Every time `stream` is accessed it will create a **new** stream + /// + /// **Note:** This is non-isolated so it can be exposed via protocols without `async`, this is safe because `AsyncStream` is + /// thread-safe internally and `Element` is `Sendable` so it's verified to be safe to send concurrently + nonisolated var stream: AsyncStream { + AsyncStream { continuation in + let bridgingTask = Task { + await self.beforeYield(to: continuation) + + let internalStream: AsyncStream = await self.makeTrackedStream() + + for await element in internalStream { + continuation.yield(element) + } + + continuation.finish() + } + + continuation.onTermination = { @Sendable _ in + bridgingTask.cancel() + } + } + } +} diff --git a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift index 26c6b9245e..b2f9f13e1a 100644 --- a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift +++ b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift @@ -2,34 +2,34 @@ import Foundation -public actor CurrentValueAsyncStream { - private var _currentValue: Element - private let continuation: AsyncStream.Continuation - public let stream: AsyncStream - - public var currentValue: Element { _currentValue } +public actor CurrentValueAsyncStream: CancellationAwareStreamType { + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + /// This is the most recently emitted value + public private(set) var currentValue: Element // MARK: - Initialization public init(_ initialValue: Element) { - self._currentValue = initialValue - - /// We use `.bufferingNewest(1)` to ensure that the stream always holds the most recent value. When a new iterator is - /// created for the stream, it will receive this buffered value first. - let (stream, continuation) = AsyncStream.makeStream(of: Element.self, bufferingPolicy: .bufferingNewest(1)) - self.stream = stream - self.continuation = continuation - self.continuation.yield(initialValue) + self.currentValue = initialValue } // MARK: - Functions - public func send(_ newValue: Element) { - _currentValue = newValue - continuation.yield(newValue) + public func send(_ newValue: Element) async { + currentValue = newValue + lifecycleManager.send(newValue) } - public func finish() { - continuation.finish() + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() + } + + public func beforeYield(to continuation: AsyncStream.Continuation) async { + continuation.yield(currentValue) + } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream } } diff --git a/SessionUtilitiesKit/Types/StreamLifecycleManager.swift b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift new file mode 100644 index 0000000000..444e431525 --- /dev/null +++ b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift @@ -0,0 +1,61 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public final class StreamLifecycleManager: @unchecked Sendable { + private let lock: NSLock = NSLock() + private var continuations: [UUID: AsyncStream.Continuation] = [:] + + // MARK: - Initialization + + public init() {} + + deinit { + finishCurrentStreams() + } + + // MARK: - Functions + + func makeTrackedStream() -> (stream: AsyncStream, id: UUID) { + let (stream, continuation) = AsyncStream.makeStream(of: Element.self) + let id: UUID = UUID() + + lock.withLock { continuations[id] = continuation } + + continuation.onTermination = { @Sendable [self] _ in + self.finishStream(id: id) + } + + return (stream, id) + } + + func send(_ value: Element) { + /// Capture current continuations before sending to avoid deadlocks where yielding could result in a new continuation being + /// added while the lock is held + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { continuations } + + for continuation in currentContinuations.values { + continuation.yield(value) + } + } + + func finishStream(id: UUID) { + lock.withLock { + if let continuation: AsyncStream.Continuation = continuations.removeValue(forKey: id) { + continuation.finish() + } + } + } + + func finishCurrentStreams() { + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { + let continuationsToFinish: [UUID: AsyncStream.Continuation] = continuations + continuations.removeAll() + return continuationsToFinish + } + + for continuation in currentContinuations.values { + continuation.finish() + } + } +} diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 3ac46b51b4..0815ea21df 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -105,7 +105,7 @@ public class TestDependencies: Dependencies { // MARK: - Initialization public init(initialState: ((TestDependencies) -> ())? = nil) { - super.init(forTesting: true) + super.init() initialState?(self) } From f20956cb23c7239da79a53095a54988c1d987b7e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 2 Sep 2025 09:47:48 +1000 Subject: [PATCH 151/244] Fixed a bug where sending an UnsendRequest wouldn't update the home screen on a linked device --- .../Message Handling/MessageReceiver+UnsendRequests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index ae75d068a5..bbb89f2e66 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -52,7 +52,7 @@ extension MessageReceiver { ) try Interaction.markAsDeleted( db, - threadId: threadId, + threadId: interactionInfo.threadId, /// Can't use `threadId` as that may be the current users threadVariant: threadVariant, interactionIds: [interactionInfo.id], options: [.local, .network], From ff7b45bf8d083cb665551c045d90f165404f71da Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 2 Sep 2025 12:21:58 +1000 Subject: [PATCH 152/244] Tweaks to CI scripts to try to resolve false failures --- .drone.jsonnet | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 04afc476a2..d24f88633f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -80,7 +80,7 @@ local clean_up_old_test_sims_on_commit_trigger = { 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', 'echo "If tests fail inconsistently from local builds this is likely the difference"', 'echo ""', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', + 'NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', ], depends_on: [ 'Reset SPM Cache if Needed', @@ -100,12 +100,24 @@ local clean_up_old_test_sims_on_commit_trigger = { }, }, { - name: 'Unit Test Summary', + name: 'Check for Build/Test Failures', commands: [ + 'echo "Checking for build errors or test failures in xcresult bundle..."', 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' ], depends_on: ['Build and Run Tests'] }, + { + name: 'Log Failed Test Summary', + commands: [ + 'echo "--- FAILED TESTS ---"', + 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' + ], + depends_on: ['Check for Build/Test Failures'], + when: { + status: ['failure'], // Only run this on failure + }, + }, { name: 'Convert xcresult to xml', commands: [ @@ -146,7 +158,7 @@ local clean_up_old_test_sims_on_commit_trigger = { name: 'Build', commands: [ 'mkdir build', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild archive -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App_Store_Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', + 'NSUnbufferedIO=YES && xcodebuild archive -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App_Store_Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', ], depends_on: [ 'Reset SPM Cache if Needed', From b8023b67978d3b1b29ceb5e211a23cd4db9b3ad5 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 2 Sep 2025 15:17:51 +0800 Subject: [PATCH 153/244] Update message request delete action --- Session/Utilities/UIContextualAction+Utilities.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 5b5ce426a4..7e30326c7e 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -649,7 +649,7 @@ public extension UIContextualAction { guard !isMessageRequest else { switch threadViewModel.threadVariant { case .group: return ThemedAttributedString(string: "groupInviteDelete".localized()) - default: return ThemedAttributedString(string: "messageRequestsDelete".localized()) + default: return ThemedAttributedString(string: "messageRequestsContactDelete".localized()) } } @@ -692,7 +692,7 @@ public extension UIContextualAction { return .deleteGroupAndContent case (.group, _, _): return .leaveGroupAsync - case (.contact, _, _): return .deleteContactConversationAndMarkHidden + case (.contact, _, _): return .deleteContactConversationAndContact } }() From a953e20b46f74b26b153a460f4eca2a574383d1e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 14:35:33 +1000 Subject: [PATCH 154/244] Removed "Delete for Everyone" option when deleting pending messages --- .../MessageViewModel+DeletionActions.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 9cbad6bf0b..8f19598ecb 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -124,7 +124,7 @@ public extension MessageViewModel.DeletionBehaviours { enum SelectedMessageState { case outgoingOnly case containsIncoming - case containsDeletedOrControlMessages + case containsLocalOnlyMessages /// Control, pending or deleted messages } /// If it's a legacy group and they have been deprecated then the user shouldn't be able to delete messages @@ -134,8 +134,9 @@ public extension MessageViewModel.DeletionBehaviours { let state: SelectedMessageState = { guard !cellViewModels.contains(where: { $0.variant.isDeletedMessage }) && - !cellViewModels.contains(where: { $0.variant.isInfoMessage }) - else { return .containsDeletedOrControlMessages } + !cellViewModels.contains(where: { $0.variant.isInfoMessage }) && + !cellViewModels.contains(where: { $0.state == .sending }) + else { return .containsLocalOnlyMessages } return (cellViewModels.contains(where: { $0.variant == .standardIncoming }) ? .containsIncoming : @@ -171,8 +172,8 @@ public extension MessageViewModel.DeletionBehaviours { }() switch (state, isAdmin) { - /// User selects messages including a control message or “deleted” message - case (.containsDeletedOrControlMessages, _): + /// User selects messages including a control, pending or “deleted” message + case (.containsLocalOnlyMessages, _): return MessageViewModel.DeletionBehaviours( title: "deleteMessage" .putNumber(cellViewModels.count) From 3f3d8e84fb30247d1eb878d2dd8164cfea5086e6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 15:29:32 +1000 Subject: [PATCH 155/244] Added failed state as well --- .../Shared Models/MessageViewModel+DeletionActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 8f19598ecb..d26763bbfa 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -135,7 +135,7 @@ public extension MessageViewModel.DeletionBehaviours { guard !cellViewModels.contains(where: { $0.variant.isDeletedMessage }) && !cellViewModels.contains(where: { $0.variant.isInfoMessage }) && - !cellViewModels.contains(where: { $0.state == .sending }) + !cellViewModels.contains(where: { $0.state == .sending || $0.state == .failed }) else { return .containsLocalOnlyMessages } return (cellViewModels.contains(where: { $0.variant == .standardIncoming }) ? From 6a5bb2273c77051f8999169d7885e408c65536f4 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 3 Sep 2025 14:23:54 +0800 Subject: [PATCH 156/244] Fix overly large file placeholder preview --- .../MediaMessageView.swift | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 1e732e5f1b..4feb892afb 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -406,12 +406,7 @@ public class MediaMessageView: UIView { ) : stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) ), - - imageView.widthAnchor.constraint( - equalTo: imageView.heightAnchor, - multiplier: clampedRatio - ), - + (maybeImageSize != nil ? imageView.widthAnchor.constraint(equalToConstant: imageSize) : imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) @@ -426,11 +421,9 @@ public class MediaMessageView: UIView { equalTo: imageView.centerYAnchor, constant: ceil(imageSize * 0.15) ), - fileTypeImageView.widthAnchor.constraint( - equalTo: fileTypeImageView.heightAnchor, - multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) - ), - fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5), + + fileTypeImageView.widthAnchor.constraint(equalToConstant: imageSize * 0.5), + fileTypeImageView.heightAnchor.constraint(equalToConstant: imageSize * 0.5), loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), @@ -438,6 +431,16 @@ public class MediaMessageView: UIView { loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3)) ]) + if imageView.image?.size == nil { + // Handle `clampedRatio` ratio when image is from data + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ) + ]) + } + // No inset for the text for URLs but there is for all other layouts if !attachment.isUrl { NSLayoutConstraint.activate([ From 684a30bf7c07e1cca3e7738d011b68eed249bd5b Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 4 Sep 2025 10:33:54 +0800 Subject: [PATCH 157/244] Updated block contacts settings button design to be consistent --- .../Settings/ConversationSettingsViewModel.swift | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index 0419aa80fc..1bfaa1e129 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -39,16 +39,11 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold switch self { case .messageTrimming: return "conversationsMessageTrimming".localized() case .audioMessages: return "conversationsAudioMessages".localized() - case .blockedContacts: return nil + case .blockedContacts: return "conversationsBlockedContacts".localized() } } - var style: SessionTableSectionStyle { - switch self { - case .blockedContacts: return .padding - default: return .titleRoundedContent - } - } + var style: SessionTableSectionStyle { .titleRoundedContent } } // MARK: - Content @@ -192,10 +187,8 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold SessionCell.Info( id: .blockedContacts, title: "conversationsBlockedContacts".localized(), - styling: SessionCell.StyleInfo( - tintColor: .danger, - backgroundStyle: .noBackground - ), + subtitle: "blockedContactsManageDescription".localized(), + trailingAccessory: .icon(.chevronRight), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( SessionTableViewController(viewModel: BlockedContactsViewModel(using: dependencies)) From 0f5f091d9fd3ac35b96813dc645d71115f42a336 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 12:46:03 +1000 Subject: [PATCH 158/244] Fix primary colour reset, main actor specifications --- .../Content Views/MediaView.swift | 2 +- Session/Settings/AppearanceViewModel.swift | 2 ++ .../Utilities/ImageLoading+Convenience.swift | 14 ++++++++------ .../Components/SessionImageView.swift | 2 +- SessionUIKit/Style Guide/ThemeManager.swift | 19 ++++++++++--------- SessionUIKit/Types/ImageDataManager.swift | 11 +++++++---- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 01a44703a9..09e9b1e7b0 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -198,7 +198,7 @@ public class MediaView: UIView { guard processedData == nil else { return } Log.error("[MediaView] Could not load thumbnail") - Task { @MainActor [weak self] in self?.configure(forError: .invalid) } + self?.configure(forError: .invalid) } } diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index 6f93c3f507..20c5762267 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -223,6 +223,8 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ ), onTap: { ThemeManager.updateThemeState( + theme: state.theme, /// Keep the current value + primaryColor: state.primaryColor, /// Keep the current value matchSystemNightModeSetting: !state.autoDarkModeEnabled ) } diff --git a/Session/Utilities/ImageLoading+Convenience.swift b/Session/Utilities/ImageLoading+Convenience.swift index 54a69d2524..1b1ef57fd3 100644 --- a/Session/Utilities/ImageLoading+Convenience.swift +++ b/Session/Utilities/ImageLoading+Convenience.swift @@ -69,10 +69,11 @@ public extension ImageDataManager.DataSource { // MARK: - ImageDataManagerType Convenience public extension ImageDataManagerType { + @MainActor func loadImage( attachment: Attachment, using dependencies: Dependencies, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( attachment: attachment, @@ -82,11 +83,12 @@ public extension ImageDataManagerType { load(source, onComplete: onComplete) } + @MainActor func loadThumbnail( size: ImageDataManager.ThumbnailSize, attachment: Attachment, using dependencies: Dependencies, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.thumbnailFrom( attachment: attachment, @@ -102,7 +104,7 @@ public extension ImageDataManagerType { public extension SessionImageView { @MainActor - func loadImage(from path: String, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { + func loadImage(from path: String, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.url(URL(fileURLWithPath: path)), onComplete: onComplete) } @@ -110,7 +112,7 @@ public extension SessionImageView { func loadImage( attachment: Attachment, using dependencies: Dependencies, - onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil + onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( attachment: attachment, @@ -128,7 +130,7 @@ public extension SessionImageView { size: ImageDataManager.ThumbnailSize, attachment: Attachment, using dependencies: Dependencies, - onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil + onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.thumbnailFrom( attachment: attachment, @@ -143,7 +145,7 @@ public extension SessionImageView { } @MainActor - func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { + func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.placeholderIcon(seed: seed, text: text, size: size), onComplete: onComplete) } } diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index 0eec4aab2b..a768725213 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -138,7 +138,7 @@ public class SessionImageView: UIImageView { } @MainActor - public func loadImage(_ source: ImageDataManager.DataSource, onComplete: ((ImageDataManager.ProcessedImageData?) -> Void)? = nil) { + public func loadImage(_ source: ImageDataManager.DataSource, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 1ceae3a4dc..7065d19d0f 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -53,10 +53,6 @@ public enum ThemeManager { _primaryColor = targetPrimaryColor _hasLoadedTheme = true - if !hasSetInitialSystemTrait || themeChanged { - updateAllUI() - } - if matchSystemChanged { _matchSystemNightModeSetting = targetMatchSystemNightModeSetting @@ -65,9 +61,14 @@ public enum ThemeManager { SNUIKit.mainWindow?.overrideUserInterfaceStyle = .unspecified } - // If the theme was changed then trigger the callback for the theme settings change (so it gets persisted) + // If the theme was changed then trigger a UI update and the callback for the theme settings + // change (so it gets persisted) guard themeChanged || matchSystemChanged else { return } + if !hasSetInitialSystemTrait || themeChanged { + updateAllUI() + } + SNUIKit.themeSettingsChanged(targetTheme, targetPrimaryColor, targetMatchSystemNightModeSetting) } @@ -82,10 +83,10 @@ public enum ThemeManager { // Swap to the appropriate light/dark mode switch (currentUserInterfaceStyle, ThemeManager.currentTheme) { - case (.light, .classicDark): updateThemeState(theme: .classicLight) - case (.light, .oceanDark): updateThemeState(theme: .oceanLight) - case (.dark, .classicLight): updateThemeState(theme: .classicDark) - case (.dark, .oceanLight): updateThemeState(theme: .oceanDark) + case (.light, .classicDark): updateThemeState(theme: .classicLight, primaryColor: _primaryColor) + case (.light, .oceanDark): updateThemeState(theme: .oceanLight, primaryColor: _primaryColor) + case (.dark, .classicLight): updateThemeState(theme: .classicDark, primaryColor: _primaryColor) + case (.dark, .oceanLight): updateThemeState(theme: .oceanDark, primaryColor: _primaryColor) default: break } } diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 4cfaa8083f..9e0dd29674 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -59,9 +59,10 @@ public actor ImageDataManager: ImageDataManagerType { return processedData } - nonisolated public func load( + @MainActor + public func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) { Task { [weak self] in let result: ImageDataManager.ProcessedImageData? = await self?.load(source) @@ -863,9 +864,11 @@ public extension ImageDataManager { public protocol ImageDataManagerType { @discardableResult func load(_ source: ImageDataManager.DataSource) async -> ImageDataManager.ProcessedImageData? - nonisolated func load( + + @MainActor + func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) func cachedImage(identifier: String) async -> ImageDataManager.ProcessedImageData? From 5e57af93573c91ea49233c0c719ff54345eb3ace Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 4 Sep 2025 11:24:54 +0800 Subject: [PATCH 159/244] Fix trailing accessory views large padding --- Session/Settings/AppearanceViewModel.swift | 6 +++++- .../Settings/ConversationSettingsViewModel.swift | 6 +++++- Session/Settings/HelpViewModel.swift | 16 ++++++++++++---- Session/Shared/Types/SessionCell+Accessory.swift | 14 ++++++++++++-- .../Shared/Views/SessionCell+AccessoryView.swift | 7 +++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index 6f93c3f507..c88b392681 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -238,7 +238,11 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ "appIconSelect".localized(), font: .titleRegular ), - trailingAccessory: .icon(.chevronRight), + trailingAccessory: .icon( + .chevronRight, + shouldFill: true , + shouldFollowIconSize: true + ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( SessionTableViewController( diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index 1bfaa1e129..68214b2380 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -188,7 +188,11 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold id: .blockedContacts, title: "conversationsBlockedContacts".localized(), subtitle: "blockedContactsManageDescription".localized(), - trailingAccessory: .icon(.chevronRight), + trailingAccessory: .icon( + .chevronRight, + shouldFill: true , + shouldFollowIconSize: true + ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( SessionTableViewController(viewModel: BlockedContactsViewModel(using: dependencies)) diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index ba9d899f8c..26e38683da 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -76,7 +76,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + shouldFill: true, + shouldFollowIconSize: true ), onTap: { guard let url: URL = URL(string: "https://getsession.org/translate") else { @@ -97,7 +99,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + shouldFill: true, + shouldFollowIconSize: true ), onTap: { guard let url: URL = URL(string: "https://getsession.org/survey") else { @@ -118,7 +122,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + shouldFill: true, + shouldFollowIconSize: true ), onTap: { guard let url: URL = URL(string: "https://getsession.org/faq") else { @@ -139,7 +145,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + shouldFill: true, + shouldFollowIconSize: true ), onTap: { guard let url: URL = URL(string: "https://sessionapp.zendesk.com/hc/en-us") else { diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index ed8d6b0a97..4ebb6e17e3 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -40,7 +40,8 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, - accessibility: Accessibility? = nil + shouldFollowIconSize: Bool = false, + accessibility: Accessibility? = nil, ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( icon: icon, @@ -48,6 +49,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, + shouldFollowIconSize: shouldFollowIconSize, accessibility: accessibility ) } @@ -57,7 +59,8 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, - accessibility: Accessibility? = nil + shouldFollowIconSize: Bool = false, + accessibility: Accessibility? = nil, ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( icon: nil, @@ -65,6 +68,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, + shouldFollowIconSize: shouldFollowIconSize, accessibility: accessibility ) } @@ -226,6 +230,7 @@ public extension SessionCell.AccessoryConfig { public let iconSize: IconSize public let customTint: ThemeValue? public let shouldFill: Bool + public let shouldFollowIconSize: Bool fileprivate init( icon: Lucide.Icon?, @@ -233,6 +238,7 @@ public extension SessionCell.AccessoryConfig { iconSize: IconSize, customTint: ThemeValue?, shouldFill: Bool, + shouldFollowIconSize: Bool = false, accessibility: Accessibility? ) { self.icon = icon @@ -240,6 +246,7 @@ public extension SessionCell.AccessoryConfig { self.iconSize = iconSize self.customTint = customTint self.shouldFill = shouldFill + self.shouldFollowIconSize = shouldFollowIconSize super.init(accessibility: accessibility) } @@ -252,6 +259,7 @@ public extension SessionCell.AccessoryConfig { iconSize.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + shouldFollowIconSize.hash(into: &hasher) accessibility.hash(into: &hasher) } @@ -264,7 +272,9 @@ public extension SessionCell.AccessoryConfig { iconSize == rhs.iconSize && customTint == rhs.customTint && shouldFill == rhs.shouldFill && + shouldFollowIconSize == rhs.shouldFollowIconSize && accessibility == rhs.accessibility + ) } } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 8d7c9ee551..a93a8c41f3 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -307,6 +307,13 @@ extension SessionCell { imageView.themeTintColor = (accessory.customTint ?? tintColor) imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit) + // Use icon size when displaying accessory view. + // 50 width causes large padding not aligning accessory to right + if accessory.shouldFollowIconSize { + fixedWidthConstraint.constant = accessory.iconSize.size + fixedWidthConstraint.isActive = true + } + switch (accessory.icon, accessory.image) { case (.some(let icon), _): imageView.image = Lucide From 9a6904f1062cfcea58d2b97eccdfdb48caacb8d0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 13:57:44 +1000 Subject: [PATCH 160/244] Tweaks to try to improve CI output --- .drone.jsonnet | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index d24f88633f..44833d72a4 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,10 +77,23 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ + 'echo "--- Running Build and Tests ---"', 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', 'echo "If tests fail inconsistently from local builds this is likely the difference"', 'echo ""', - 'NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', + 'xcodebuild_output=$(mktemp)', + 'xcodebuild_exit_code=0', + 'NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee "$xcodebuild_output" | xcbeautify --is-ci || xcodebuild_exit_code=${PIPESTATUS[0]}', + 'echo ""', + 'echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---"', + 'echo ""', + 'if [ $xcodebuild_exit_code -ne 0 ]; then', + ' echo "🔴 Build failed. See log above for compile errors."', + ' exit $xcodebuild_exit_code', + 'fi', + 'echo ""', + 'echo "✅ Build Succeeded. Verifying test results..."', + 'xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult', ], depends_on: [ 'Reset SPM Cache if Needed', @@ -99,31 +112,26 @@ local clean_up_old_test_sims_on_commit_trigger = { status: ['success', 'failure'], }, }, - { - name: 'Check for Build/Test Failures', - commands: [ - 'echo "Checking for build errors or test failures in xcresult bundle..."', - 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' - ], - depends_on: ['Build and Run Tests'] - }, { name: 'Log Failed Test Summary', commands: [ 'echo "--- FAILED TESTS ---"', - 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' + 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult', ], - depends_on: ['Check for Build/Test Failures'], + depends_on: ['Build and Run Tests'], when: { status: ['failure'], // Only run this on failure }, }, { - name: 'Convert xcresult to xml', + name: 'Generate Code Coverage Report', commands: [ 'xcresultparser --output-format cobertura ./build/artifacts/testResults.xcresult > ./build/artifacts/coverage.xml', ], depends_on: ['Build and Run Tests'], + when: { + status: ['success'], + }, }, ], }, From 27d691863f81415f41b432d13fbb783f2684a618 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 14:03:11 +1000 Subject: [PATCH 161/244] Another CI config tweak attempt --- .drone.jsonnet | 53 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 44833d72a4..da03b07860 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,23 +77,42 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ - 'echo "--- Running Build and Tests ---"', - 'echo "Explicitly running unit tests on \'App_Store_Release\' configuration to ensure optimisation behaviour is consistent"', - 'echo "If tests fail inconsistently from local builds this is likely the difference"', - 'echo ""', - 'xcodebuild_output=$(mktemp)', - 'xcodebuild_exit_code=0', - 'NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee "$xcodebuild_output" | xcbeautify --is-ci || xcodebuild_exit_code=${PIPESTATUS[0]}', - 'echo ""', - 'echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---"', - 'echo ""', - 'if [ $xcodebuild_exit_code -ne 0 ]; then', - ' echo "🔴 Build failed. See log above for compile errors."', - ' exit $xcodebuild_exit_code', - 'fi', - 'echo ""', - 'echo "✅ Build Succeeded. Verifying test results..."', - 'xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult', + ''' + bash -c ' + # set -e makes the script exit immediately if a command fails. + # set -o pipefail ensures that a pipeline fails if any command in it fails. + set -eo pipefail + + echo "--- Running Build and Tests ---" + echo "Explicitly running unit tests on \\'App_Store_Release\\' configuration..." + echo "" + + xcodebuild_output=$(mktemp) + xcodebuild_exit_code=0 + + # Run the command and capture the true exit code of xcodebuild, even if xcbeautify succeeds. + # We add a `|| true` at the end because `set -e` would otherwise exit the script here + # if the pipeline fails, and we want to handle the exit code manually. + (NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee "$xcodebuild_output" | xcbeautify --is-ci) || xcodebuild_exit_code=${PIPESTATUS[0]} + + echo "" + echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" + echo "" + + # Check for a build failure (e.g., compile error) + if [ "$xcodebuild_exit_code" -ne 0 ]; then + echo "🔴 Build failed. See log above for compile errors." + exit "$xcodebuild_exit_code" + fi + + echo "" + echo "✅ Build Succeeded. Verifying test results..." + + # If the build succeeded, check the xcresult for test failures. + # The exit code of this command will determine the final status of this step. + xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult + ' + ''' ], depends_on: [ 'Reset SPM Cache if Needed', From bace35a5321d7df06958e7040ee30e074405d41c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 4 Sep 2025 12:07:20 +0800 Subject: [PATCH 162/244] Align dark mode toggle setting design with other platforms --- Session/Settings/AppearanceViewModel.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index 6f93c3f507..775a97abc0 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -40,7 +40,7 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ case .themes: return "appearanceThemes".localized() case .primaryColor: return "appearancePrimaryColor".localized() case .primaryColorSelection: return nil - case .autoDarkMode: return "appearanceAutoDarkMode".localized() + case .autoDarkMode: return "darkMode".localized() case .appIcon: return "appIcon".localized() } } @@ -213,10 +213,8 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ elements: [ SessionCell.Info( id: .darkModeMatchSystemSettings, - title: SessionCell.TextInfo( - "followSystemSettings".localized(), - font: .titleRegular - ), + title: "appearanceAutoDarkMode".localized(), + subtitle: "followSystemSettings".localized(), trailingAccessory: .toggle( state.autoDarkModeEnabled, oldValue: previousState.autoDarkModeEnabled From e1af97d3a9f28b37da11d8d52174cf60be670ef8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 14:11:02 +1000 Subject: [PATCH 163/244] Further tweaks --- .drone.jsonnet | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index da03b07860..d38005a180 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -79,21 +79,16 @@ local clean_up_old_test_sims_on_commit_trigger = { commands: [ ''' bash -c ' - # set -e makes the script exit immediately if a command fails. - # set -o pipefail ensures that a pipeline fails if any command in it fails. - set -eo pipefail - + set -o pipefail + echo "--- Running Build and Tests ---" echo "Explicitly running unit tests on \\'App_Store_Release\\' configuration..." - echo "" - - xcodebuild_output=$(mktemp) + + # This variable will hold the true exit code of xcodebuild xcodebuild_exit_code=0 - # Run the command and capture the true exit code of xcodebuild, even if xcbeautify succeeds. - # We add a `|| true` at the end because `set -e` would otherwise exit the script here - # if the pipeline fails, and we want to handle the exit code manually. - (NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee "$xcodebuild_output" | xcbeautify --is-ci) || xcodebuild_exit_code=${PIPESTATUS[0]} + # Build and test + (NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee /tmp/xcodebuild_raw.log | xcbeautify --is-ci) || xcodebuild_exit_code=${PIPESTATUS[0]} echo "" echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" From f09e8a07173683a5a24b46d98d6728d82bd7b05c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 14:28:20 +1000 Subject: [PATCH 164/244] More tweaks --- .drone.jsonnet | 35 ++---------------------- Scripts/build_ci.sh | 66 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 33 deletions(-) create mode 100755 Scripts/build_ci.sh diff --git a/.drone.jsonnet b/.drone.jsonnet index d38005a180..e921c8c5ee 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,37 +77,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ - ''' - bash -c ' - set -o pipefail - - echo "--- Running Build and Tests ---" - echo "Explicitly running unit tests on \\'App_Store_Release\\' configuration..." - - # This variable will hold the true exit code of xcodebuild - xcodebuild_exit_code=0 - - # Build and test - (NSUnbufferedIO=YES xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | tee /tmp/xcodebuild_raw.log | xcbeautify --is-ci) || xcodebuild_exit_code=${PIPESTATUS[0]} - - echo "" - echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" - echo "" - - # Check for a build failure (e.g., compile error) - if [ "$xcodebuild_exit_code" -ne 0 ]; then - echo "🔴 Build failed. See log above for compile errors." - exit "$xcodebuild_exit_code" - fi - - echo "" - echo "✅ Build Succeeded. Verifying test results..." - - # If the build succeeded, check the xcresult for test failures. - # The exit code of this command will determine the final status of this step. - xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult - ' - ''' + './Scripts/run_xcode_ci.sh test -resultBundlePath ./build/artifacts/testResults.xcresult -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES', ], depends_on: [ 'Reset SPM Cache if Needed', @@ -179,8 +149,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build', commands: [ - 'mkdir build', - 'NSUnbufferedIO=YES && xcodebuild archive -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App_Store_Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', + './Scripts/run_xcode_ci.sh archive -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator"', ], depends_on: [ 'Reset SPM Cache if Needed', diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh new file mode 100755 index 0000000000..dd9a410b7a --- /dev/null +++ b/Scripts/build_ci.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Error: Missing mode. Usage: $0 [test|archive] [unique_xcodebuild_args...]" + exit 1 +fi + +MODE="$1" +shift + +COMMON_ARGS=( + -project Session.xcodeproj + -scheme Session + -derivedDataPath ./build/derivedData + -parallelizeTargets + -configuration "App_Store_Release" +) + +UNIQUE_ARGS=("$@") + +if [[ "$MODE" == "test" ]]; then + + echo "--- Running Build and Unit Tests (App_Store_Release) ---" + + mkdir build + xcodebuild_exit_code=0 + + # We wrap the pipeline in parentheses to capture the exit code of xcodebuild + # which is at PIPESTATUS[0]. We do not use tee to a file here, as the complexity + # of reading back the UUID is not necessary if we pass it via args. + ( + NSUnbufferedIO=YES xcodebuild test \ + "${COMMON_ARGS[@]}" \ + "${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci + ) || xcodebuild_exit_code=${PIPESTATUS[0]} + + echo "" + echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" + + # Check for a build failure (e.g., compile error, linker issue, or simulator connection error) + if [ "$xcodebuild_exit_code" -ne 0 ]; then + echo "🔴 Build failed. See log above for compile errors." + exit "$xcodebuild_exit_code" + fi + + echo "" + echo "✅ Build Succeeded. Verifying test results from xcresult bundle..." + + # If the build passed, xcresultparser becomes the final gatekeeper for test results. + xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult + +elif [[ "$MODE" == "archive" ]]; then + + echo "--- Running Simulator Archive Build (App_Store_Release) ---" + + mkdir build + NSUnbufferedIO=YES xcodebuild archive \ + "${COMMON_ARGS[@]}" \ + "${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci + +else + echo "Error: Invalid mode '$MODE' specified. Use 'test' or 'archive'." + exit 1 +fi From 1d2e00bed251ad869c3b414599141967d03cbda3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 15:00:04 +1000 Subject: [PATCH 165/244] Script name error --- .drone.jsonnet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index e921c8c5ee..2befffe493 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,7 +77,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ - './Scripts/run_xcode_ci.sh test -resultBundlePath ./build/artifacts/testResults.xcresult -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES', + './Scripts/build_ci.sh test -resultBundlePath ./build/artifacts/testResults.xcresult -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES', ], depends_on: [ 'Reset SPM Cache if Needed', @@ -149,7 +149,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build', commands: [ - './Scripts/run_xcode_ci.sh archive -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator"', + './Scripts/build_ci.sh archive -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator"', ], depends_on: [ 'Reset SPM Cache if Needed', From 9369d228f219ce11bef1c51dd9a4843c4f5baa1d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 15:14:27 +1000 Subject: [PATCH 166/244] Another CI tweak --- Scripts/build_ci.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index dd9a410b7a..5be06c83d7 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -24,7 +24,6 @@ if [[ "$MODE" == "test" ]]; then echo "--- Running Build and Unit Tests (App_Store_Release) ---" - mkdir build xcodebuild_exit_code=0 # We wrap the pipeline in parentheses to capture the exit code of xcodebuild @@ -55,7 +54,6 @@ elif [[ "$MODE" == "archive" ]]; then echo "--- Running Simulator Archive Build (App_Store_Release) ---" - mkdir build NSUnbufferedIO=YES xcodebuild archive \ "${COMMON_ARGS[@]}" \ "${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci From c7d253fb4962540b610d862e0311c12a9cf6f40c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 15:36:33 +1000 Subject: [PATCH 167/244] Try to output error summary to avoid having to read log --- Scripts/build_ci.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index 5be06c83d7..bec8b93877 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -40,7 +40,11 @@ if [[ "$MODE" == "test" ]]; then # Check for a build failure (e.g., compile error, linker issue, or simulator connection error) if [ "$xcodebuild_exit_code" -ne 0 ]; then - echo "🔴 Build failed. See log above for compile errors." + echo "🔴 Build failed. See log above for full context." + echo "" + echo "--- Summary of Errors ---" + grep -i --color=always "error:" /tmp/xcodebuild_raw.log || true + echo "-------------------------" exit "$xcodebuild_exit_code" fi From ab527466650250464589ddc189aca5e18b8db574 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 4 Sep 2025 14:01:16 +0800 Subject: [PATCH 168/244] Aligned deleted message bubble's font and text color --- .../Message Cells/Content Views/DeletedMessageView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 566ddceca1..522dffc243 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -36,6 +36,7 @@ final class DeletedMessageView: UIView { let imageView = UIImageView(image: Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)?.withRenderingMode(.alwaysTemplate)) imageView.themeTintColor = textColor + imageView.alpha = Values.mediumOpacity imageView.contentMode = .scaleAspectFit imageView.set(.width, to: DeletedMessageView.iconSize) imageView.set(.height, to: DeletedMessageView.iconSize) @@ -46,7 +47,7 @@ final class DeletedMessageView: UIView { let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) titleLabel.preferredMaxLayoutWidth = maxWidth - 6 // `6` for the `stackView.layoutMargins` - titleLabel.font = .systemFont(ofSize: Values.smallFontSize) + titleLabel.font = .italicSystemFont(ofSize: Values.smallFontSize) titleLabel.text = { switch variant { case .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: @@ -56,6 +57,7 @@ final class DeletedMessageView: UIView { } }() titleLabel.themeTextColor = textColor + titleLabel.alpha = Values.mediumOpacity titleLabel.lineBreakMode = .byTruncatingTail titleLabel.numberOfLines = 2 From 547595a1c0468fb7da6cafff868010716f28dead Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 16:07:24 +1000 Subject: [PATCH 169/244] Log output tweaks --- Scripts/build_ci.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index bec8b93877..4e4dd14ee8 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -19,6 +19,9 @@ COMMON_ARGS=( ) UNIQUE_ARGS=("$@") +XCODEBUILD_RAW_LOG=$(mktemp) + +trap 'rm -f "$XCODEBUILD_RAW_LOG"' EXIT if [[ "$MODE" == "test" ]]; then @@ -32,7 +35,7 @@ if [[ "$MODE" == "test" ]]; then ( NSUnbufferedIO=YES xcodebuild test \ "${COMMON_ARGS[@]}" \ - "${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci + "${UNIQUE_ARGS[@]}" 2>&1 | tee "$XCODEBUILD_RAW_LOG" | xcbeautify --is-ci ) || xcodebuild_exit_code=${PIPESTATUS[0]} echo "" @@ -43,7 +46,7 @@ if [[ "$MODE" == "test" ]]; then echo "🔴 Build failed. See log above for full context." echo "" echo "--- Summary of Errors ---" - grep -i --color=always "error:" /tmp/xcodebuild_raw.log || true + grep -i --color=always "error:" "$XCODEBUILD_RAW_LOG" || true echo "-------------------------" exit "$xcodebuild_exit_code" fi From 6777740f9c573193917d09fedc103474bf1311c9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 17:03:54 +1000 Subject: [PATCH 170/244] Fix build error --- .../_TestUtilities/MockImageDataManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift index d409bede10..78693da700 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift @@ -12,9 +12,10 @@ class MockImageDataManager: Mock, ImageDataManagerType { return mock(args: [source]) } + @MainActor func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) { mockNoReturn(args: [source], untrackedArgs: [onComplete]) } From addad4cfd20e14e68d6548cf491cc56c3fd30fb7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 17:32:14 +1000 Subject: [PATCH 171/244] More tweaks --- Scripts/build_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index 4e4dd14ee8..2b06471851 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -46,7 +46,7 @@ if [[ "$MODE" == "test" ]]; then echo "🔴 Build failed. See log above for full context." echo "" echo "--- Summary of Errors ---" - grep -i --color=always "error:" "$XCODEBUILD_RAW_LOG" || true + grep -E --color=always '(:[0-9]+:[0-9]+: error:)|(ld: error:)|(Command PhaseScriptExecution failed)' "$XCODEBUILD_RAW_LOG" || true echo "-------------------------" exit "$xcodebuild_exit_code" fi From bc13d9d5214eab2a065a8c6b974f0bc2c07dfbdd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Sep 2025 08:29:38 +1000 Subject: [PATCH 172/244] Verify test results --- Scripts/build_ci.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index 2b06471851..4b9864f660 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -1,5 +1,7 @@ #!/bin/bash +IFS=$' \t\n' + set -euo pipefail if [ $# -lt 1 ]; then @@ -54,8 +56,20 @@ if [[ "$MODE" == "test" ]]; then echo "" echo "✅ Build Succeeded. Verifying test results from xcresult bundle..." - # If the build passed, xcresultparser becomes the final gatekeeper for test results. - xcresultparser --output-format cli --exit-with-error-on-failure ./build/artifacts/testResults.xcresult + # If the build passed, xcresultparser becomes the final gatekeeper for test results + parser_output=$(xcresultparser --output-format cli ./build/artifacts/testResults.xcresult) + echo "$parser_output" + + build_errors_count=$(echo "$parser_output" | grep "Number of errors" | awk '{print $NF}') + failed_tests_count=$(echo "$parser_output" | grep "Number of failed tests" | awk '{print $NF}') + + if [ "${build_errors_count:-0}" -gt 0 ] || [ "${failed_tests_count:-0}" -gt 0 ]; then + echo "" + echo "🔴 Verification failed: Found $build_errors_count build error(s) and $failed_tests_count failed test(s) in the xcresult bundle." + exit 1 + else + echo "✅ Verification successful: No build errors or test failures found." + fi elif [[ "$MODE" == "archive" ]]; then From b08d759f93d14b059d8f9b1aa4b5851edd45f1f8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Sep 2025 08:49:08 +1000 Subject: [PATCH 173/244] Further tweaks --- Scripts/build_ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index 4b9864f660..fcc8c2c1b2 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -57,8 +57,8 @@ if [[ "$MODE" == "test" ]]; then echo "✅ Build Succeeded. Verifying test results from xcresult bundle..." # If the build passed, xcresultparser becomes the final gatekeeper for test results - parser_output=$(xcresultparser --output-format cli ./build/artifacts/testResults.xcresult) - echo "$parser_output" + xcresultparser --output-format cli --no-test-result --coverage ./build/artifacts/testResults.xcresult + parser_output=$(xcresultparser --output-format cli --no-test-result ./build/artifacts/testResults.xcresult) build_errors_count=$(echo "$parser_output" | grep "Number of errors" | awk '{print $NF}') failed_tests_count=$(echo "$parser_output" | grep "Number of failed tests" | awk '{print $NF}') From 2fb0173db24bfd088a1a81bd81bf193a90723523 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Sep 2025 09:37:04 +1000 Subject: [PATCH 174/244] Another tweak --- Scripts/build_ci.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index fcc8c2c1b2..5c5cf69b07 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -47,9 +47,15 @@ if [[ "$MODE" == "test" ]]; then if [ "$xcodebuild_exit_code" -ne 0 ]; then echo "🔴 Build failed. See log above for full context." echo "" - echo "--- Summary of Errors ---" + echo "--- Summary of Potential Build Errors ---" grep -E --color=always '(:[0-9]+:[0-9]+: error:)|(ld: error:)|(Command PhaseScriptExecution failed)' "$XCODEBUILD_RAW_LOG" || true - echo "-------------------------" + echo "" + echo "--- End of Raw Log (for context on unknown errors) ---" + + # If the grep above was empty, the error is likely in the last few lines + tail -n 50 "$XCODEBUILD_RAW_LOG" + + echo "----------------------------------------------------" exit "$xcodebuild_exit_code" fi From ab0ce7a5116231ee41b08d7e998f3d6f7e287a0d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Sep 2025 09:57:51 +1000 Subject: [PATCH 175/244] A few more tweaks to better indicate failures --- .drone.jsonnet | 1 + Scripts/build_ci.sh | 42 ++++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 2befffe493..64e175cee2 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -101,6 +101,7 @@ local clean_up_old_test_sims_on_commit_trigger = { commands: [ 'echo "--- FAILED TESTS ---"', 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult', + 'exit 1' // Always fail if this runs to make it more obvious in the UI ], depends_on: ['Build and Run Tests'], when: { diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh index 5c5cf69b07..9ff7932d3d 100755 --- a/Scripts/build_ci.sh +++ b/Scripts/build_ci.sh @@ -43,26 +43,16 @@ if [[ "$MODE" == "test" ]]; then echo "" echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" - # Check for a build failure (e.g., compile error, linker issue, or simulator connection error) - if [ "$xcodebuild_exit_code" -ne 0 ]; then - echo "🔴 Build failed. See log above for full context." - echo "" - echo "--- Summary of Potential Build Errors ---" - grep -E --color=always '(:[0-9]+:[0-9]+: error:)|(ld: error:)|(Command PhaseScriptExecution failed)' "$XCODEBUILD_RAW_LOG" || true - echo "" - echo "--- End of Raw Log (for context on unknown errors) ---" - - # If the grep above was empty, the error is likely in the last few lines - tail -n 50 "$XCODEBUILD_RAW_LOG" - - echo "----------------------------------------------------" - exit "$xcodebuild_exit_code" + if [ "$xcodebuild_exit_code" -eq 0 ]; then + echo "✅ All tests passed and build succeeded!" + exit 0 fi - + echo "" - echo "✅ Build Succeeded. Verifying test results from xcresult bundle..." - - # If the build passed, xcresultparser becomes the final gatekeeper for test results + echo "🔴 Build failed" + echo "----------------------------------------------------" + echo "Checking for test failures in xcresult bundle..." + xcresultparser --output-format cli --no-test-result --coverage ./build/artifacts/testResults.xcresult parser_output=$(xcresultparser --output-format cli --no-test-result ./build/artifacts/testResults.xcresult) @@ -71,11 +61,23 @@ if [[ "$MODE" == "test" ]]; then if [ "${build_errors_count:-0}" -gt 0 ] || [ "${failed_tests_count:-0}" -gt 0 ]; then echo "" - echo "🔴 Verification failed: Found $build_errors_count build error(s) and $failed_tests_count failed test(s) in the xcresult bundle." + echo "🔴 Found $build_errors_count build error(s) and $failed_tests_count failed test(s) in the xcresult bundle." exit 1 else - echo "✅ Verification successful: No build errors or test failures found." + echo "No test failures found in results. Failure was likely a build error." + echo "" + + echo "--- Summary of Potential Build Errors ---" + grep -E --color=always '(:[0-9]+:[0-9]+: error:)|(ld: error:)|(error: linker command failed)|(PhaseScriptExecution)|(rsync error:)' "$XCODEBUILD_RAW_LOG" || true + echo "" + echo "--- End of Raw Log ---" + tail -n 20 "$XCODEBUILD_RAW_LOG" + echo "-------------------------" + exit "$xcodebuild_exit_code" fi + + echo "----------------------------------------------------" + exit "$xcodebuild_exit_code" elif [[ "$MODE" == "archive" ]]; then From 5deb0be9b7a82d02367659186354d2d9cd7e6349 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 5 Sep 2025 08:52:49 +0800 Subject: [PATCH 176/244] Updated paddings and font sizes --- .../Message Cells/CallMessageCell.swift | 2 +- .../Content Views/DeletedMessageView.swift | 15 +++++++++++---- SessionUIKit/Style Guide/Values.swift | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 305ea3a29c..13566d5769 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -48,7 +48,7 @@ final class CallMessageCell: MessageCell { private lazy var label: UILabel = { let result: UILabel = UILabel() - result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.themeTextColor = .textPrimary result.textAlignment = .center result.lineBreakMode = .byWordWrapping diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 522dffc243..72f5e5c4b1 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -10,6 +10,8 @@ import SessionUtilitiesKit final class DeletedMessageView: UIView { private static let iconSize: CGFloat = 18 private static let iconImageViewSize: CGFloat = 30 + private static let horizontalInset = Values.mediumSmallSpacing + private static let verticalInset = Values.smallSpacing // MARK: - Lifecycle @@ -36,7 +38,7 @@ final class DeletedMessageView: UIView { let imageView = UIImageView(image: Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)?.withRenderingMode(.alwaysTemplate)) imageView.themeTintColor = textColor - imageView.alpha = Values.mediumOpacity + imageView.alpha = Values.highOpacity imageView.contentMode = .scaleAspectFit imageView.set(.width, to: DeletedMessageView.iconSize) imageView.set(.height, to: DeletedMessageView.iconSize) @@ -47,7 +49,7 @@ final class DeletedMessageView: UIView { let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) titleLabel.preferredMaxLayoutWidth = maxWidth - 6 // `6` for the `stackView.layoutMargins` - titleLabel.font = .italicSystemFont(ofSize: Values.smallFontSize) + titleLabel.font = .italicSystemFont(ofSize: Values.mediumFontSize) titleLabel.text = { switch variant { case .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: @@ -57,7 +59,7 @@ final class DeletedMessageView: UIView { } }() titleLabel.themeTextColor = textColor - titleLabel.alpha = Values.mediumOpacity + titleLabel.alpha = Values.highOpacity titleLabel.lineBreakMode = .byTruncatingTail titleLabel.numberOfLines = 2 @@ -70,7 +72,12 @@ final class DeletedMessageView: UIView { addSubview(stackView) let calculatedSize: CGSize = stackView.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 999)) - stackView.pin(to: self, withInset: Values.smallSpacing) + + stackView.pin(.top, to: .top, of: self, withInset: Self.verticalInset) + stackView.pin(.leading, to: .leading, of: self, withInset: Self.horizontalInset) + stackView.pin(.trailing, to: .trailing, of: self, withInset: -Self.horizontalInset) + stackView.pin(.bottom, to: .bottom, of: self, withInset: -Self.verticalInset) + stackView.set(.height, to: calculatedSize.height) } } diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index ffd825c2af..e1b8d4eb8c 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -8,7 +8,7 @@ public enum Values { public static let veryLowOpacity = CGFloat(0.12) public static let lowOpacity = CGFloat(0.4) public static let mediumOpacity = CGFloat(0.6) - public static let highOpacity = CGFloat(0.75) + public static let highOpacity = CGFloat(0.7) // MARK: - Font Sizes public static let miniFontSize = isIPhone5OrSmaller ? CGFloat(8) : CGFloat(10) From e6bda959abf8c36816356046beeffdf1bd0ea899 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 5 Sep 2025 11:40:15 +0800 Subject: [PATCH 177/244] Refactor accessory view alignment handling --- Session/Settings/AppearanceViewModel.swift | 3 +- .../ConversationSettingsViewModel.swift | 3 +- Session/Settings/HelpViewModel.swift | 12 ++--- .../Shared/Types/SessionCell+Accessory.swift | 25 +++++---- .../Views/SessionCell+AccessoryView.swift | 54 ++++++++++++++----- 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index c88b392681..8ae3cba518 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -240,8 +240,7 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ ), trailingAccessory: .icon( .chevronRight, - shouldFill: true , - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index 68214b2380..d26cff429d 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -190,8 +190,7 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold subtitle: "blockedContactsManageDescription".localized(), trailingAccessory: .icon( .chevronRight, - shouldFill: true , - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 26e38683da..4ab69ce0d1 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -77,8 +77,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small, - shouldFill: true, - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/translate") else { @@ -100,8 +99,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small, - shouldFill: true, - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/survey") else { @@ -123,8 +121,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small, - shouldFill: true, - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/faq") else { @@ -146,8 +143,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small, - shouldFill: true, - shouldFollowIconSize: true + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://sessionapp.zendesk.com/hc/en-us") else { diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index 4ebb6e17e3..f27e6e19db 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -40,7 +40,7 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, - shouldFollowIconSize: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil, ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( @@ -49,7 +49,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, - shouldFollowIconSize: shouldFollowIconSize, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -59,7 +59,7 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, - shouldFollowIconSize: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil, ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( @@ -68,7 +68,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, - shouldFollowIconSize: shouldFollowIconSize, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -78,6 +78,7 @@ public extension SessionCell.Accessory { source: ImageDataManager.DataSource?, customTint: ThemeValue? = nil, shouldFill: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.IconAsync( @@ -85,6 +86,7 @@ public extension SessionCell.Accessory { source: source, customTint: customTint, shouldFill: shouldFill, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -230,7 +232,7 @@ public extension SessionCell.AccessoryConfig { public let iconSize: IconSize public let customTint: ThemeValue? public let shouldFill: Bool - public let shouldFollowIconSize: Bool + public let pinEdges: [UIView.HorizontalEdge] fileprivate init( icon: Lucide.Icon?, @@ -238,7 +240,7 @@ public extension SessionCell.AccessoryConfig { iconSize: IconSize, customTint: ThemeValue?, shouldFill: Bool, - shouldFollowIconSize: Bool = false, + pinEdges: [UIView.HorizontalEdge], accessibility: Accessibility? ) { self.icon = icon @@ -246,7 +248,7 @@ public extension SessionCell.AccessoryConfig { self.iconSize = iconSize self.customTint = customTint self.shouldFill = shouldFill - self.shouldFollowIconSize = shouldFollowIconSize + self.pinEdges = pinEdges super.init(accessibility: accessibility) } @@ -259,7 +261,7 @@ public extension SessionCell.AccessoryConfig { iconSize.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) - shouldFollowIconSize.hash(into: &hasher) + pinEdges.hash(into: &hasher) accessibility.hash(into: &hasher) } @@ -272,7 +274,7 @@ public extension SessionCell.AccessoryConfig { iconSize == rhs.iconSize && customTint == rhs.customTint && shouldFill == rhs.shouldFill && - shouldFollowIconSize == rhs.shouldFollowIconSize && + pinEdges == rhs.pinEdges && accessibility == rhs.accessibility ) @@ -288,18 +290,21 @@ public extension SessionCell.AccessoryConfig { public let source: ImageDataManager.DataSource? public let customTint: ThemeValue? public let shouldFill: Bool + public let pinEdges: [UIView.HorizontalEdge] fileprivate init( iconSize: IconSize, source: ImageDataManager.DataSource?, customTint: ThemeValue?, shouldFill: Bool, + pinEdges: [UIView.HorizontalEdge], accessibility: Accessibility? ) { self.iconSize = iconSize self.source = source self.customTint = customTint self.shouldFill = shouldFill + self.pinEdges = pinEdges super.init(accessibility: accessibility) } @@ -311,6 +316,7 @@ public extension SessionCell.AccessoryConfig { source?.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + pinEdges.hash(into: &hasher) accessibility.hash(into: &hasher) } @@ -322,6 +328,7 @@ public extension SessionCell.AccessoryConfig { source == rhs.source && customTint == rhs.customTint && shouldFill == rhs.shouldFill && + pinEdges == rhs.pinEdges && accessibility == rhs.accessibility ) } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index a93a8c41f3..d3b47b0dd1 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -73,7 +73,7 @@ extension SessionCell { if let newView: UIView = maybeView { addSubview(newView) - newView.pin(to: self) + pin(view: newView, accessory: accessory) layout(view: newView, accessory: accessory) } @@ -189,13 +189,36 @@ extension SessionCell { } } + // Determine the type of accessory to decide how to pin the view. + private func pin(view: UIView?, accessory: Accessory) { + // Icon and IconAsync types have specific layouts, so we do nothing here. + switch accessory { + case _ as SessionCell.AccessoryConfig.Icon: break + case _ as SessionCell.AccessoryConfig.IconAsync: break + default: + // For all other accessory types, pin the view to the current view. + // This is a generic layout for accessories that don't need a custom position. + view?.pin(to: self) + } + } + private func layout(view: UIView?, accessory: Accessory) { switch accessory { case let accessory as SessionCell.AccessoryConfig.Icon: - layoutIconView(view, iconSize: accessory.iconSize, shouldFill: accessory.shouldFill) + layoutIconView( + view, + iconSize: accessory.iconSize, + shouldFill: accessory.shouldFill, + pin: accessory.pinEdges + ) case let accessory as SessionCell.AccessoryConfig.IconAsync: - layoutIconView(view, iconSize: accessory.iconSize, shouldFill: accessory.shouldFill) + layoutIconView( + view, + iconSize: accessory.iconSize, + shouldFill: accessory.shouldFill, + pin: accessory.pinEdges + ) case is SessionCell.AccessoryConfig.Toggle: layoutToggleView(view) case is SessionCell.AccessoryConfig.DropDown: layoutDropDownView(view) @@ -288,13 +311,25 @@ extension SessionCell { return result } - private func layoutIconView(_ view: UIView?, iconSize: IconSize, shouldFill: Bool) { + private func layoutIconView(_ view: UIView?, iconSize: IconSize, shouldFill: Bool, pin edges: [UIView.HorizontalEdge]) { guard let imageView: SessionImageView = view as? SessionImageView else { return } imageView.set(.width, to: iconSize.size) imageView.set(.height, to: iconSize.size) - imageView.pin(.leading, to: .leading, of: self, withInset: (shouldFill ? 0 : Values.smallSpacing)) - imageView.pin(.trailing, to: .trailing, of: self, withInset: (shouldFill ? 0 : -Values.smallSpacing)) + imageView.pin(.top, to: .top, of: self) + imageView.pin(.bottom, to: .bottom, of: self) + + let shouldInvertPadding: [UIView.HorizontalEdge] = [.left, .trailing] + + for edge in edges { + let inset: CGFloat = ( + (shouldFill ? 0 : Values.smallSpacing) * + (shouldInvertPadding.contains(edge) ? -1 : 1) + ) + + imageView.pin(edge, to: edge, of: self, withInset: inset) + } + fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) minWidthConstraint.isActive = !fixedWidthConstraint.isActive } @@ -306,13 +341,6 @@ extension SessionCell { imageView.accessibilityLabel = accessory.accessibility?.label imageView.themeTintColor = (accessory.customTint ?? tintColor) imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit) - - // Use icon size when displaying accessory view. - // 50 width causes large padding not aligning accessory to right - if accessory.shouldFollowIconSize { - fixedWidthConstraint.constant = accessory.iconSize.size - fixedWidthConstraint.isActive = true - } switch (accessory.icon, accessory.image) { case (.some(let icon), _): From fb13dee5900689db1b987e2332ba8e7bb2b9f51c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 8 Sep 2025 07:51:18 +0800 Subject: [PATCH 178/244] Clean up code --- Session/Settings/Views/NewTagView.swift | 2 +- .../Views/SessionCell+AccessoryView.swift | 20 +++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Session/Settings/Views/NewTagView.swift b/Session/Settings/Views/NewTagView.swift index f50daca9d8..5156a4885a 100644 --- a/Session/Settings/Views/NewTagView.swift +++ b/Session/Settings/Views/NewTagView.swift @@ -34,7 +34,7 @@ final class NewTagView: UIView { private func setupUI() { addSubview(newTagLabel) - newTagLabel.pin(.leading, to: .leading, of: self, withInset: -Values.mediumSpacing + Values.verySmallSpacing) + newTagLabel.pin(.leading, to: .leading, of: self, withInset: -(Values.mediumSpacing + Values.verySmallSpacing)) newTagLabel.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.trailing ], to: self) } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index d3b47b0dd1..7a953315b8 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -73,7 +73,6 @@ extension SessionCell { if let newView: UIView = maybeView { addSubview(newView) - pin(view: newView, accessory: accessory) layout(view: newView, accessory: accessory) } @@ -188,20 +187,7 @@ extension SessionCell { return nil } } - - // Determine the type of accessory to decide how to pin the view. - private func pin(view: UIView?, accessory: Accessory) { - // Icon and IconAsync types have specific layouts, so we do nothing here. - switch accessory { - case _ as SessionCell.AccessoryConfig.Icon: break - case _ as SessionCell.AccessoryConfig.IconAsync: break - default: - // For all other accessory types, pin the view to the current view. - // This is a generic layout for accessories that don't need a custom position. - view?.pin(to: self) - } - } - + private func layout(view: UIView?, accessory: Accessory) { switch accessory { case let accessory as SessionCell.AccessoryConfig.Icon: @@ -595,6 +581,8 @@ extension SessionCell { let radioView: UIView = radioBorderView.subviews.first else { return } + label.pin(to: self) + label.pin(.top, to: .top, of: self) label.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) label.pin(.trailing, to: .leading, of: radioBorderView, withInset: -Values.smallSpacing) @@ -790,6 +778,8 @@ extension SessionCell { minWidthConstraint.isActive = true } + view.pin(.top, to: .top, of: self) + view.pin(.bottom, to: .bottom, of: self) view.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) view.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) } From b8c4196eadb2c05ecc2032b96dfc3e25b1c16dd8 Mon Sep 17 00:00:00 2001 From: mpretty-cyro <15862619+mpretty-cyro@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:36:44 +0000 Subject: [PATCH 179/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 1673 ++++++++++++++++- 1 file changed, 1671 insertions(+), 2 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 11c67635b7..5e0dfa04f5 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -20924,6 +20924,12 @@ "appearanceAutoDarkMode" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avto-qaranlıq rejimi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -30959,6 +30965,12 @@ "appProBadge" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} nişanı" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -63360,6 +63372,30 @@ "value" : "Mesaj göndərmək üçün bu kontaktı əngəldən çıxardın." } }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیغام بھیجنے کے لئے اس رابطہ کو غیر بلاک کریں۔" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблакуйце гэты кантакт, каб адправіць паведамленне" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отблокирай този контакт за да изпратиш съобщение" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "মেসেজ পাঠাতে এই কন্টাক্টটি আনব্লক করুন।" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -63408,6 +63444,12 @@ "value" : "Desbloquea este contacto para enviar mensajes." } }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontaktu hau desblokeatu mezu bat bidaltzeko" + } + }, "fi" : { "stringUnit" : { "state" : "translated", @@ -63426,12 +63468,24 @@ "value" : "कोई संदेश भेजने के लिए इस संपर्क को अनवरोधित करें" } }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokiraj ovaj kontakt za slanje poruke" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Üzenet küldéséhez oldd fel a kontakt letiltását." } }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Արգելաբացել այս կոնտակտը հաղորդագրություն ուղարկելու համար" + } + }, "id" : { "stringUnit" : { "state" : "translated", @@ -63486,6 +63540,12 @@ "value" : "Nyahsekat kontak ini untuk menghantar mesej" } }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "မက်ဆေ့ချ် ပို့ရန်အတွက်ဤဆက်သွယ်မှုသို့ ဘလော့ကိုဖြေပါ။" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -63510,6 +63570,12 @@ "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." } }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokankha Lamulo Llitsa lemba uthenga" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -63540,6 +63606,12 @@ "value" : "Разблокируйте этот контакт, чтобы отправить сообщение" } }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Që t’i dërgohet një mesazh, zhbllokojeni këtë kontakt" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -65030,6 +65102,12 @@ "blockedContactsManageDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əngəllənmiş kontaktları görün və idarə edin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -78446,6 +78524,12 @@ "callsVoiceAndVideoModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beta zənglərini istifadə edərkən IP-niz zəng tərəfdaşınıza və {session_foundation} serverinə görünür." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -83271,6 +83355,12 @@ "cancelPlan" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planı ləğv et" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -83282,6 +83372,12 @@ "change" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dəyişdir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -83772,6 +83868,12 @@ "changePasswordModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün parolunuzu dəyişdirin. Daxili olaraq saxlanılmış verilər, yeni parolunuzla təkrar şifrələnəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -113098,6 +113200,12 @@ "contentNotificationDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni bir mesaj alındıqda daxili bildirişlərdə nümayiş olunacaq məzmunu seçin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -118923,6 +119031,12 @@ "conversationsEnterDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter və Shift+Enter düymələrinin danışıqlarda necə işləyəcəyini təyin edin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -118934,6 +119048,12 @@ "conversationsEnterNewLine" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER mesajı göndərir, ENTER yeni sətrə keçir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -118945,6 +119065,12 @@ "conversationsEnterSends" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER mesajı göndərir, SHIFT + ENTER yeni sətrə keçir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -120393,6 +120519,12 @@ "conversationsMessageTrimmingTrimCommunitiesDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "2,000-dən çox mesajı olan icmalarda 6 aydan köhnə mesajları avtomatik sil." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -121362,6 +121494,12 @@ "conversationsSendWithEnterKey" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter ilə göndər" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -121852,6 +121990,12 @@ "conversationsSendWithShiftEnter" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift+Enter ilə göndər" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -125413,6 +125557,12 @@ "currentPassword" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı parol" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -125424,6 +125574,12 @@ "currentPlan" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı plan" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -125920,6 +126076,12 @@ "darkMode" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qaranlıq rejim" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -168918,6 +169080,12 @@ "display" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nümayiş" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -187186,6 +187354,12 @@ "enableNotifications" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni mesaj aldığınız zaman bildirişlər göstərilsin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -187691,6 +187865,12 @@ "enter" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daxil ol" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -190437,6 +190617,12 @@ "feedback" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əks-əlaqə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -190448,6 +190634,12 @@ "feedbackDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qısa anketi dolduraraq {app_name} ilə təcrübənizi paylaşın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -191417,6 +191609,12 @@ "followSystemSettings" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem ayarlarını izlə." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -232304,6 +232502,12 @@ "helpFAQDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ümumi suallara cavab tapmaq üçün {app_name} TVS-yə baxın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -232788,6 +232992,12 @@ "helpReportABug" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir xəta bildir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -234727,6 +234937,12 @@ "helpReportABugExportLogsSaveToDesktopDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu faylı saxlayın, sonra onu {app_name} gəlişdiriciləri ilə paylaşın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -235217,6 +235433,12 @@ "helpTranslateSessionDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqini 80-dən çox dildə tərcümə etməyə kömək edin!" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -236186,6 +236408,12 @@ "hideMenuBarDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem menyu çubuğunun görünməsini dəyişdir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -237477,6 +237705,12 @@ "important" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vacib" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -242032,10 +242266,16 @@ "launchOnStartDescriptionDesktop" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spustit {app_name} automaticky při spuštění počítače." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Launch Session automatically when your computer starts up." + "value" : "Launch {app_name} automatically when your computer starts up." } } } @@ -242043,6 +242283,12 @@ "launchOnStartDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açılışda başlat" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -242054,10 +242300,16 @@ "launchOnStartupDisabledDesktop" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toto nastavení spravuje váš systém s Linuxem. Chcete-li povolit automatické spuštění, přidejte {app_name} do spouštěných aplikací v nastavení systému." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "This setting is managed by your system on Linux. To enable automatic startup, add Session to your startup applications in system settings." + "value" : "This setting is managed by your system on Linux. To enable automatic startup, add {app_name} to your startup applications in system settings." } } } @@ -252019,6 +252271,12 @@ "links" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keçidlər" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -257790,6 +258048,12 @@ "logs" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log-lar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -257962,6 +258226,12 @@ "managePro" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} - idarə et" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -268192,6 +268462,12 @@ "menuBar" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyu çubuğu" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -268819,6 +269095,12 @@ "messageCopy" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajı kopyala" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -293004,6 +293286,12 @@ "newPassword" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni parol" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -293494,6 +293782,12 @@ "nextSteps" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Növbəti addımlar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -299001,6 +299295,12 @@ "notificationDisplay" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildiriş nümayişi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -301892,6 +302192,12 @@ "notificationSenderNameAndPreview" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajın göndərənin adı və mesaj məzmununun bir önizləməsi nümayiş olunsun." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -301903,6 +302209,12 @@ "notificationSenderNameOnly" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heç bir mesaj məzmunu olmadan yalnız mesajı göndərənin adı nümayiş olunsun." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -303506,6 +303818,12 @@ "notificationsGenericOnly" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajı göndərənin adı və ya mesajın məzmunu olmadan ümumi {app_name} bildirişi nümayiş olunsun." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -306840,6 +307158,12 @@ "notificationsMakeSound" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni mesaj aldığınız zaman bir səs oxudulsun." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -323950,6 +324274,12 @@ "onDevice" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{device_type} cihazınızda" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -323961,6 +324291,12 @@ "onDeviceDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda qeydiyyatdan keçdiyiniz {platform_account} hesabına giriş etdiyiniz {device_type} cihazından bu {app_name} hesabını açın. Sonra planınızı {app_pro} ayarları vasitəsilə dəyişdirin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -328301,6 +328637,12 @@ "openStoreWebsite" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_store} veb saytını aç" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -328916,6 +329258,12 @@ "password" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parol" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -329412,6 +329760,12 @@ "passwordChangedDescriptionToast" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parolunuz dəyişdirilib. Lütfən onu güvəndə saxlayın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -329423,6 +329777,12 @@ "passwordChangeShortDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu dəyişdir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -329919,6 +330279,12 @@ "passwordCreate" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parol yarat" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -333875,6 +334241,12 @@ "passwordNewConfirm" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni parolu təsdiqlə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334365,6 +334737,12 @@ "passwordRemovedDescriptionToast" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parolunuz silinib." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334376,6 +334754,12 @@ "passwordRemoveShortDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu sil" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334387,6 +334771,12 @@ "passwords" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parollar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334877,6 +335267,12 @@ "passwordSetDescriptionToast" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parolunuz təyin edilib. Lütfən onu güvəndə saxlayın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334888,6 +335284,12 @@ "passwordSetShortDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açılışda {app_name} kilidini açmaq üçün parol tələb edilsin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334899,6 +335301,12 @@ "passwordStrengthCharLength" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 xarakterdən uzun" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334910,6 +335318,12 @@ "passwordStrengthIncludeNumber" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir rəqəm ehtiva etməlidir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334921,6 +335335,12 @@ "passwordStrengthIncludesLowercase" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir kiçik hərf ehtiva etməlidir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334932,6 +335352,12 @@ "passwordStrengthIncludesSymbol" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir simvol daxildir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334943,6 +335369,12 @@ "passwordStrengthIncludesUppercase" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir böyük hərf ehtiva etməlidir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334954,6 +335386,12 @@ "passwordStrengthIndicator" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parol gücü göstəricisi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -334965,6 +335403,12 @@ "passwordStrengthIndicatorDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güclü bir parol təyin etmək, cihazınız itsə və ya oğurlansa belə mesajlarınızı və qoşmalarınızı qorumağa kömək edir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -339458,6 +339902,12 @@ "permissionsKeepInSystemTrayDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pəncərəni bağladığınız zaman {app_name} arxaplanda çalışmağa davam edir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349013,6 +349463,12 @@ "plusLoadsMore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üstəgəl daha çoxu gəlir..." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349024,6 +349480,12 @@ "plusLoadsMoreDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} üçün yeni özəlliklər tezliklə gəlir. {icon} {pro} Yol Xəritəsində yenilikləri kəşf edin" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349035,6 +349497,12 @@ "preferences" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tərcihlər" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349525,6 +349993,12 @@ "previewNotification" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildirişi önizlə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349661,6 +350135,12 @@ "proAllSet" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hər şey hazırdır!" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -349672,6 +350152,12 @@ "proAllSetDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız güncəlləndi! Hazırkı {pro} planınız avtomatik olaraq {date} tarixində yeniləndiyi zaman ödəniş haqqı alınacaq." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350326,6 +350812,12 @@ "proAnimatedDisplayPictures" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animasiyalı ekran şəkilləri" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350337,6 +350829,12 @@ "proAnimatedDisplayPicturesDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animasiyalı GIF və WebP təsvirlərini ekran şəklini olaraq təyin edin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350479,6 +350977,12 @@ "proAutoRenewTime" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro}, {time} tarixində avto-yenilənir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350490,6 +350994,12 @@ "proBadge" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} nişanı" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350501,6 +351011,12 @@ "proBadges" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nişanlar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350512,6 +351028,12 @@ "proBadgesDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran adınızın yanında eksklüziv bir nişanla {app_name} tətbiqini dəstəklədiyinizi göstərin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350523,6 +351045,34 @@ "proBadgesSent" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} nişan göndərildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} nişan göndərildi" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350556,6 +351106,12 @@ "proBadgeVisible" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} nişanını digər istifadəçilərə göstər" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350567,6 +351123,12 @@ "proBilledAnnually" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} - illik haqq" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350578,6 +351140,12 @@ "proBilledMonthly" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} - aylıq haqq" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350589,6 +351157,12 @@ "proBilledQuarterly" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} - rüblük haqq" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -350999,6 +351573,12 @@ "processingRefundRequest" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} geri ödəmə tələbinizi emal edir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351010,6 +351590,12 @@ "proDiscountTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı planınızda artıq\r\ntam {app_pro} qiymətinin {percent}% endirimi mövcuddur." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351021,6 +351607,12 @@ "proExpired" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Müddəti bitib" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351032,6 +351624,12 @@ "proExpiredDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Təəssüf ki, {pro} planınızın müddəti bitib. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün yeniləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351043,6 +351641,12 @@ "proExpiringSoon" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tezliklə bitir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351054,6 +351658,12 @@ "proExpiringSoonDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınızın müddəti {time} vaxtında bitir. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün planınızı güncəlləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351065,6 +351675,12 @@ "proExpiringTime" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro}, {time} vaxtında başa çatır" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351076,6 +351692,12 @@ "proFaq" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} TVS" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351087,6 +351709,12 @@ "proFaqDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} TVS-da tez-tez verilən suallara cavab tapın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351765,6 +352393,12 @@ "proFeatures" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} özəllikləri" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -354894,6 +355528,34 @@ "proGroupsUpgraded" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} qrup yüksəldildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} qrup yüksəldildi" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -354927,6 +355589,12 @@ "proImportantDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəniş tələbi qətidir. Əgər təsdiqlənsə, {pro} planınız dərhal ləğv ediləcək və bütün {pro} özəlliklərinə erişimi itirəcəksiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355176,6 +355844,12 @@ "proLargerGroups" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha böyük qruplar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355187,6 +355861,12 @@ "proLargerGroupsDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admin olduğunuz qruplar, avtomatik olaraq 300 üzvü dəstəkləmək üçün təkmilləşdirilir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355198,6 +355878,12 @@ "proLongerMessages" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha uzun mesajlar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355209,6 +355895,12 @@ "proLongerMessagesDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bütün danışıqlarda 10,000 xarakterə qədər mesaj göndərə bilərsiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355220,6 +355912,34 @@ "proLongerMessagesSent" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} daha uzun mesaj göndərildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} daha uzun mesaj göndərildi" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357465,6 +358185,12 @@ "proPercentOff" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% endirim" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357476,6 +358202,34 @@ "proPinnedConversations" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} sancılmış danışıq" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} sancılmış danışıq" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357509,6 +358263,12 @@ "proPlanActivatedAuto" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız aktivdir!

    Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək. Planınıza edilən güncəlləmələr növbəti {pro} yenilənməsi zamanı qüvvəyə minəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357520,6 +358280,12 @@ "proPlanActivatedAutoShort" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız aktivdir!

    Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357531,6 +358297,12 @@ "proPlanActivatedNotAuto" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir.

    Eksklüziv Pro özəlliklərinə kəsintisiz erişimi təmin etmək üçün planınızı indi güncəlləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357542,6 +358314,12 @@ "proPlanExpireDate" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357553,6 +358331,12 @@ "proPlanNotFound" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı tapılmadı" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357564,6 +358348,12 @@ "proPlanNotFoundDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesabınız üçün heç bir aktiv plan tapılmadı. Bunun bir səhv olduğunu düşünürsünüzsə, lütfən kömək üçün {app_name} dəstəyi ilə əlaqə saxlayın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357575,6 +358365,12 @@ "proPlanPlatformRefund" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri ödəmə tələbini göndərmək üçün eyni {platform_account} hesabını istifadə etməlisiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357586,6 +358382,12 @@ "proPlanPlatformRefundLong" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

    Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəmə tələbinizi göndərin.

    {app_name} Dəstək komandası, geri ödəmə tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357597,6 +358399,12 @@ "proPlanRecover" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planını geri qaytar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357608,6 +358416,12 @@ "proPlanRenew" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planını yenilə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357619,6 +358433,12 @@ "proPlanRenewDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırda, {pro} planları, yalnızca {platform_store} və {platform_store} Mağazaları vasitəsilə satın alına və yenilənə bilər. {app_name} Masaüstü istifadə etdiyinizə görə planınızı burada yeniləyə bilməzsiniz.

    {app_pro} gəlişdiriciləri, istifadəçilərin {pro} planlarını {platform_store} və {platform_store} Mağazalarından kənarda almağına imkan verəcək alternativ ödəniş variantları üzərində ciddi şəkildə çalışırlar. {pro} Yol Xəritəsi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357630,6 +358450,12 @@ "proPlanRenewDesktopLinked" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_store} və ya {platform_store} Mağazaları vasitəsilə planınızı {app_name} quraşdırılmış və əlaqələndirilmiş cihazda {app_pro} ayarlarında yeniləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357641,6 +358467,12 @@ "proPlanRenewDesktopStore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} üçün qeydiyyatdan keçdiyiniz {platform_account} hesabınızla {platform_store} veb saytında planınızı yeniləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357652,6 +358484,12 @@ "proPlanRenewStart" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güclü {app_pro} özəlliklərini yenidən istifadə etməyə başlamaq üçün {app_pro} planınızı yeniləyin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357663,6 +358501,12 @@ "proPlanRenewSupport" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız yeniləndi! {network_name} dəstək verdiyiniz üçün təşəkkürlər." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357674,6 +358518,12 @@ "proPlanRestored" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı bərpa edildi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357685,6 +358535,12 @@ "proPlanRestoredDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} üçün yararlı bir plan aşkarlandı və {pro} statusunuz bərpa edildi!" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357696,6 +358552,12 @@ "proPlanSignUp" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357707,6 +358569,12 @@ "proPriceOneMonth" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 ay - {monthly_price}/ay" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357718,6 +358586,12 @@ "proPriceThreeMonths" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 ay - {monthly_price}/ay" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357729,6 +358603,12 @@ "proPriceTwelveMonths" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 ay - {monthly_price}/ay" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357740,6 +358620,12 @@ "proRefundDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getməyinizə məyus olduq. Geri ödəmə tələb etməzdən əvvəl bilməli olduğunuz şeylər." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357751,6 +358637,12 @@ "proRefunding" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} geri ödəməsi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357762,6 +358654,12 @@ "proRefundingDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planları üçün geri ödəmələr yalnız {platform_store} Mağazası vasitəsilə {platform_account} tərəfindən həyata keçirilir.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357773,6 +358671,12 @@ "proRefundNextSteps" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} hazırda geri ödəniş tələbinizi emal edir. Bu, adətən 24-48 saat çəkir. Onların qərarından asılı olaraq, {app_name} tətbiqində {pro} statusunuzun dəyişdiyini görə bilərsiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357784,6 +358688,12 @@ "proRefundRequestSessionSupport" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

    Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəniş tələbinizi göndərin.

    {app_name} Dəstək komandası, geri ödəniş tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357795,6 +358705,12 @@ "proRefundRequestStorePolicies" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəniş tələbiniz yalnız {platform_account} veb saytında {platform_account} hesabı üzərindən icra olunacaq.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357806,6 +358722,12 @@ "proRefundSupport" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəmə tələbinizlə bağlı daha çox güncəlləmə üçün lütfən {platform_account} ilə əlaqə saxlayın. {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz.

    {platform_store} Geri ödəmə dəstəyi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357817,6 +358739,12 @@ "proRequestedRefund" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəmə tələb edildi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357965,6 +358893,12 @@ "proSettings" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} ayarları" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357976,6 +358910,12 @@ "proStats" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikalarınız" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357987,6 +358927,12 @@ "proStatsTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikaları, bu cihazdakı istifadəni əks-etdirir və əlaqələndirilmiş cihazlarda fərqli görünə bilər." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -357998,6 +358944,12 @@ "proSupportDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınızla bağlı kömək lazımdır? Dəstək komandamıza müraciət edin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358009,6 +358961,12 @@ "proTosPrivacy" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəlləyərək, {app_pro} Xidmət Şərtləri {icon} və Məxfilik Siyasəti {icon} ilə razılaşırsınız" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358020,6 +358978,12 @@ "proUnlimitedPins" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitsiz sancma" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358031,6 +358995,12 @@ "proUnlimitedPinsDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitsiz sancılmış danışıqla bütün söhbətlərinizi təşkil edin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358042,6 +359012,12 @@ "proUpdatePlanDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırda {current_plan} Planı üzərindəsiniz. {selected_plan} Planınana keçmək istədiyinizə əminsiniz?

    Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} {pro} erişimi üçün avtomatik yenilənəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358053,6 +359029,12 @@ "proUpdatePlanExpireDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planınız {date} tarixində bitəcək.

    Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} Pro erişimi üçün avtomatik olaraq yenilənəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -366256,6 +367238,12 @@ "recoveryPasswordDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesabınızı yeni cihazlara yükləmək üçün geri qaytarma parolunuzu istifadə edin.

    Geri qaytarma parolunuz olmadan hesabınız geri qaytarıla bilməz. Parolu təhlükəsiz və etibarlı yerdə saxladığınıza əmin olun və heç kəslə paylaşmayın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -370272,6 +371260,12 @@ "recoveryPasswordHidePermanentlyDescription2" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma parolunuzu bu cihazdan həmişəlik gizlətmək istədiyinizə əminsiniz?

    Bunun geri dönüşü yoxdur." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -371720,6 +372714,12 @@ "recoveryPasswordView" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma paroluna bax" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -371731,6 +372731,12 @@ "recoveryPasswordVisibility" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma parolu görünməsi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -372879,6 +373885,12 @@ "refundPlanNonOriginatorApple" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda fərqli {platform_account} vasitəsilə {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -374428,6 +375440,12 @@ "removePasswordModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün hazırkı parolunuzu silin. Daxili olaraq saxlanılmış verilər, cihazınızda saxlanılan təsadüfi yaradılmış açarla təkrar şifrələnəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -374439,6 +375457,12 @@ "renew" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenilə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -374929,6 +375953,12 @@ "requestRefund" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəmə tələb et" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -381414,6 +382444,28 @@ } } }, + "screenshotProtectionDescriptionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conceal the {app_name} window in screenshots taken on this device." + } + } + } + }, + "screenshotProtectionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screenshot Protection" + } + } + } + }, "screenshotTaken" : { "extractionState" : "manual", "localizations" : { @@ -395972,6 +397024,12 @@ "sessionProBeta" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -397581,6 +398639,12 @@ "setPasswordModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün bir parol təyin edin. Daxili olaraq saxlanılmış verilər, bu parolla şifrələnəcək. {app_name} tətbiqini hər başlatdıqda, sizdən bu parolu daxil etməyiniz istənəcək." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -397592,6 +398656,12 @@ "settingsCannotChangeDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayar güncəllənə bilmir" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -398079,9 +399149,494 @@ } } }, + "settingsScreenSecurityDesktop" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skermveiligheid" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أمان الشاشة" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran güvənliyi" + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "سکرین سیکورٹی" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бяспека экрану" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигурност на екрана" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "স্ক্রীন সিকিউরিটি" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguretat de pantalla" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zabezpečení obrazovky" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diogelu'r sgrin" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skærmsikkerhed" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildschirmschutz" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ασφάλεια Οθόνης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screen Security" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrana sekurigo" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad de pantalla" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad de pantalla" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekraani turvalisus" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantailaren Segurtasuna" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امنیت صفحه نمایش" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Näytön suojaus" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screen Security" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sécurité d'écran" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguranza da pantalla" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tsaron Allo" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אבטחת מסך" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन सुरक्षा" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigurnost zaslona" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Képernyőbiztonság" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Էկրանի անվտանգություն" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keamanan Layar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicurezza Schermo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリーンセキュリティ" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "ეკრანის დაცვა" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "សុវត្ថិភាពអេក្រង់" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪರದೆಯ ಭದ್ರತೆ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "화면 보안" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پاراستنی پردە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parastina Ekranê" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obukuumi bwa ekikola ekiriko akabonero" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrano saugumas" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrāna drošība" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екранска Безбедност" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дэлгэцийн аюулгүй байдал" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keselamatan Skrin" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "မျက်နှာပြင် လုံခြုံရေး" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermsikkerhet" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermsikkerhet" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन सुरक्षा" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scherm beveiliging" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermtryggleik" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rikuripa pakallayachina" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਸਕ੍ਰੀਨ ਸੁਰੱਖਿਆ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona ekranu" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د سکرین امنیت" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Segurança de Tela" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Segurança de ecrã" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Securitate ecran" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита экрана" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigurnost ekrana" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "තිර ආරක්ෂාව" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zabezpečenie obrazovky" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Varnost zaslona" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siguri ekrani" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Безбедност екрана" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezbednost ekrana" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skärmsäkerhet" + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usalama wa Skrini" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "திரை பாதுகாப்பு" + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "స్క్రీన్ భద్రత" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ความปลอดภัยหน้าจอ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran Güvenliği" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Безпека перегляду" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "سکرین سیکیورٹی" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran xavfsizligi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "An ninh màn hình" + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukhuseleko lweSikrini" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏幕安全性" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "螢幕安全性" + } + } + } + }, "settingsStartCategoryDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açılış" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -402390,6 +403945,12 @@ "spellChecker" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yazı yoxlanışı" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -402880,6 +404441,12 @@ "strength" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gücü" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -402891,6 +404458,12 @@ "supportDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Problemlə üzləşmisiniz? Kömək məqalələrini oxuyun, ya da {app_name} Dəstək ilə bir sorğu açın." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -405482,6 +407055,12 @@ "themePreview" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema önizləməsi" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -405493,6 +407072,12 @@ "theReturn" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qayıt" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -405766,6 +407351,12 @@ "translate" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tərcümə et" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -405777,6 +407368,12 @@ "tray" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sini" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -412499,6 +414096,12 @@ "updatePlan" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planı güncəllə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -412510,6 +414113,12 @@ "updatePlanTwo" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planınızı güncəlləməyin iki yolu var:" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -412521,6 +414130,12 @@ "updateProfileInformation" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil məlumatlarını güncəllə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -412532,6 +414147,12 @@ "updateProfileInformationDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran adınız və ekran şəkliniz bütün danışıqlarda görünür." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413022,6 +414643,12 @@ "updates" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəlləmələr" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413997,6 +415624,12 @@ "updating" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəllənir..." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -416540,6 +418173,12 @@ "urlOpenDescriptionAlternative" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keçidlər, brauzerinizdə açılacaq." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -417030,6 +418669,12 @@ "viaStoreWebsite" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_store} veb saytı vasitəsilə" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -417041,6 +418686,12 @@ "viaStoreWebsiteDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qeydiyyatdan keçərkən istifadə etdiyiniz {platform_account} hesabı ilə {platform_store} veb saytı üzərindən planınızı dəyişdirin." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -421685,6 +423336,12 @@ "yourRecoveryPassword" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma parolunuz" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -421696,6 +423353,12 @@ "zoomFactor" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Böyütmə amili" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -421707,6 +423370,12 @@ "zoomFactorDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mətnin və vizual elementlərin ölçüsünü ayarla." + } + }, "en" : { "stringUnit" : { "state" : "translated", From 5109ec037d70cfd3568e7a9c37c385afa0b96daf Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 8 Sep 2025 08:14:46 +0800 Subject: [PATCH 180/244] Updated font for call cells Adjusted call vertical padding --- .../Message Cells/CallMessageCell.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 13566d5769..78b834ff6c 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -10,6 +10,8 @@ final class CallMessageCell: MessageCell { private static let iconSize: CGFloat = 16 private static let timerViewSize: CGFloat = 16 private static let inset = Values.mediumSpacing + private static let verticalInset = Values.mediumSmallSpacing // Added 4pt vertical to align margins with other bubbles with author padding in `VisibleMessageCell` + private static let horizontalInset = Values.mediumSmallSpacing private static let margin = UIScreen.main.bounds.width * 0.1 private var isHandlingLongPress: Bool = false @@ -48,7 +50,7 @@ final class CallMessageCell: MessageCell { private lazy var label: UILabel = { let result: UILabel = UILabel() - result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.font = .systemFont(ofSize: Values.mediumFontSize) result.themeTextColor = .textPrimary result.textAlignment = .center result.lineBreakMode = .byWordWrapping @@ -63,28 +65,28 @@ final class CallMessageCell: MessageCell { result.layer.cornerRadius = 18 result.addSubview(label) - label.pin(.top, to: .top, of: result, withInset: CallMessageCell.inset) + label.pin(.top, to: .top, of: result, withInset: CallMessageCell.verticalInset) label.pin( .left, to: .left, of: result, - withInset: ((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) + withInset: ((CallMessageCell.horizontalInset * 2) + infoImageView.bounds.size.width) ) label.pin( .right, to: .right, of: result, - withInset: -((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) + withInset: -((CallMessageCell.horizontalInset * 2) + infoImageView.bounds.size.width) ) - label.pin(.bottom, to: .bottom, of: result, withInset: -CallMessageCell.inset) + label.pin(.bottom, to: .bottom, of: result, withInset: -CallMessageCell.verticalInset) result.addSubview(iconImageView) iconImageView.center(.vertical, in: result) - iconImageView.pin(.left, to: .left, of: result, withInset: CallMessageCell.inset) + iconImageView.pin(.left, to: .left, of: result, withInset: CallMessageCell.horizontalInset) result.addSubview(infoImageView) infoImageView.center(.vertical, in: result) - infoImageView.pin(.right, to: .right, of: result, withInset: -CallMessageCell.inset) + infoImageView.pin(.right, to: .right, of: result, withInset: -CallMessageCell.horizontalInset) return result }() From 89832abf055e9aa492841ccfc8176c220d2b0def Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 8 Sep 2025 09:56:43 +0800 Subject: [PATCH 181/244] Remove extra 4pt vertical padding on call cell --- Session/Conversations/Message Cells/CallMessageCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 78b834ff6c..de80fef7c1 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -10,7 +10,7 @@ final class CallMessageCell: MessageCell { private static let iconSize: CGFloat = 16 private static let timerViewSize: CGFloat = 16 private static let inset = Values.mediumSpacing - private static let verticalInset = Values.mediumSmallSpacing // Added 4pt vertical to align margins with other bubbles with author padding in `VisibleMessageCell` + private static let verticalInset = Values.smallSpacing private static let horizontalInset = Values.mediumSmallSpacing private static let margin = UIScreen.main.bounds.width * 0.1 From aff16e99c603ce851f75c90453aa924c6f646680 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 9 Sep 2025 10:42:55 +0800 Subject: [PATCH 182/244] Fix failing test due to `unexpected ',' separator` --- Session/Shared/Types/SessionCell+Accessory.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index f27e6e19db..887a554092 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -41,7 +41,7 @@ public extension SessionCell.Accessory { customTint: ThemeValue? = nil, shouldFill: Bool = false, pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], - accessibility: Accessibility? = nil, + accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( icon: icon, @@ -60,7 +60,7 @@ public extension SessionCell.Accessory { customTint: ThemeValue? = nil, shouldFill: Bool = false, pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], - accessibility: Accessibility? = nil, + accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( icon: nil, From 84ed82760f857e69b82bb21b813505dac2d78fc4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Sep 2025 12:45:02 +1000 Subject: [PATCH 183/244] Fixed a layout issue, removed unused code --- Session.xcodeproj/project.pbxproj | 4 - .../Closed Groups/EditGroupViewModel.swift | 3 +- .../Settings/PrivacySettingsViewModel.swift | 1 - Session/Settings/SettingsViewModel.swift | 3 +- .../Shared/SessionTableViewController.swift | 43 -------- Session/Shared/Types/EditableState.swift | 76 ------------- .../Shared/Types/SessionCell+Styling.swift | 2 - .../Views/SessionCell+AccessoryView.swift | 19 ++-- Session/Shared/Views/SessionCell.swift | 102 ------------------ .../SessionHighlightingBackgroundLabel.swift | 5 + 10 files changed, 18 insertions(+), 240 deletions(-) delete mode 100644 Session/Shared/Types/EditableState.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c3c86fd017..6e907cba39 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -466,7 +466,6 @@ FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */; }; FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; }; FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; }; - FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */; }; FD12A83F2AD63BDF00EEBA0D /* Navigatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */; }; FD12A8412AD63BEA00EEBA0D /* NavigatableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */; }; FD12A8432AD63BF600EEBA0D /* ObservableTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */; }; @@ -1841,7 +1840,6 @@ FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionListViewModel.swift; sourceTree = ""; }; FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feature+ServiceNetwork.swift"; sourceTree = ""; }; FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationSpec.swift; sourceTree = ""; }; - FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableState.swift; sourceTree = ""; }; FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigatable.swift; sourceTree = ""; }; FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatableState.swift; sourceTree = ""; }; FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableTableSource.swift; sourceTree = ""; }; @@ -4455,7 +4453,6 @@ isa = PBXGroup; children = ( FD71164928E3EA5B00B47552 /* DismissType.swift */, - FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */, FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */, FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */, FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */, @@ -6740,7 +6737,6 @@ FDEF57222C3CF03D00131302 /* (null) in Sources */, 7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */, FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */, - FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */, FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */, C328255225CA64470062D0A7 /* ContextMenuVC+ActionView.swift in Sources */, C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */, diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 01916f533c..e21687aed1 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -10,10 +10,9 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, EditableStateHolder, ObservableTableSource { +class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() private let selectedIdsSubject: CurrentValueSubject<(name: String, ids: Set), Never> = CurrentValueSubject(("", [])) diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 86f4dc37b7..d3d6b80444 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -12,7 +12,6 @@ import SessionUtilitiesKit class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() private let shouldShowCloseButton: Bool diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 2220e6279f..f8fa6a6a42 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -285,8 +285,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl title: SessionCell.TextInfo( state.profile.displayName(), font: .titleLarge, - alignment: .center, - interaction: .editable + alignment: .center ), trailingAccessory: .icon( .pencil, diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index 439421c5e3..8e0119bd9a 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -362,34 +362,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa disposables: &disposables ) - (viewModel as? ErasedEditableStateHolder)?.isEditing - .receive(on: DispatchQueue.main) - .sink { [weak self, weak tableView] isEditing in - UIView.animate(withDuration: 0.25) { - self?.setEditing(isEditing, animated: true) - - tableView?.visibleCells - .compactMap { $0 as? SessionCell } - .filter { $0.interactionMode == .editable || $0.interactionMode == .alwaysEditing } - .enumerated() - .forEach { index, cell in - cell.update( - isEditing: (isEditing || cell.interactionMode == .alwaysEditing), - becomeFirstResponder: ( - isEditing && - index == 0 && - cell.interactionMode != .alwaysEditing - ), - animated: true - ) - } - - tableView?.beginUpdates() - tableView?.endUpdates() - } - } - .store(in: &disposables) - viewModel.bannerInfo .receive(on: DispatchQueue.main) .sink { [weak self] info in @@ -489,21 +461,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa }, using: viewModel.dependencies ) - cell.update( - isEditing: (self.isEditing || (info.title?.interaction == .alwaysEditing)), - becomeFirstResponder: false, - animated: false - ) - - switch viewModel { - case let editableStateHolder as ErasedEditableStateHolder: - cell.textPublisher - .sink(receiveValue: { [weak editableStateHolder] text in - editableStateHolder?.textChanged(text, for: info.id) - }) - .store(in: &cell.disposables) - default: break - } case (let cell as FullConversationCell, let threadInfo as SessionCell.Info): cell.accessibilityIdentifier = info.accessibility?.identifier diff --git a/Session/Shared/Types/EditableState.swift b/Session/Shared/Types/EditableState.swift deleted file mode 100644 index 873e9da2b5..0000000000 --- a/Session/Shared/Types/EditableState.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine -import DifferenceKit -import SessionUtilitiesKit - -// MARK: - EditableStateHolder - -public protocol EditableStateHolder: AnyObject, TableData, ErasedEditableStateHolder { - var editableState: EditableState { get } -} - -public extension EditableStateHolder { - var textChanged: AnyPublisher<(text: String?, item: TableItem), Never> { editableState.textChanged } - - func setIsEditing(_ isEditing: Bool) { - editableState._isEditing.send(isEditing) - } - - func textChanged(_ text: String?, for item: TableItem) { - editableState._textChanged.send((text, item)) - } -} - -// MARK: - ErasedEditableStateHolder - -public protocol ErasedEditableStateHolder: AnyObject { - var isEditing: AnyPublisher { get } - - func setIsEditing(_ isEditing: Bool) - func textChanged(_ text: String?, for item: Item) -} - -public extension ErasedEditableStateHolder { - var isEditing: AnyPublisher { Just(false).eraseToAnyPublisher() } - - func setIsEditing(_ isEditing: Bool) {} - func textChanged(_ text: String?, for item: Item) {} -} - -public extension ErasedEditableStateHolder where Self: EditableStateHolder { - var isEditing: AnyPublisher { editableState.isEditing } - - func setIsEditing(_ isEditing: Bool) { - editableState._isEditing.send(isEditing) - } - - func textChanged(_ text: String?, for item: Item) { - guard let convertedItem: TableItem = item as? TableItem else { return } - - editableState._textChanged.send((text, convertedItem)) - } -} - -// MARK: - EditableState - -public struct EditableState { - let isEditing: AnyPublisher - let textChanged: AnyPublisher<(text: String?, item: TableItem), Never> - - // MARK: - Internal Variables - - fileprivate let _isEditing: CurrentValueSubject = CurrentValueSubject(false) - fileprivate let _textChanged: PassthroughSubject<(text: String?, item: TableItem), Never> = PassthroughSubject() - - // MARK: - Initialization - - init() { - self.isEditing = _isEditing - .removeDuplicates() - .shareReplay(1) - self.textChanged = _textChanged - .eraseToAnyPublisher() - } -} diff --git a/Session/Shared/Types/SessionCell+Styling.swift b/Session/Shared/Types/SessionCell+Styling.swift index a83a09a0da..8df10aa317 100644 --- a/Session/Shared/Types/SessionCell+Styling.swift +++ b/Session/Shared/Types/SessionCell+Styling.swift @@ -9,9 +9,7 @@ public extension SessionCell { struct TextInfo: Hashable, Equatable { public enum Interaction: Hashable, Equatable { case none - case editable case copy - case alwaysEditing case expandable } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 8d7c9ee551..5b16c44ffc 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -55,7 +55,6 @@ extension SessionCell { accessory: accessory, tintColor: tintColor, isEnabled: isEnabled, - maxContentWidth: maxContentWidth, using: dependencies ) return @@ -82,7 +81,6 @@ extension SessionCell { accessory: accessory, tintColor: tintColor, isEnabled: isEnabled, - maxContentWidth: maxContentWidth, using: dependencies ) @@ -163,11 +161,13 @@ extension SessionCell { return createIconView(using: dependencies) case is SessionCell.AccessoryConfig.Toggle: return createToggleView() - case is SessionCell.AccessoryConfig.DropDown: return createDropDownView() + case is SessionCell.AccessoryConfig.DropDown: + return createDropDownView(maxContentWidth: maxContentWidth) + case is SessionCell.AccessoryConfig.Radio: return createRadioView() case is SessionCell.AccessoryConfig.HighlightingBackgroundLabel: - return createHighlightingBackgroundLabelView() + return createHighlightingBackgroundLabelView(maxContentWidth: maxContentWidth) case is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio: return createHighlightingBackgroundLabelAndRadioView() @@ -227,7 +227,6 @@ extension SessionCell { accessory: Accessory, tintColor: ThemeValue, isEnabled: Bool, - maxContentWidth: CGFloat, using dependencies: Dependencies ) { switch accessory { @@ -377,7 +376,7 @@ extension SessionCell { // MARK: -- DropDown - private func createDropDownView() -> UIView { + private func createDropDownView(maxContentWidth: CGFloat) -> UIView { let result: UIStackView = UIStackView() result.translatesAutoresizingMaskIntoConstraints = false result.axis = .horizontal @@ -397,6 +396,7 @@ extension SessionCell { label.themeTextColor = .textPrimary label.setContentHugging(to: .required) label.setCompressionResistance(to: .required) + label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width label.numberOfLines = 0 result.addArrangedSubview(imageView) @@ -511,8 +511,11 @@ extension SessionCell { // MARK: -- HighlightingBackgroundLabel - private func createHighlightingBackgroundLabelView() -> UIView { - return SessionHighlightingBackgroundLabel() + private func createHighlightingBackgroundLabelView(maxContentWidth: CGFloat) -> UIView { + let result: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel() + result.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width + + return result } private func layoutHighlightingBackgroundLabelView(_ view: UIView?) { diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 47bdebb394..cb1bcdfce2 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -10,7 +10,6 @@ import SessionUtilitiesKit public class SessionCell: UITableViewCell { public static let cornerRadius: CGFloat = 17 - private var isEditingTitle = false public private(set) var interactionMode: SessionCell.TextInfo.Interaction = .none public var lastTouchLocation: UITouch? private var shouldHighlightTitle: Bool = true @@ -34,10 +33,6 @@ public class SessionCell: UITableViewCell { private lazy var contentStackViewHorizontalCenterConstraint: NSLayoutConstraint = contentStackView.center(.horizontal, in: cellBackgroundView) private lazy var contentStackViewWidthConstraint: NSLayoutConstraint = contentStackView.set(.width, lessThanOrEqualTo: .width, of: cellBackgroundView) private lazy var leadingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leadingAccessoryView) - private lazy var titleTextFieldLeadingConstraint: NSLayoutConstraint = titleTextField.pin(.leading, to: .leading, of: cellBackgroundView) - private lazy var titleTextFieldTrailingConstraint: NSLayoutConstraint = titleTextField.pin(.trailing, to: .trailing, of: cellBackgroundView) - private lazy var titleMinHeightConstraint: NSLayoutConstraint = titleStackView.heightAnchor - .constraint(greaterThanOrEqualTo: titleTextField.heightAnchor) private lazy var trailingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: trailingAccessoryView) private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView.set(.width, to: .width, of: trailingAccessoryView) @@ -109,17 +104,6 @@ public class SessionCell: UITableViewCell { return result }() - fileprivate let titleTextField: UITextField = { - let textField: SNTextField = SNTextField(placeholder: "", usesDefaultHeight: false) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.textAlignment = .center - textField.alpha = 0 - textField.isHidden = true - textField.set(.height, to: Values.largeButtonHeight) - - return textField - }() - private let subtitleLabel: SRCopyableLabel = { let result: SRCopyableLabel = SRCopyableLabel() result.translatesAutoresizingMaskIntoConstraints = false @@ -195,8 +179,6 @@ public class SessionCell: UITableViewCell { titleStackView.addArrangedSubview(titleLabel) titleStackView.addArrangedSubview(subtitleLabel) - cellBackgroundView.addSubview(titleTextField) - setupLayout() } @@ -215,8 +197,6 @@ public class SessionCell: UITableViewCell { contentStackViewTopConstraint.isActive = true contentStackViewBottomConstraint.isActive = true - titleTextField.center(.vertical, in: titleLabel) - botSeparatorLeadingConstraint = botSeparator.pin(.leading, to: .leading, of: cellBackgroundView) botSeparatorTrailingConstraint = botSeparator.pin(.trailing, to: .trailing, of: cellBackgroundView) botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView) @@ -297,7 +277,6 @@ public class SessionCell: UITableViewCell { public override func prepareForReuse() { super.prepareForReuse() - isEditingTitle = false interactionMode = .none shouldHighlightTitle = true accessibilityIdentifier = nil @@ -315,18 +294,12 @@ public class SessionCell: UITableViewCell { contentStackViewTrailingConstraint.isActive = false contentStackViewHorizontalCenterConstraint.isActive = false contentStackViewWidthConstraint.isActive = false - titleMinHeightConstraint.isActive = false leadingAccessoryView.prepareForReuse() leadingAccessoryView.alpha = 1 leadingAccessoryFillConstraint.isActive = false titleLabel.text = "" titleLabel.themeTextColor = .textPrimary titleLabel.alpha = 1 - titleTextField.text = "" - titleTextField.textAlignment = .center - titleTextField.themeTextColor = .textPrimary - titleTextField.isHidden = true - titleTextField.alpha = 0 subtitleLabel.isUserInteractionEnabled = false subtitleLabel.attributedText = nil subtitleLabel.themeTextColor = .textPrimary @@ -418,16 +391,6 @@ public class SessionCell: UITableViewCell { return -(leadingFitToEdge || trailingFitToEdge ? 0 : Values.mediumSpacing) }() - titleTextFieldLeadingConstraint.constant = { - guard info.styling.backgroundStyle != .noBackground else { return 0 } - - return (leadingFitToEdge ? 0 : Values.mediumSpacing) - }() - titleTextFieldTrailingConstraint.constant = { - guard info.styling.backgroundStyle != .noBackground else { return 0 } - - return -(trailingFitToEdge ? 0 : Values.mediumSpacing) - }() // Styling and positioning let defaultEdgePadding: CGFloat @@ -567,12 +530,6 @@ public class SessionCell: UITableViewCell { titleLabel.accessibilityIdentifier = info.title?.accessibility?.identifier titleLabel.accessibilityLabel = info.title?.accessibility?.label titleLabel.isHidden = (info.title == nil) - titleTextField.text = info.title?.text - titleTextField.textAlignment = (info.title?.textAlignment ?? .left) - titleTextField.placeholder = info.title?.editingPlaceholder - titleTextField.isHidden = (info.title == nil) - titleTextField.accessibilityIdentifier = info.title?.accessibility?.identifier - titleTextField.accessibilityLabel = info.title?.accessibility?.label subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) subtitleLabel.font = info.subtitle?.font subtitleLabel.themeAttributedText = info.subtitle.map { subtitle -> ThemedAttributedString? in @@ -602,57 +559,17 @@ public class SessionCell: UITableViewCell { ) } - public func update(isEditing: Bool, becomeFirstResponder: Bool, animated: Bool) { - // Note: We set 'isUserInteractionEnabled' based on the 'info.isEditable' flag - // so can use that to determine whether this element can become editable - guard interactionMode == .editable || interactionMode == .alwaysEditing else { return } - - self.isEditingTitle = isEditing - - let changes = { [weak self] in - self?.titleLabel.alpha = (isEditing ? 0 : 1) - self?.titleTextField.alpha = (isEditing ? 1 : 0) - self?.leadingAccessoryView.alpha = (isEditing ? 0 : 1) - self?.trailingAccessoryView.alpha = (isEditing ? 0 : 1) - self?.titleMinHeightConstraint.isActive = isEditing - } - let completion: (Bool) -> Void = { [weak self] complete in - self?.titleTextField.text = self?.originalInputValue - } - - if animated { - UIView.animate(withDuration: 0.25, animations: changes, completion: completion) - } - else { - changes() - completion(true) - } - - if isEditing && becomeFirstResponder { - titleTextField.becomeFirstResponder() - } - else if !isEditing { - titleTextField.resignFirstResponder() - } - } - // MARK: - Interaction public override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) - // When editing disable the highlighted state changes (would result in UI elements - // reappearing otherwise) - guard !self.isEditingTitle else { return } - // If the 'cellSelectedBackgroundView' is hidden then there is no background so we // should update the titleLabel to indicate the highlighted state if cellSelectedBackgroundView.isHidden && shouldHighlightTitle { // Note: We delay the "unhighlight" of the titleLabel so that the transition doesn't // conflict with the transition into edit mode DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak self] in - guard self?.isEditingTitle == false else { return } - self?.titleLabel.alpha = (highlighted ? 0.8 : 1) } } @@ -675,22 +592,3 @@ public class SessionCell: UITableViewCell { lastTouchLocation = touches.first } } - -// MARK: - Compose - -extension CombineCompatible where Self: SessionCell { - var textPublisher: AnyPublisher { - return self.titleTextField.publisher(for: [.editingChanged, .editingDidEnd]) - .handleEvents( - receiveOutput: { [weak self] textField in - // When editing the text update the 'accessibilityLabel' of the cell to match - // the text - let targetText: String? = (textField.isEditing ? textField.text : self?.titleLabel.text) - self?.accessibilityLabel = (targetText ?? self?.accessibilityLabel) - } - ) - .filter { $0.isEditing } // Don't bother sending events for 'editingDidEnd' - .map { textField -> String in (textField.text ?? "") } - .eraseToAnyPublisher() - } -} diff --git a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift index 7e6d6ead22..6bbe3022cb 100644 --- a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift +++ b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift @@ -15,6 +15,11 @@ public class SessionHighlightingBackgroundLabel: UIView { set { label.themeTextColor = newValue } } + var preferredMaxLayoutWidth: CGFloat { + get { label.preferredMaxLayoutWidth - (Values.smallSpacing * 2) } + set { label.preferredMaxLayoutWidth = (newValue - (Values.smallSpacing * 2)) } + } + // MARK: - Components private let label: UILabel = { From 145ad5be0a6b705fd9c46a9d1e9aee9b250dffc0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Sep 2025 12:49:06 +1000 Subject: [PATCH 184/244] Added the logic to `createHighlightingBackgroundLabelAndRadioView` --- Session/Shared/Views/SessionCell+AccessoryView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 5b16c44ffc..d601eb5bf5 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -170,7 +170,7 @@ extension SessionCell { return createHighlightingBackgroundLabelView(maxContentWidth: maxContentWidth) case is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio: - return createHighlightingBackgroundLabelAndRadioView() + return createHighlightingBackgroundLabelAndRadioView(maxContentWidth: maxContentWidth) case is SessionCell.AccessoryConfig.DisplayPicture: return createDisplayPictureView() case is SessionCell.AccessoryConfig.Search: return createSearchView() @@ -544,10 +544,11 @@ extension SessionCell { // MARK: -- HighlightingBackgroundLabelAndRadio - private func createHighlightingBackgroundLabelAndRadioView() -> UIView { + private func createHighlightingBackgroundLabelAndRadioView(maxContentWidth: CGFloat) -> UIView { let result: UIView = UIView() let label: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel() let radio: UIView = createRadioView() + label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width result.addSubview(label) result.addSubview(radio) From 16db95c42ef7fad922c7d28c7bbd36ccffa67c27 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Sep 2025 13:52:11 +1000 Subject: [PATCH 185/244] Fixed breaking SessionHeaderView and Separator constraints --- Session/Shared/Views/SessionHeaderView.swift | 55 ++++++++----------- SessionUIKit/Components/Separator.swift | 19 ++++--- .../Utilities/UIView+Constraints.swift | 22 +++++--- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/Session/Shared/Views/SessionHeaderView.swift b/Session/Shared/Views/SessionHeaderView.swift index ebe88b1b95..b845d027bb 100644 --- a/Session/Shared/Views/SessionHeaderView.swift +++ b/Session/Shared/Views/SessionHeaderView.swift @@ -6,14 +6,10 @@ import SessionUIKit class SessionHeaderView: UITableViewHeaderFooterView { // MARK: - UI - private lazy var titleLabelConstraints: [NSLayoutConstraint] = [ - titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing), - titleLabel.pin(.bottom, to: .bottom, of: self, withInset: -Values.mediumSpacing) - ] - private lazy var titleLabelLeadingConstraint: NSLayoutConstraint = titleLabel.pin(.leading, to: .leading, of: self) - private lazy var titleLabelTrailingConstraint: NSLayoutConstraint = titleLabel.pin(.trailing, to: .trailing, of: self) - private lazy var titleSeparatorLeadingConstraint: NSLayoutConstraint = titleSeparator.pin(.leading, to: .leading, of: self) - private lazy var titleSeparatorTrailingConstraint: NSLayoutConstraint = titleSeparator.pin(.trailing, to: .trailing, of: self) + private var titleLabelLeadingConstraint: NSLayoutConstraint? + private var titleLabelTrailingConstraint: NSLayoutConstraint? + private var titleSeparatorLeadingConstraint: NSLayoutConstraint? + private var titleSeparatorTrailingConstraint: NSLayoutConstraint? private let titleLabel: UILabel = { let result: UILabel = UILabel() @@ -51,9 +47,9 @@ class SessionHeaderView: UITableViewHeaderFooterView { self.backgroundView = UIView() self.backgroundView?.themeBackgroundColor = .backgroundPrimary - addSubview(titleLabel) - addSubview(titleSeparator) - addSubview(loadingIndicator) + contentView.addSubview(titleLabel) + contentView.addSubview(titleSeparator) + contentView.addSubview(loadingIndicator) setupLayout() } @@ -63,12 +59,18 @@ class SessionHeaderView: UITableViewHeaderFooterView { } private func setupLayout() { - titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing) - titleLabel.pin(.bottom, to: .bottom, of: self, withInset: Values.mediumSpacing) - titleLabel.center(.vertical, in: self) + titleLabel.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing) + titleLabelLeadingConstraint = titleLabel.pin(.leading, to: .leading, of: contentView) + titleLabelTrailingConstraint = titleLabel.pin(.trailing, to: .trailing, of: contentView) + titleLabel + .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.mediumSpacing) + .setting(priority: .defaultHigh) - titleSeparator.center(.vertical, in: self) - loadingIndicator.center(in: self) + titleSeparator.center(.vertical, in: contentView) + titleSeparatorLeadingConstraint = titleSeparator.pin(.leading, to: .leading, of: contentView) + titleSeparatorTrailingConstraint = titleSeparator.pin(.trailing, to: .trailing, of: contentView) + + loadingIndicator.center(in: contentView) } // MARK: - Content @@ -79,14 +81,6 @@ class SessionHeaderView: UITableViewHeaderFooterView { titleLabel.isHidden = true titleSeparator.isHidden = true loadingIndicator.isHidden = true - - titleLabelLeadingConstraint.isActive = false - titleLabelTrailingConstraint.isActive = false - titleLabelConstraints.forEach { $0.isActive = false } - - titleSeparator.center(.vertical, in: self) - titleSeparatorLeadingConstraint.isActive = false - titleSeparatorTrailingConstraint.isActive = false } public func update( @@ -94,24 +88,19 @@ class SessionHeaderView: UITableViewHeaderFooterView { style: SessionTableSectionStyle = .titleRoundedContent ) { let titleIsEmpty: Bool = (title ?? "").isEmpty + titleLabelLeadingConstraint?.constant = style.edgePadding + titleLabelTrailingConstraint?.constant = -style.edgePadding + titleSeparatorLeadingConstraint?.constant = style.edgePadding + titleSeparatorTrailingConstraint?.constant = -style.edgePadding switch style { case .titleRoundedContent, .titleEdgeToEdgeContent, .titleNoBackgroundContent: titleLabel.text = title titleLabel.isHidden = titleIsEmpty - titleLabelLeadingConstraint.constant = style.edgePadding - titleLabelTrailingConstraint.constant = -style.edgePadding - titleLabelLeadingConstraint.isActive = !titleIsEmpty - titleLabelTrailingConstraint.isActive = !titleIsEmpty - titleLabelConstraints.forEach { $0.isActive = true } case .titleSeparator: titleSeparator.update(title: title) titleSeparator.isHidden = false - titleSeparatorLeadingConstraint.constant = style.edgePadding - titleSeparatorTrailingConstraint.constant = -style.edgePadding - titleSeparatorLeadingConstraint.isActive = !titleIsEmpty - titleSeparatorTrailingConstraint.isActive = !titleIsEmpty case .none, .padding: break case .loadMore: loadingIndicator.isHidden = false diff --git a/SessionUIKit/Components/Separator.swift b/SessionUIKit/Components/Separator.swift index d35add8fda..3230665d81 100644 --- a/SessionUIKit/Components/Separator.swift +++ b/SessionUIKit/Components/Separator.swift @@ -68,17 +68,20 @@ public final class Separator: UIView { addSubview(rightLine) addSubview(titleLabel) - titleLabel.center(.horizontal, in: self) - titleLabel.center(.vertical, in: self) - roundedLine.pin(.top, to: .top, of: self) - roundedLine.pin(.top, to: .top, of: titleLabel, withInset: -6) - roundedLine.pin(.leading, to: .leading, of: titleLabel, withInset: -10) - roundedLine.pin(.trailing, to: .trailing, of: titleLabel, withInset: 10) - roundedLine.pin(.bottom, to: .bottom, of: titleLabel, withInset: 6) - roundedLine.pin(.bottom, to: .bottom, of: self) + titleLabel.pin(.top, to: .top, of: roundedLine, withInset: 6) + titleLabel.pin(.leading, to: .leading, of: roundedLine, withInset: 10) + titleLabel.pin(.trailing, to: .trailing, of: roundedLine, withInset: -10) + titleLabel.pin(.bottom, to: .bottom, of: roundedLine, withInset: -6) + + roundedLine.center(.horizontal, in: self) + roundedLine.center(.vertical, in: self) + roundedLine.setContentHugging(.horizontal, to: .required) + roundedLine.setCompressionResistance(.horizontal, to: .required) + leftLine.pin(.leading, to: .leading, of: self) leftLine.pin(.trailing, to: .leading, of: roundedLine) leftLine.center(.vertical, in: self) + rightLine.pin(.leading, to: .trailing, of: roundedLine) rightLine.pin(.trailing, to: .trailing, of: self) rightLine.center(.vertical, in: self) diff --git a/SessionUIKit/Utilities/UIView+Constraints.swift b/SessionUIKit/Utilities/UIView+Constraints.swift index 0570ed3325..6d905e234a 100644 --- a/SessionUIKit/Utilities/UIView+Constraints.swift +++ b/SessionUIKit/Utilities/UIView+Constraints.swift @@ -211,16 +211,22 @@ public extension UIView { } } - func pin(to view: UIView) { - [ HorizontalEdge.leading, HorizontalEdge.trailing ].forEach { pin($0, to: $0, of: view) } - [ VerticalEdge.top, VerticalEdge.bottom ].forEach { pin($0, to: $0, of: view) } + @discardableResult + func pin(to view: UIView) -> [NSLayoutConstraint] { + return [ + [ HorizontalEdge.leading, HorizontalEdge.trailing ].map { pin($0, to: $0, of: view) }, + [ VerticalEdge.top, VerticalEdge.bottom ].map { pin($0, to: $0, of: view) } + ].flatMap { $0 } } - func pin(to view: UIView, withInset inset: CGFloat) { - pin(.leading, to: .leading, of: view, withInset: inset) - pin(.top, to: .top, of: view, withInset: inset) - view.pin(.trailing, to: .trailing, of: self, withInset: inset) - view.pin(.bottom, to: .bottom, of: self, withInset: inset) + @discardableResult + func pin(to view: UIView, withInset inset: CGFloat) -> [NSLayoutConstraint] { + return [ + pin(.leading, to: .leading, of: view, withInset: inset), + pin(.top, to: .top, of: view, withInset: inset), + view.pin(.trailing, to: .trailing, of: self, withInset: inset), + view.pin(.bottom, to: .bottom, of: self, withInset: inset) + ] } @discardableResult From d19cd4257cbcd368aadd49bfb5fdf394ccc2d853 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 10 Sep 2025 11:15:42 +0800 Subject: [PATCH 186/244] Fix edit image via add text not working Fix gradient palette selector not touchable --- SessionUIKit/Style Guide/Values.swift | 2 ++ .../ImageEditorBrushViewController.swift | 3 ++- .../Image Editing/ImageEditorCanvasView.swift | 8 +++++--- .../Image Editing/ImageEditorPaletteView.swift | 12 +++++++----- .../ImageEditorTextViewController.swift | 8 ++++++-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index ffd825c2af..8f5037401f 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -31,6 +31,8 @@ public enum Values { public static let accentLineThickness = CGFloat(4) public static let searchBarHeight = CGFloat(36) + + public static let gradientPaletteWidth = CGFloat(12) public static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift index b8adcf5717..35a39417a9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift @@ -63,7 +63,8 @@ public class ImageEditorBrushViewController: OWSViewController { paletteView.delegate = self self.view.addSubview(paletteView) paletteView.center(.vertical, in: self.view, withInset: -(bottomInset / 2)) - paletteView.pin(.trailing, to: .trailing, of: self.view) + paletteView.pin(.trailing, to: .trailing, of: self.view, withInset: -Values.smallSpacing) + paletteView.set(.width, to: Values.gradientPaletteWidth) self.view.isUserInteractionEnabled = true diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift index f323b7adb9..3fd75252e0 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift @@ -501,10 +501,12 @@ public class ImageEditorCanvasView: UIView { ] ) let layer = EditorTextLayer(itemId: item.itemId) - layer.string = attributedString - layer.themeForegroundColorForced = .color(item.color.color) - layer.font = CGFont(item.font.fontName as CFString) + // Set as .strings, passing only attributed string does not display text + // `attributedString` is now only used to compute sizes + layer.string = attributedString.string + layer.font = item.font layer.fontSize = fontSize + layer.themeForegroundColorForced = .color(item.color.color) layer.isWrapped = true layer.alignmentMode = CATextLayerAlignmentMode.center // I don't think we need to enable allowsFontSubpixelQuantization diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift index 2d7ec71558..b98469d677 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift @@ -227,11 +227,13 @@ public class ImageEditorPaletteView: UIView { } addSubview(imageView) // We use an invisible margin to expand the hot area of this control. - let margin: CGFloat = 20 - imageView.pin(.top, to: .top, of: self, withInset: margin) - imageView.pin(.leading, to: .leading, of: self, withInset: -margin) - imageView.pin(.trailing, to: .trailing, of: self, withInset: margin) - imageView.pin(.bottom, to: .bottom, of: self, withInset: -margin) + let verticalMargin: CGFloat = 20 + let horizontalMargin: CGFloat = 8 + + imageView.pin(.top, to: .top, of: self, withInset: verticalMargin) + imageView.pin(.leading, to: .leading, of: self, withInset: -horizontalMargin) + imageView.pin(.trailing, to: .trailing, of: self, withInset: horizontalMargin) + imageView.pin(.bottom, to: .bottom, of: self, withInset: -verticalMargin) imageView.themeBorderColor = .white imageView.layer.borderWidth = 1 diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift index 77d8f261bb..4febd32c7b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift @@ -190,8 +190,12 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel paletteView.delegate = self self.view.addSubview(paletteView) - paletteView.center(.horizontal, in: textView) - paletteView.pin(.trailing, to: .trailing, of: self.view) + paletteView.center(.vertical, in: self.view, withInset: -((bottomInset / 2) + Values.largeSpacing)) + paletteView.pin(.trailing, to: .trailing, of: self.view, withInset: -Values.smallSpacing) + + // Size of gradient image and touchable area + paletteView.set(.width, to: Values.gradientPaletteWidth) + // This will determine the text view's size. paletteView.pin(.leading, to: .trailing, of: textView) From 1466b8796e38601394e2054d12d91550e2ccd09e Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 10 Sep 2025 13:04:41 +0800 Subject: [PATCH 187/244] Added accessibility identifier for block contact cell --- Session/Settings/ConversationSettingsViewModel.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index d26cff429d..5b52afca21 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -192,6 +192,9 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold .chevronRight, pinEdges: [.right] ), + accessibility: Accessibility( + identifier: "Block contacts - Navigation" + ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( SessionTableViewController(viewModel: BlockedContactsViewModel(using: dependencies)) From 15e9c7097752e4cae3ced9e78cb1b8891a194f47 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Wed, 10 Sep 2025 14:26:25 +0800 Subject: [PATCH 188/244] Fix emoji category title not displayed properly --- .../EmojiPickerCollectionView.swift | 7 +++++- Session/Emoji/Emoji+Category.swift | 24 ++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index b366b5e2a8..289a6bb8e2 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -322,7 +322,12 @@ private class EmojiSectionHeader: UICollectionReusableView { label.font = .systemFont(ofSize: Values.smallFontSize) label.themeTextColor = .textPrimary addSubview(label) - label.pin(to: self) + + label.pin(.top, to: .top, of: self) + label.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + label.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) + label.pin(.bottom, to: .bottom, of: self) + label.setCompressionResistance(to: .required) } diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 343d9a0e87..905a6f8179 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -20,21 +20,29 @@ extension Emoji { var localizedName: String { switch self { case .smileysAndPeople: - return NSLocalizedString("emojiCategorySmileys", comment: "The name for the emoji category 'Smileys & People'") + // The name for the emoji category 'Smileys & People' + return "emojiCategorySmileys".localized() case .animals: - return NSLocalizedString("emojiCategoryAnimals", comment: "The name for the emoji category 'Animals & Nature'") + // The name for the emoji category 'Animals & Nature' + return "emojiCategoryAnimals".localized() case .food: - return NSLocalizedString("emojiCategoryFood", comment: "The name for the emoji category 'Food & Drink'") + // The name for the emoji category 'Food & Drink' + return "emojiCategoryFood".localized() case .activities: - return NSLocalizedString("emojiCategoryActivities", comment: "The name for the emoji category 'Activities'") + // The name for the emoji category 'Activities' + return "emojiCategoryActivities".localized() case .travel: - return NSLocalizedString("emojiCategoryTravel", comment: "The name for the emoji category 'Travel & Places'") + // The name for the emoji category 'Travel & Places' + return "emojiCategoryTravel".localized() case .objects: - return NSLocalizedString("emojiCategoryObjects", comment: "The name for the emoji category 'Objects'") + // The name for the emoji category 'Objects' + return "emojiCategoryObjects".localized() case .symbols: - return NSLocalizedString("emojiCategorySymbols", comment: "The name for the emoji category 'Symbols'") + // The name for the emoji category 'Symbols' + return "emojiCategorySymbols".localized() case .flags: - return NSLocalizedString("emojiCategoryFlags", comment: "The name for the emoji category 'Flags'") + // The name for the emoji category 'Flags' + return "emojiCategoryFlags".localized() } } From a5fed93f6d6a8cc944fd009fe4699e5aeedc3381 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 11 Sep 2025 08:49:12 +0800 Subject: [PATCH 189/244] Updated emoji localized string generation Fix Emoji generator script `enums must not contain stored properties` error --- Scripts/EmojiGenerator.swift | 11 +- Session/Emoji/Emoji+Category.swift | 61 +++- Session/Emoji/Emoji+Name.swift | 37 ++- Session/Emoji/Emoji+SkinTones.swift | 149 +++++++++- Session/Emoji/Emoji.swift | 35 ++- Session/Emoji/EmojiWithSkinTones+String.swift | 264 +++++++++++++++++- 6 files changed, 534 insertions(+), 23 deletions(-) diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 519ce65220..1c2102c81f 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -53,7 +53,7 @@ enum RemoteModel { case flags = "Flags" case components = "Component" - var localizedKey: String = { + var localizedKey: String { switch self { case .smileys: return "Smileys" @@ -77,8 +77,8 @@ enum RemoteModel { return "Flags" case .components: return "Component" - } - }() + } + } } static func fetchEmojiData() throws -> Data { @@ -569,8 +569,9 @@ extension EmojiGenerator { fileHandle.indent { let stringKey = "emojiCategory\(category.localizedKey)" let stringComment = "The name for the emoji category '\(category.rawValue)'" - - fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")") + + fileHandle.writeLine("// \(stringComment)") + fileHandle.writeLine("return \"\(stringKey)\".localized()") } } fileHandle.writeLine("}") diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 905a6f8179..996f7d83a4 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -100,6 +100,8 @@ extension Emoji { .faceExhaling, .lyingFace, .shakingFace, + .headShakingHorizontally, + .headShakingVertically, .relieved, .pensive, .sleepy, @@ -458,24 +460,42 @@ extension Emoji { .walking, .manWalking, .womanWalking, + .personWalkingFacingRight, + .womanWalkingFacingRight, + .manWalkingFacingRight, .standingPerson, .manStanding, .womanStanding, .kneelingPerson, .manKneeling, .womanKneeling, + .personKneelingFacingRight, + .womanKneelingFacingRight, + .manKneelingFacingRight, .personWithProbingCane, + .personWithWhiteCaneFacingRight, .manWithProbingCane, + .manWithWhiteCaneFacingRight, .womanWithProbingCane, + .womanWithWhiteCaneFacingRight, .personInMotorizedWheelchair, + .personInMotorizedWheelchairFacingRight, .manInMotorizedWheelchair, + .manInMotorizedWheelchairFacingRight, .womanInMotorizedWheelchair, + .womanInMotorizedWheelchairFacingRight, .personInManualWheelchair, + .personInManualWheelchairFacingRight, .manInManualWheelchair, + .manInManualWheelchairFacingRight, .womanInManualWheelchair, + .womanInManualWheelchairFacingRight, .runner, .manRunning, .womanRunning, + .personRunningFacingRight, + .womanRunningFacingRight, + .manRunningFacingRight, .dancer, .manDancing, .manInBusinessSuitLevitating, @@ -548,7 +568,6 @@ extension Emoji { .womanHeartMan, .manHeartMan, .womanHeartWoman, - .family, .manWomanBoy, .manWomanGirl, .manWomanGirlBoy, @@ -578,6 +597,11 @@ extension Emoji { .bustInSilhouette, .bustsInSilhouette, .peopleHugging, + .family, + .familyAdultAdultChild, + .familyAdultAdultChildChild, + .familyAdultChild, + .familyAdultChildChild, .footprints, ] case .animals: @@ -669,6 +693,7 @@ extension Emoji { .wing, .blackBird, .goose, + .phoenix, .frog, .crocodile, .turtle, @@ -742,6 +767,7 @@ extension Emoji { .watermelon, .tangerine, .lemon, + .lime, .banana, .pineapple, .mango, @@ -773,6 +799,7 @@ extension Emoji { .chestnut, .gingerRoot, .peaPod, + .brownMushroom, .bread, .croissant, .baguetteBread, @@ -1390,6 +1417,7 @@ extension Emoji { .scales, .probingCane, .link, + .brokenChain, .chains, .hook, .toolbox, @@ -1997,6 +2025,8 @@ extension Emoji { case .faceExhaling: return .smileysAndPeople case .lyingFace: return .smileysAndPeople case .shakingFace: return .smileysAndPeople + case .headShakingHorizontally: return .smileysAndPeople + case .headShakingVertically: return .smileysAndPeople case .relieved: return .smileysAndPeople case .pensive: return .smileysAndPeople case .sleepy: return .smileysAndPeople @@ -2355,24 +2385,42 @@ extension Emoji { case .walking: return .smileysAndPeople case .manWalking: return .smileysAndPeople case .womanWalking: return .smileysAndPeople + case .personWalkingFacingRight: return .smileysAndPeople + case .womanWalkingFacingRight: return .smileysAndPeople + case .manWalkingFacingRight: return .smileysAndPeople case .standingPerson: return .smileysAndPeople case .manStanding: return .smileysAndPeople case .womanStanding: return .smileysAndPeople case .kneelingPerson: return .smileysAndPeople case .manKneeling: return .smileysAndPeople case .womanKneeling: return .smileysAndPeople + case .personKneelingFacingRight: return .smileysAndPeople + case .womanKneelingFacingRight: return .smileysAndPeople + case .manKneelingFacingRight: return .smileysAndPeople case .personWithProbingCane: return .smileysAndPeople + case .personWithWhiteCaneFacingRight: return .smileysAndPeople case .manWithProbingCane: return .smileysAndPeople + case .manWithWhiteCaneFacingRight: return .smileysAndPeople case .womanWithProbingCane: return .smileysAndPeople + case .womanWithWhiteCaneFacingRight: return .smileysAndPeople case .personInMotorizedWheelchair: return .smileysAndPeople + case .personInMotorizedWheelchairFacingRight: return .smileysAndPeople case .manInMotorizedWheelchair: return .smileysAndPeople + case .manInMotorizedWheelchairFacingRight: return .smileysAndPeople case .womanInMotorizedWheelchair: return .smileysAndPeople + case .womanInMotorizedWheelchairFacingRight: return .smileysAndPeople case .personInManualWheelchair: return .smileysAndPeople + case .personInManualWheelchairFacingRight: return .smileysAndPeople case .manInManualWheelchair: return .smileysAndPeople + case .manInManualWheelchairFacingRight: return .smileysAndPeople case .womanInManualWheelchair: return .smileysAndPeople + case .womanInManualWheelchairFacingRight: return .smileysAndPeople case .runner: return .smileysAndPeople case .manRunning: return .smileysAndPeople case .womanRunning: return .smileysAndPeople + case .personRunningFacingRight: return .smileysAndPeople + case .womanRunningFacingRight: return .smileysAndPeople + case .manRunningFacingRight: return .smileysAndPeople case .dancer: return .smileysAndPeople case .manDancing: return .smileysAndPeople case .manInBusinessSuitLevitating: return .smileysAndPeople @@ -2445,7 +2493,6 @@ extension Emoji { case .womanHeartMan: return .smileysAndPeople case .manHeartMan: return .smileysAndPeople case .womanHeartWoman: return .smileysAndPeople - case .family: return .smileysAndPeople case .manWomanBoy: return .smileysAndPeople case .manWomanGirl: return .smileysAndPeople case .manWomanGirlBoy: return .smileysAndPeople @@ -2475,6 +2522,11 @@ extension Emoji { case .bustInSilhouette: return .smileysAndPeople case .bustsInSilhouette: return .smileysAndPeople case .peopleHugging: return .smileysAndPeople + case .family: return .smileysAndPeople + case .familyAdultAdultChild: return .smileysAndPeople + case .familyAdultAdultChildChild: return .smileysAndPeople + case .familyAdultChild: return .smileysAndPeople + case .familyAdultChildChild: return .smileysAndPeople case .footprints: return .smileysAndPeople case .monkeyFace: return .animals case .monkey: return .animals @@ -2563,6 +2615,7 @@ extension Emoji { case .wing: return .animals case .blackBird: return .animals case .goose: return .animals + case .phoenix: return .animals case .frog: return .animals case .crocodile: return .animals case .turtle: return .animals @@ -2633,6 +2686,7 @@ extension Emoji { case .watermelon: return .food case .tangerine: return .food case .lemon: return .food + case .lime: return .food case .banana: return .food case .pineapple: return .food case .mango: return .food @@ -2664,6 +2718,7 @@ extension Emoji { case .chestnut: return .food case .gingerRoot: return .food case .peaPod: return .food + case .brownMushroom: return .food case .bread: return .food case .croissant: return .food case .baguetteBread: return .food @@ -3272,6 +3327,7 @@ extension Emoji { case .scales: return .objects case .probingCane: return .objects case .link: return .objects + case .brokenChain: return .objects case .chains: return .objects case .hook: return .objects case .toolbox: return .objects @@ -3829,4 +3885,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift index 2ddb050adb..2b6abe71f9 100644 --- a/Session/Emoji/Emoji+Name.swift +++ b/Session/Emoji/Emoji+Name.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { var name: String { switch self { @@ -57,6 +59,8 @@ extension Emoji { case .faceExhaling: return "face exhaling, face_exhaling, faceexhaling" case .lyingFace: return "lying face, lying_face, lyingface" case .shakingFace: return "shaking face, shaking_face, shakingface" + case .headShakingHorizontally: return "head shaking horizontally, head_shaking_horizontally, headshakinghorizontally" + case .headShakingVertically: return "head shaking vertically, head_shaking_vertically, headshakingvertically" case .relieved: return "relieved, relieved face" case .pensive: return "pensive, pensive face" case .sleepy: return "sleepy, sleepy face" @@ -415,24 +419,42 @@ extension Emoji { case .walking: return "pedestrian, walking" case .manWalking: return "man walking, man-walking, manwalking" case .womanWalking: return "woman walking, woman-walking, womanwalking" + case .personWalkingFacingRight: return "person walking facing right, person_walking_facing_right, personwalkingfacingright" + case .womanWalkingFacingRight: return "woman walking facing right, woman_walking_facing_right, womanwalkingfacingright" + case .manWalkingFacingRight: return "man walking facing right, man_walking_facing_right, manwalkingfacingright" case .standingPerson: return "standing person, standing_person, standingperson" case .manStanding: return "man standing, man_standing, manstanding" case .womanStanding: return "woman standing, woman_standing, womanstanding" case .kneelingPerson: return "kneeling person, kneeling_person, kneelingperson" case .manKneeling: return "man kneeling, man_kneeling, mankneeling" case .womanKneeling: return "woman kneeling, woman_kneeling, womankneeling" + case .personKneelingFacingRight: return "person kneeling facing right, person_kneeling_facing_right, personkneelingfacingright" + case .womanKneelingFacingRight: return "woman kneeling facing right, woman_kneeling_facing_right, womankneelingfacingright" + case .manKneelingFacingRight: return "man kneeling facing right, man_kneeling_facing_right, mankneelingfacingright" case .personWithProbingCane: return "person with white cane, person_with_probing_cane, personwithprobingcane" + case .personWithWhiteCaneFacingRight: return "person with white cane facing right, person_with_white_cane_facing_right, personwithwhitecanefacingright" case .manWithProbingCane: return "man with white cane, man_with_probing_cane, manwithprobingcane" + case .manWithWhiteCaneFacingRight: return "man with white cane facing right, man_with_white_cane_facing_right, manwithwhitecanefacingright" case .womanWithProbingCane: return "woman with white cane, woman_with_probing_cane, womanwithprobingcane" + case .womanWithWhiteCaneFacingRight: return "woman with white cane facing right, woman_with_white_cane_facing_right, womanwithwhitecanefacingright" case .personInMotorizedWheelchair: return "person in motorized wheelchair, person_in_motorized_wheelchair, personinmotorizedwheelchair" + case .personInMotorizedWheelchairFacingRight: return "person in motorized wheelchair facing right, person_in_motorized_wheelchair_facing_right, personinmotorizedwheelchairfacingright" case .manInMotorizedWheelchair: return "man in motorized wheelchair, man_in_motorized_wheelchair, maninmotorizedwheelchair" + case .manInMotorizedWheelchairFacingRight: return "man in motorized wheelchair facing right, man_in_motorized_wheelchair_facing_right, maninmotorizedwheelchairfacingright" case .womanInMotorizedWheelchair: return "woman in motorized wheelchair, woman_in_motorized_wheelchair, womaninmotorizedwheelchair" + case .womanInMotorizedWheelchairFacingRight: return "woman in motorized wheelchair facing right, woman_in_motorized_wheelchair_facing_right, womaninmotorizedwheelchairfacingright" case .personInManualWheelchair: return "person in manual wheelchair, person_in_manual_wheelchair, personinmanualwheelchair" + case .personInManualWheelchairFacingRight: return "person in manual wheelchair facing right, person_in_manual_wheelchair_facing_right, personinmanualwheelchairfacingright" case .manInManualWheelchair: return "man in manual wheelchair, man_in_manual_wheelchair, maninmanualwheelchair" + case .manInManualWheelchairFacingRight: return "man in manual wheelchair facing right, man_in_manual_wheelchair_facing_right, maninmanualwheelchairfacingright" case .womanInManualWheelchair: return "woman in manual wheelchair, woman_in_manual_wheelchair, womaninmanualwheelchair" + case .womanInManualWheelchairFacingRight: return "woman in manual wheelchair facing right, woman_in_manual_wheelchair_facing_right, womaninmanualwheelchairfacingright" case .runner: return "runner, running" case .manRunning: return "man running, man-running, manrunning" case .womanRunning: return "woman running, woman-running, womanrunning" + case .personRunningFacingRight: return "person running facing right, person_running_facing_right, personrunningfacingright" + case .womanRunningFacingRight: return "woman running facing right, woman_running_facing_right, womanrunningfacingright" + case .manRunningFacingRight: return "man running facing right, man_running_facing_right, manrunningfacingright" case .dancer: return "dancer" case .manDancing: return "man dancing, man_dancing, mandancing" case .manInBusinessSuitLevitating: return "man_in_business_suit_levitating, maninbusinesssuitlevitating, person in suit levitating" @@ -505,7 +527,6 @@ extension Emoji { case .womanHeartMan: return "couple with heart: woman, man, woman-heart-man, womanheartman" case .manHeartMan: return "couple with heart: man, man, man-heart-man, manheartman" case .womanHeartWoman: return "couple with heart: woman, woman, woman-heart-woman, womanheartwoman" - case .family: return "family" case .manWomanBoy: return "family: man, woman, boy, man-woman-boy, manwomanboy" case .manWomanGirl: return "family: man, woman, girl, man-woman-girl, manwomangirl" case .manWomanGirlBoy: return "family: man, woman, girl, boy, man-woman-girl-boy, manwomangirlboy" @@ -535,6 +556,11 @@ extension Emoji { case .bustInSilhouette: return "bust in silhouette, bust_in_silhouette, bustinsilhouette" case .bustsInSilhouette: return "busts in silhouette, busts_in_silhouette, bustsinsilhouette" case .peopleHugging: return "people hugging, people_hugging, peoplehugging" + case .family: return "family" + case .familyAdultAdultChild: return "family: adult, adult, child, family_adult_adult_child, familyadultadultchild" + case .familyAdultAdultChildChild: return "family: adult, adult, child, child, family_adult_adult_child_child, familyadultadultchildchild" + case .familyAdultChild: return "family: adult, child, family_adult_child, familyadultchild" + case .familyAdultChildChild: return "family: adult, child, child, family_adult_child_child, familyadultchildchild" case .footprints: return "footprints" case .skinTone2: return "emoji modifier fitzpatrick type-1-2, skin-tone-2, skintone2" case .skinTone3: return "emoji modifier fitzpatrick type-3, skin-tone-3, skintone3" @@ -628,6 +654,7 @@ extension Emoji { case .wing: return "wing" case .blackBird: return "black bird, black_bird, blackbird" case .goose: return "goose" + case .phoenix: return "phoenix" case .frog: return "frog, frog face" case .crocodile: return "crocodile" case .turtle: return "turtle" @@ -698,6 +725,7 @@ extension Emoji { case .watermelon: return "watermelon" case .tangerine: return "tangerine" case .lemon: return "lemon" + case .lime: return "lime" case .banana: return "banana" case .pineapple: return "pineapple" case .mango: return "mango" @@ -729,6 +757,7 @@ extension Emoji { case .chestnut: return "chestnut" case .gingerRoot: return "ginger root, ginger_root, gingerroot" case .peaPod: return "pea pod, pea_pod, peapod" + case .brownMushroom: return "brown mushroom, brown_mushroom, brownmushroom" case .bread: return "bread" case .croissant: return "croissant" case .baguetteBread: return "baguette bread, baguette_bread, baguettebread" @@ -1337,6 +1366,7 @@ extension Emoji { case .scales: return "balance scale, scales" case .probingCane: return "probing cane, probing_cane, probingcane" case .link: return "link, link symbol" + case .brokenChain: return "broken chain, broken_chain, brokenchain" case .chains: return "chains" case .hook: return "hook" case .toolbox: return "toolbox" @@ -1852,7 +1882,7 @@ extension Emoji { case .flagTm: return "flag-tm, flagtm, turkmenistan flag" case .flagTn: return "flag-tn, flagtn, tunisia flag" case .flagTo: return "flag-to, flagto, tonga flag" - case .flagTr: return "flag-tr, flagtr, turkey flag" + case .flagTr: return "flag-tr, flagtr, türkiye flag" case .flagTt: return "flag-tt, flagtt, trinidad & tobago flag" case .flagTv: return "flag-tv, flagtv, tuvalu flag" case .flagTw: return "flag-tw, flagtw, taiwan flag" @@ -1885,4 +1915,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+SkinTones.swift b/Session/Emoji/Emoji+SkinTones.swift index f1fb18434f..9f34de6151 100644 --- a/Session/Emoji/Emoji+SkinTones.swift +++ b/Session/Emoji/Emoji+SkinTones.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { enum SkinTone: String, CaseIterable, Equatable { case light = "🏻" @@ -1841,6 +1843,30 @@ extension Emoji { [.mediumDark]: "🚶🏾‍♀️", [.dark]: "🚶🏿‍♀️", ] + case .personWalkingFacingRight: + return [ + [.light]: "🚶🏻‍➡️", + [.mediumLight]: "🚶🏼‍➡️", + [.medium]: "🚶🏽‍➡️", + [.mediumDark]: "🚶🏾‍➡️", + [.dark]: "🚶🏿‍➡️", + ] + case .womanWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♀️‍➡️", + [.mediumLight]: "🚶🏼‍♀️‍➡️", + [.medium]: "🚶🏽‍♀️‍➡️", + [.mediumDark]: "🚶🏾‍♀️‍➡️", + [.dark]: "🚶🏿‍♀️‍➡️", + ] + case .manWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♂️‍➡️", + [.mediumLight]: "🚶🏼‍♂️‍➡️", + [.medium]: "🚶🏽‍♂️‍➡️", + [.mediumDark]: "🚶🏾‍♂️‍➡️", + [.dark]: "🚶🏿‍♂️‍➡️", + ] case .standingPerson: return [ [.light]: "🧍🏻", @@ -1889,6 +1915,30 @@ extension Emoji { [.mediumDark]: "🧎🏾‍♀️", [.dark]: "🧎🏿‍♀️", ] + case .personKneelingFacingRight: + return [ + [.light]: "🧎🏻‍➡️", + [.mediumLight]: "🧎🏼‍➡️", + [.medium]: "🧎🏽‍➡️", + [.mediumDark]: "🧎🏾‍➡️", + [.dark]: "🧎🏿‍➡️", + ] + case .womanKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♀️‍➡️", + [.mediumLight]: "🧎🏼‍♀️‍➡️", + [.medium]: "🧎🏽‍♀️‍➡️", + [.mediumDark]: "🧎🏾‍♀️‍➡️", + [.dark]: "🧎🏿‍♀️‍➡️", + ] + case .manKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♂️‍➡️", + [.mediumLight]: "🧎🏼‍♂️‍➡️", + [.medium]: "🧎🏽‍♂️‍➡️", + [.mediumDark]: "🧎🏾‍♂️‍➡️", + [.dark]: "🧎🏿‍♂️‍➡️", + ] case .personWithProbingCane: return [ [.light]: "🧑🏻‍🦯", @@ -1897,6 +1947,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦯", [.dark]: "🧑🏿‍🦯", ] + case .personWithWhiteCaneFacingRight: + return [ + [.light]: "🧑🏻‍🦯‍➡️", + [.mediumLight]: "🧑🏼‍🦯‍➡️", + [.medium]: "🧑🏽‍🦯‍➡️", + [.mediumDark]: "🧑🏾‍🦯‍➡️", + [.dark]: "🧑🏿‍🦯‍➡️", + ] case .manWithProbingCane: return [ [.light]: "👨🏻‍🦯", @@ -1905,6 +1963,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦯", [.dark]: "👨🏿‍🦯", ] + case .manWithWhiteCaneFacingRight: + return [ + [.light]: "👨🏻‍🦯‍➡️", + [.mediumLight]: "👨🏼‍🦯‍➡️", + [.medium]: "👨🏽‍🦯‍➡️", + [.mediumDark]: "👨🏾‍🦯‍➡️", + [.dark]: "👨🏿‍🦯‍➡️", + ] case .womanWithProbingCane: return [ [.light]: "👩🏻‍🦯", @@ -1913,6 +1979,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦯", [.dark]: "👩🏿‍🦯", ] + case .womanWithWhiteCaneFacingRight: + return [ + [.light]: "👩🏻‍🦯‍➡️", + [.mediumLight]: "👩🏼‍🦯‍➡️", + [.medium]: "👩🏽‍🦯‍➡️", + [.mediumDark]: "👩🏾‍🦯‍➡️", + [.dark]: "👩🏿‍🦯‍➡️", + ] case .personInMotorizedWheelchair: return [ [.light]: "🧑🏻‍🦼", @@ -1921,6 +1995,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦼", [.dark]: "🧑🏿‍🦼", ] + case .personInMotorizedWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦼‍➡️", + [.mediumLight]: "🧑🏼‍🦼‍➡️", + [.medium]: "🧑🏽‍🦼‍➡️", + [.mediumDark]: "🧑🏾‍🦼‍➡️", + [.dark]: "🧑🏿‍🦼‍➡️", + ] case .manInMotorizedWheelchair: return [ [.light]: "👨🏻‍🦼", @@ -1929,6 +2011,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦼", [.dark]: "👨🏿‍🦼", ] + case .manInMotorizedWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦼‍➡️", + [.mediumLight]: "👨🏼‍🦼‍➡️", + [.medium]: "👨🏽‍🦼‍➡️", + [.mediumDark]: "👨🏾‍🦼‍➡️", + [.dark]: "👨🏿‍🦼‍➡️", + ] case .womanInMotorizedWheelchair: return [ [.light]: "👩🏻‍🦼", @@ -1937,6 +2027,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦼", [.dark]: "👩🏿‍🦼", ] + case .womanInMotorizedWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦼‍➡️", + [.mediumLight]: "👩🏼‍🦼‍➡️", + [.medium]: "👩🏽‍🦼‍➡️", + [.mediumDark]: "👩🏾‍🦼‍➡️", + [.dark]: "👩🏿‍🦼‍➡️", + ] case .personInManualWheelchair: return [ [.light]: "🧑🏻‍🦽", @@ -1945,6 +2043,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦽", [.dark]: "🧑🏿‍🦽", ] + case .personInManualWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦽‍➡️", + [.mediumLight]: "🧑🏼‍🦽‍➡️", + [.medium]: "🧑🏽‍🦽‍➡️", + [.mediumDark]: "🧑🏾‍🦽‍➡️", + [.dark]: "🧑🏿‍🦽‍➡️", + ] case .manInManualWheelchair: return [ [.light]: "👨🏻‍🦽", @@ -1953,6 +2059,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦽", [.dark]: "👨🏿‍🦽", ] + case .manInManualWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦽‍➡️", + [.mediumLight]: "👨🏼‍🦽‍➡️", + [.medium]: "👨🏽‍🦽‍➡️", + [.mediumDark]: "👨🏾‍🦽‍➡️", + [.dark]: "👨🏿‍🦽‍➡️", + ] case .womanInManualWheelchair: return [ [.light]: "👩🏻‍🦽", @@ -1961,6 +2075,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦽", [.dark]: "👩🏿‍🦽", ] + case .womanInManualWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦽‍➡️", + [.mediumLight]: "👩🏼‍🦽‍➡️", + [.medium]: "👩🏽‍🦽‍➡️", + [.mediumDark]: "👩🏾‍🦽‍➡️", + [.dark]: "👩🏿‍🦽‍➡️", + ] case .runner: return [ [.light]: "🏃🏻", @@ -1985,6 +2107,30 @@ extension Emoji { [.mediumDark]: "🏃🏾‍♀️", [.dark]: "🏃🏿‍♀️", ] + case .personRunningFacingRight: + return [ + [.light]: "🏃🏻‍➡️", + [.mediumLight]: "🏃🏼‍➡️", + [.medium]: "🏃🏽‍➡️", + [.mediumDark]: "🏃🏾‍➡️", + [.dark]: "🏃🏿‍➡️", + ] + case .womanRunningFacingRight: + return [ + [.light]: "🏃🏻‍♀️‍➡️", + [.mediumLight]: "🏃🏼‍♀️‍➡️", + [.medium]: "🏃🏽‍♀️‍➡️", + [.mediumDark]: "🏃🏾‍♀️‍➡️", + [.dark]: "🏃🏿‍♀️‍➡️", + ] + case .manRunningFacingRight: + return [ + [.light]: "🏃🏻‍♂️‍➡️", + [.mediumLight]: "🏃🏼‍♂️‍➡️", + [.medium]: "🏃🏽‍♂️‍➡️", + [.mediumDark]: "🏃🏾‍♂️‍➡️", + [.dark]: "🏃🏿‍♂️‍➡️", + ] case .dancer: return [ [.light]: "💃🏻", @@ -2741,4 +2887,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji.swift b/Session/Emoji/Emoji.swift index aa8352ca24..a315b53273 100644 --- a/Session/Emoji/Emoji.swift +++ b/Session/Emoji/Emoji.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + /// A sorted representation of all available emoji enum Emoji: String, CaseIterable, Equatable { case grinning = "😀" @@ -56,6 +58,8 @@ enum Emoji: String, CaseIterable, Equatable { case faceExhaling = "😮‍💨" case lyingFace = "🤥" case shakingFace = "🫨" + case headShakingHorizontally = "🙂‍↔️" + case headShakingVertically = "🙂‍↕️" case relieved = "😌" case pensive = "😔" case sleepy = "😪" @@ -414,24 +418,42 @@ enum Emoji: String, CaseIterable, Equatable { case walking = "🚶" case manWalking = "🚶‍♂️" case womanWalking = "🚶‍♀️" + case personWalkingFacingRight = "🚶‍➡️" + case womanWalkingFacingRight = "🚶‍♀️‍➡️" + case manWalkingFacingRight = "🚶‍♂️‍➡️" case standingPerson = "🧍" case manStanding = "🧍‍♂️" case womanStanding = "🧍‍♀️" case kneelingPerson = "🧎" case manKneeling = "🧎‍♂️" case womanKneeling = "🧎‍♀️" + case personKneelingFacingRight = "🧎‍➡️" + case womanKneelingFacingRight = "🧎‍♀️‍➡️" + case manKneelingFacingRight = "🧎‍♂️‍➡️" case personWithProbingCane = "🧑‍🦯" + case personWithWhiteCaneFacingRight = "🧑‍🦯‍➡️" case manWithProbingCane = "👨‍🦯" + case manWithWhiteCaneFacingRight = "👨‍🦯‍➡️" case womanWithProbingCane = "👩‍🦯" + case womanWithWhiteCaneFacingRight = "👩‍🦯‍➡️" case personInMotorizedWheelchair = "🧑‍🦼" + case personInMotorizedWheelchairFacingRight = "🧑‍🦼‍➡️" case manInMotorizedWheelchair = "👨‍🦼" + case manInMotorizedWheelchairFacingRight = "👨‍🦼‍➡️" case womanInMotorizedWheelchair = "👩‍🦼" + case womanInMotorizedWheelchairFacingRight = "👩‍🦼‍➡️" case personInManualWheelchair = "🧑‍🦽" + case personInManualWheelchairFacingRight = "🧑‍🦽‍➡️" case manInManualWheelchair = "👨‍🦽" + case manInManualWheelchairFacingRight = "👨‍🦽‍➡️" case womanInManualWheelchair = "👩‍🦽" + case womanInManualWheelchairFacingRight = "👩‍🦽‍➡️" case runner = "🏃" case manRunning = "🏃‍♂️" case womanRunning = "🏃‍♀️" + case personRunningFacingRight = "🏃‍➡️" + case womanRunningFacingRight = "🏃‍♀️‍➡️" + case manRunningFacingRight = "🏃‍♂️‍➡️" case dancer = "💃" case manDancing = "🕺" case manInBusinessSuitLevitating = "🕴️" @@ -504,7 +526,6 @@ enum Emoji: String, CaseIterable, Equatable { case womanHeartMan = "👩‍❤️‍👨" case manHeartMan = "👨‍❤️‍👨" case womanHeartWoman = "👩‍❤️‍👩" - case family = "👪" case manWomanBoy = "👨‍👩‍👦" case manWomanGirl = "👨‍👩‍👧" case manWomanGirlBoy = "👨‍👩‍👧‍👦" @@ -534,6 +555,11 @@ enum Emoji: String, CaseIterable, Equatable { case bustInSilhouette = "👤" case bustsInSilhouette = "👥" case peopleHugging = "🫂" + case family = "👪" + case familyAdultAdultChild = "🧑‍🧑‍🧒" + case familyAdultAdultChildChild = "🧑‍🧑‍🧒‍🧒" + case familyAdultChild = "🧑‍🧒" + case familyAdultChildChild = "🧑‍🧒‍🧒" case footprints = "👣" case skinTone2 = "🏻" case skinTone3 = "🏼" @@ -627,6 +653,7 @@ enum Emoji: String, CaseIterable, Equatable { case wing = "🪽" case blackBird = "🐦‍⬛" case goose = "🪿" + case phoenix = "🐦‍🔥" case frog = "🐸" case crocodile = "🐊" case turtle = "🐢" @@ -697,6 +724,7 @@ enum Emoji: String, CaseIterable, Equatable { case watermelon = "🍉" case tangerine = "🍊" case lemon = "🍋" + case lime = "🍋‍🟩" case banana = "🍌" case pineapple = "🍍" case mango = "🥭" @@ -728,6 +756,7 @@ enum Emoji: String, CaseIterable, Equatable { case chestnut = "🌰" case gingerRoot = "🫚" case peaPod = "🫛" + case brownMushroom = "🍄‍🟫" case bread = "🍞" case croissant = "🥐" case baguetteBread = "🥖" @@ -1336,6 +1365,7 @@ enum Emoji: String, CaseIterable, Equatable { case scales = "⚖️" case probingCane = "🦯" case link = "🔗" + case brokenChain = "⛓️‍💥" case chains = "⛓️" case hook = "🪝" case toolbox = "🧰" @@ -1882,4 +1912,3 @@ enum Emoji: String, CaseIterable, Equatable { case flagScotland = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" case flagWales = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" } -// swiftlint:disable all diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift index e1b1c84daa..3572ff1249 100644 --- a/Session/Emoji/EmojiWithSkinTones+String.swift +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension EmojiWithSkinTones { init?(rawValue: String) { guard rawValue.isSingleEmoji else { return nil } @@ -75,16 +77,19 @@ extension EmojiWithSkinTones { case 1934: self = EmojiWithSkinTones.emojiFrom1934(rawValue) case 1935: self = EmojiWithSkinTones.emojiFrom1935(rawValue) case 1937: self = EmojiWithSkinTones.emojiFrom1937(rawValue) + case 2104: self = EmojiWithSkinTones.emojiFrom2104(rawValue) case 2109: self = EmojiWithSkinTones.emojiFrom2109(rawValue) case 2111: self = EmojiWithSkinTones.emojiFrom2111(rawValue) case 2112: self = EmojiWithSkinTones.emojiFrom2112(rawValue) case 2113: self = EmojiWithSkinTones.emojiFrom2113(rawValue) case 2116: self = EmojiWithSkinTones.emojiFrom2116(rawValue) case 2117: self = EmojiWithSkinTones.emojiFrom2117(rawValue) + case 2120: self = EmojiWithSkinTones.emojiFrom2120(rawValue) case 2123: self = EmojiWithSkinTones.emojiFrom2123(rawValue) case 2125: self = EmojiWithSkinTones.emojiFrom2125(rawValue) case 2126: self = EmojiWithSkinTones.emojiFrom2126(rawValue) case 2127: self = EmojiWithSkinTones.emojiFrom2127(rawValue) + case 2128: self = EmojiWithSkinTones.emojiFrom2128(rawValue) case 2129: self = EmojiWithSkinTones.emojiFrom2129(rawValue) case 2210: self = EmojiWithSkinTones.emojiFrom2210(rawValue) case 2549: self = EmojiWithSkinTones.emojiFrom2549(rawValue) @@ -105,8 +110,10 @@ extension EmojiWithSkinTones { case 2641: self = EmojiWithSkinTones.emojiFrom2641(rawValue) case 2642: self = EmojiWithSkinTones.emojiFrom2642(rawValue) case 2644: self = EmojiWithSkinTones.emojiFrom2644(rawValue) + case 2645: self = EmojiWithSkinTones.emojiFrom2645(rawValue) case 2646: self = EmojiWithSkinTones.emojiFrom2646(rawValue) case 2649: self = EmojiWithSkinTones.emojiFrom2649(rawValue) + case 2650: self = EmojiWithSkinTones.emojiFrom2650(rawValue) case 2655: self = EmojiWithSkinTones.emojiFrom2655(rawValue) case 2656: self = EmojiWithSkinTones.emojiFrom2656(rawValue) case 2657: self = EmojiWithSkinTones.emojiFrom2657(rawValue) @@ -117,6 +124,9 @@ extension EmojiWithSkinTones { case 2760: self = EmojiWithSkinTones.emojiFrom2760(rawValue) case 2761: self = EmojiWithSkinTones.emojiFrom2761(rawValue) case 2764: self = EmojiWithSkinTones.emojiFrom2764(rawValue) + case 2943: self = EmojiWithSkinTones.emojiFrom2943(rawValue) + case 2951: self = EmojiWithSkinTones.emojiFrom2951(rawValue) + case 2959: self = EmojiWithSkinTones.emojiFrom2959(rawValue) case 3289: self = EmojiWithSkinTones.emojiFrom3289(rawValue) case 3295: self = EmojiWithSkinTones.emojiFrom3295(rawValue) case 3389: self = EmojiWithSkinTones.emojiFrom3389(rawValue) @@ -126,12 +136,16 @@ extension EmojiWithSkinTones { case 3394: self = EmojiWithSkinTones.emojiFrom3394(rawValue) case 3396: self = EmojiWithSkinTones.emojiFrom3396(rawValue) case 3397: self = EmojiWithSkinTones.emojiFrom3397(rawValue) + case 3400: self = EmojiWithSkinTones.emojiFrom3400(rawValue) case 3403: self = EmojiWithSkinTones.emojiFrom3403(rawValue) case 3404: self = EmojiWithSkinTones.emojiFrom3404(rawValue) case 3405: self = EmojiWithSkinTones.emojiFrom3405(rawValue) case 3406: self = EmojiWithSkinTones.emojiFrom3406(rawValue) case 3407: self = EmojiWithSkinTones.emojiFrom3407(rawValue) + case 3408: self = EmojiWithSkinTones.emojiFrom3408(rawValue) case 3477: self = EmojiWithSkinTones.emojiFrom3477(rawValue) + case 3491: self = EmojiWithSkinTones.emojiFrom3491(rawValue) + case 3505: self = EmojiWithSkinTones.emojiFrom3505(rawValue) case 3921: self = EmojiWithSkinTones.emojiFrom3921(rawValue) case 3922: self = EmojiWithSkinTones.emojiFrom3922(rawValue) case 3924: self = EmojiWithSkinTones.emojiFrom3924(rawValue) @@ -149,9 +163,16 @@ extension EmojiWithSkinTones { case 3951: self = EmojiWithSkinTones.emojiFrom3951(rawValue) case 4007: self = EmojiWithSkinTones.emojiFrom4007(rawValue) case 4046: self = EmojiWithSkinTones.emojiFrom4046(rawValue) + case 4048: self = EmojiWithSkinTones.emojiFrom4048(rawValue) + case 4223: self = EmojiWithSkinTones.emojiFrom4223(rawValue) + case 4231: self = EmojiWithSkinTones.emojiFrom4231(rawValue) + case 4239: self = EmojiWithSkinTones.emojiFrom4239(rawValue) + case 4771: self = EmojiWithSkinTones.emojiFrom4771(rawValue) + case 4785: self = EmojiWithSkinTones.emojiFrom4785(rawValue) case 4840: self = EmojiWithSkinTones.emojiFrom4840(rawValue) case 5237: self = EmojiWithSkinTones.emojiFrom5237(rawValue) case 5370: self = EmojiWithSkinTones.emojiFrom5370(rawValue) + case 5425: self = EmojiWithSkinTones.emojiFrom5425(rawValue) case 6037: self = EmojiWithSkinTones.emojiFrom6037(rawValue) case 6065: self = EmojiWithSkinTones.emojiFrom6065(rawValue) case 6579: self = EmojiWithSkinTones.emojiFrom6579(rawValue) @@ -961,9 +982,9 @@ extension EmojiWithSkinTones { "👬": EmojiWithSkinTones(baseEmoji: .twoMenHoldingHands, skinTones: nil), "💏": EmojiWithSkinTones(baseEmoji: .personKissPerson, skinTones: nil), "💑": EmojiWithSkinTones(baseEmoji: .personHeartPerson, skinTones: nil), - "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "👤": EmojiWithSkinTones(baseEmoji: .bustInSilhouette, skinTones: nil), "👥": EmojiWithSkinTones(baseEmoji: .bustsInSilhouette, skinTones: nil), + "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "💐": EmojiWithSkinTones(baseEmoji: .bouquet, skinTones: nil), "💮": EmojiWithSkinTones(baseEmoji: .whiteFlower, skinTones: nil), "💒": EmojiWithSkinTones(baseEmoji: .wedding, skinTones: nil), @@ -1993,6 +2014,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2104(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🙂‍↔️": EmojiWithSkinTones(baseEmoji: .headShakingHorizontally, skinTones: nil), + "🙂‍↕️": EmojiWithSkinTones(baseEmoji: .headShakingVertically, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2109(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏃‍♂️": EmojiWithSkinTones(baseEmoji: .manRunning, skinTones: nil), @@ -2046,7 +2075,9 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: nil), "👩‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: nil), - "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil) + "🏃‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: nil), + "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil), + "⛓️‍💥": EmojiWithSkinTones(baseEmoji: .brokenChain, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -2084,6 +2115,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2120(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2123(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: nil), @@ -2159,6 +2197,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2128(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2129(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "❤️‍🩹": EmojiWithSkinTones(baseEmoji: .mendingHeart, skinTones: nil) @@ -3197,6 +3242,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2645(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🐦‍🔥": EmojiWithSkinTones(baseEmoji: .phoenix, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2646(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍🔧": EmojiWithSkinTones(baseEmoji: .maleMechanic, skinTones: nil), @@ -3219,6 +3271,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2650(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🍋‍🟩": EmojiWithSkinTones(baseEmoji: .lime, skinTones: nil), + "🍄‍🟫": EmojiWithSkinTones(baseEmoji: .brownMushroom, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2655(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🧑‍🎓": EmojiWithSkinTones(baseEmoji: .student, skinTones: nil), @@ -3293,7 +3353,8 @@ extension EmojiWithSkinTones { "🧑‍🦲": EmojiWithSkinTones(baseEmoji: .baldPerson, skinTones: nil), "🧑‍🦯": EmojiWithSkinTones(baseEmoji: .personWithProbingCane, skinTones: nil), "🧑‍🦼": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchair, skinTones: nil), - "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil) + "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil), + "🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChild, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3323,6 +3384,30 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2943(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: nil), + "🏃‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2951(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: nil), + "🚶‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2959(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: nil), + "🧎‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3289(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏳️‍🌈": EmojiWithSkinTones(baseEmoji: .rainbowFlag, skinTones: nil) @@ -3519,14 +3604,19 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.light]), "👩🏻‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.light]), + "🏃🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.light]), "👨🏼‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumLight]), "👩🏼‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumLight]), + "🏃🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumLight]), "👨🏽‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.medium]), "👩🏽‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.medium]), + "🏃🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.medium]), "👨🏾‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumDark]), "👩🏾‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumDark]), + "🏃🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumDark]), "👨🏿‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.dark]), - "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]) + "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]), + "🏃🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.dark]) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3659,6 +3749,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3400(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3403(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦🏻‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: [.light]), @@ -3914,6 +4015,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3408(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3477(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍👨": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: nil), @@ -3923,6 +4035,27 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3491(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: nil), + "👩‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: nil), + "👨‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: nil), + "👩‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: nil), + "👨‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: nil), + "👩‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom3505(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: nil), + "🧑‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: nil), + "🧑‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3921(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍🎓": EmojiWithSkinTones(baseEmoji: .maleStudent, skinTones: [.light]), @@ -4359,6 +4492,119 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom4048(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChild, skinTones: nil), + "🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4223(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.light]), + "🏃🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.light]), + "🏃🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.medium]), + "🏃🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.medium]), + "🏃🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.dark]), + "🏃🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4231(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.light]), + "🚶🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.medium]), + "🚶🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.dark]), + "🚶🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4239(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.light]), + "🧎🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.medium]), + "🧎🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.dark]), + "🧎🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4771(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.light]), + "👩🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.light]), + "👨🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👨🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.light]), + "👨🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.medium]), + "👩🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.medium]), + "👨🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👨🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.medium]), + "👨🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.dark]), + "👩🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.dark]), + "👨🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👨🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4785(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.light]), + "🧑🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.light]), + "🧑🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.light]), + "🧑🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.medium]), + "🧑🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "🧑🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.medium]), + "🧑🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.dark]), + "🧑🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "🧑🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom4840(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍💋‍👨": EmojiWithSkinTones(baseEmoji: .womanKissMan, skinTones: nil), @@ -4409,6 +4655,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom5425(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom6037(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩🏻‍❤️‍👨🏻": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: [.light]), @@ -4729,4 +4982,3 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } } -// swiftlint:disable all From d8d97e0b7e93dc7f49ee4cf6008c16d19090634c Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 11 Sep 2025 09:55:22 +0800 Subject: [PATCH 190/244] Fix padding icon padding in deleted message cell --- .../Content Views/DeletedMessageView.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 72f5e5c4b1..0d552b34b4 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -31,19 +31,15 @@ final class DeletedMessageView: UIView { } private func setUpViewHierarchy(textColor: ThemeValue, variant: Interaction.Variant, maxWidth: CGFloat) { - // Image view - let imageContainerView: UIView = UIView() - imageContainerView.set(.width, to: DeletedMessageView.iconImageViewSize) - imageContainerView.set(.height, to: DeletedMessageView.iconImageViewSize) + let trashIcon = Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)? + .withRenderingMode(.alwaysTemplate) - let imageView = UIImageView(image: Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)?.withRenderingMode(.alwaysTemplate)) + let imageView = UIImageView(image: trashIcon) imageView.themeTintColor = textColor imageView.alpha = Values.highOpacity imageView.contentMode = .scaleAspectFit imageView.set(.width, to: DeletedMessageView.iconSize) imageView.set(.height, to: DeletedMessageView.iconSize) - imageContainerView.addSubview(imageView) - imageView.center(in: imageContainerView) // Body label let titleLabel = UILabel() @@ -64,9 +60,13 @@ final class DeletedMessageView: UIView { titleLabel.numberOfLines = 2 // Stack view - let stackView = UIStackView(arrangedSubviews: [ imageContainerView, titleLabel ]) + let stackView = UIStackView(arrangedSubviews: [ + imageView, + titleLabel + ]) stackView.axis = .horizontal stackView.alignment = .center + stackView.spacing = Values.smallSpacing stackView.isLayoutMarginsRelativeArrangement = true stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 6) addSubview(stackView) From 835de818e187d4c28a33ef48c6d497db9dd5d5a3 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 11 Sep 2025 15:17:48 +1000 Subject: [PATCH 191/244] feat: share config for current user profile picture update --- Session.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Session/Settings/SettingsViewModel.swift | 2 +- .../Jobs/UpdateProfilePictureJob.swift | 2 +- .../Config Handling/LibSession+Contacts.swift | 5 +---- .../Config Handling/LibSession+Shared.swift | 3 ++- .../LibSession+UserGroups.swift | 3 +-- .../LibSession+UserProfile.swift | 20 +++++++++++++------ .../LibSession+SessionMessagingKit.swift | 11 ++++------ .../Utilities/DisplayPictureManager.swift | 2 +- .../Utilities/Profile+CurrentUser.swift | 7 +++++-- 11 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d632b3b355..1072c73d28 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -10308,7 +10308,7 @@ repositoryURL = "https://github.com/session-foundation/libsession-util-spm"; requirement = { kind = exactVersion; - version = 1.5.2; + version = 1.5.5; }; }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 586ff2e952..4c3ec45ea4 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/libsession-util-spm", "state" : { - "revision" : "c224b53ae973d5cc707def1c11a20e7104ffa028", - "version" : "1.5.2" + "revision" : "05ba2d3194726058f801987775be81f61a52be7d", + "version" : "1.5.5" } }, { diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index fa0558cc14..dabbd23c12 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -723,7 +723,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } self?.updateProfile( - displayPictureUpdate: .currentUserUploadImageData(data: imageData), + displayPictureUpdate: .currentUserUploadImageData(data: imageData, isReupload: false), onComplete: { [weak modal] in modal?.close() } ) diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index 6da65d872f..6b1051be05 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -44,7 +44,7 @@ public enum UpdateProfilePictureJob: JobExecutor { let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } .map { dependencies[singleton: .fileManager].contents(atPath: $0) } - .map { .currentUserUploadImageData(data: $0)} + .map { .currentUserUploadImageData(data: $0, isReupload: true)} .defaulting(to: .none) Profile diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index bda8611474..bbd066678a 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -37,8 +37,7 @@ internal extension LibSessionCacheType { func handleContactsUpdate( _ db: ObservingDatabase, in config: LibSession.Config?, - oldState: [ObservableKey: Any], - serverTimestampMs: Int64 + oldState: [ObservableKey: Any] ) throws { guard configNeedsDump(config) else { return } guard case .contacts(let conf) = config else { @@ -49,7 +48,6 @@ internal extension LibSessionCacheType { // actually a bug) let targetContactData: [String: ContactData] = try LibSession.extractContacts( from: conf, - serverTimestampMs: serverTimestampMs, using: dependencies ).filter { $0.key != userSessionId.hexString } @@ -850,7 +848,6 @@ internal struct ContactData { internal extension LibSession { static func extractContacts( from conf: UnsafeMutablePointer?, - serverTimestampMs: Int64, using dependencies: Dependencies ) throws -> [String: ContactData] { var infiniteLoopGuard: Int = 0 diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 9a6a75bbbc..b4cb0af8e1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -761,6 +761,7 @@ public extension LibSession.Cache { let displayNameInMessage: String? = (visibleMessage?.sender != contactId ? nil : visibleMessage?.profile?.displayName?.nullIfEmpty ) + let profileLastUpdatedInMessage: TimeInterval? = visibleMessage?.profile?.updateTimestampMs.map { TimeInterval($0 / 1000) } let fallbackProfile: Profile? = displayNameInMessage.map { Profile(id: contactId, name: $0) } guard var cContactId: [CChar] = contactId.cString(using: .utf8) else { @@ -785,7 +786,7 @@ public extension LibSession.Cache { nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : displayPic.get(\.key)), - profileLastUpdated: nil + profileLastUpdated: profileLastUpdatedInMessage ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 27f7bf7fdf..56778de0ab 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -39,8 +39,7 @@ internal extension LibSession { internal extension LibSessionCacheType { func handleUserGroupsUpdate( _ db: ObservingDatabase, - in config: LibSession.Config?, - serverTimestampMs: Int64 + in config: LibSession.Config? ) throws { guard configNeedsDump(config) else { return } guard case .userGroups(let conf) = config else { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 4f4bd20321..366e91e4fc 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -11,7 +11,8 @@ internal extension LibSession { static let columnsRelatedToUserProfile: [Profile.Columns] = [ Profile.Columns.name, Profile.Columns.displayPictureUrl, - Profile.Columns.displayPictureEncryptionKey + Profile.Columns.displayPictureEncryptionKey, + Profile.Columns.profileLastUpdated ] static let syncedSettings: [String] = [ @@ -25,8 +26,7 @@ internal extension LibSessionCacheType { func handleUserProfileUpdate( _ db: ObservingDatabase, in config: LibSession.Config?, - oldState: [ObservableKey: Any], - serverTimestampMs: Int64 + oldState: [ObservableKey: Any] ) throws { guard configNeedsDump(config) else { return } guard case .userProfile(let conf) = config else { @@ -39,10 +39,12 @@ internal extension LibSessionCacheType { let profileName: String = String(cString: profileNamePtr) let displayPic: user_profile_pic = user_profile_get_pic(conf) let displayPictureUrl: String? = displayPic.get(\.url, nullIfEmpty: true) + let profileLastUpdateTimestamp: TimeInterval = TimeInterval(user_profile_get_profile_updated(conf)) let updatedProfile: Profile = Profile( id: userSessionId.hexString, name: profileName, - displayPictureUrl: (oldState[.profile(userSessionId.hexString)] as? Profile)?.displayPictureUrl + displayPictureUrl: (oldState[.profile(userSessionId.hexString)] as? Profile)?.displayPictureUrl, + profileLastUpdated: profileLastUpdateTimestamp ) if let profile: Profile = oldState[.profile(userSessionId.hexString)] as? Profile { @@ -74,7 +76,7 @@ internal extension LibSessionCacheType { sessionProProof: getProProof() // TODO: double check if this is needed after Pro Proof is implemented ) }(), - profileUpdateTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), + profileUpdateTimestamp: profileLastUpdateTimestamp, using: dependencies ) @@ -234,7 +236,13 @@ public extension LibSession.Cache { var profilePic: user_profile_pic = user_profile_pic() profilePic.set(\.url, to: displayPictureUrl) profilePic.set(\.key, to: displayPictureEncryptionKey) - user_profile_set_pic(conf, profilePic) + let isReupload: Bool = (displayPictureUrl != oldDisplayPictureUrl) + if isReupload { + user_profile_set_reupload_pic(conf, profilePic) + } else { + user_profile_set_pic(conf, profilePic) + } + try LibSessionError.throwIfNeeded(conf) /// Add a pending observation to notify any observers of the change once it's committed diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 8bbd82e93b..150f4f113a 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -744,7 +744,7 @@ public extension LibSession { case .contacts(let conf): return try LibSession - .extractContacts(from: conf, serverTimestampMs: -1, using: dependencies) + .extractContacts(from: conf, using: dependencies) .reduce(into: [:]) { result, next in result[.contact(next.key)] = next.value.contact result[.profile(next.key)] = next.value.profile @@ -794,16 +794,14 @@ public extension LibSession { try handleUserProfileUpdate( db, in: config, - oldState: oldState, - serverTimestampMs: latestServerTimestampMs + oldState: oldState ) case .contacts: try handleContactsUpdate( db, in: config, - oldState: oldState, - serverTimestampMs: latestServerTimestampMs + oldState: oldState ) case .convoInfoVolatile: @@ -815,8 +813,7 @@ public extension LibSession { case .userGroups: try handleUserGroupsUpdate( db, - in: config, - serverTimestampMs: latestServerTimestampMs + in: config ) case .groupInfo: diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 8a5be77e34..472a45859d 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -34,7 +34,7 @@ public class DisplayPictureManager { case contactUpdateTo(url: String, key: Data, filePath: String, contactProProof: String?) case currentUserRemove - case currentUserUploadImageData(data: Data) + case currentUserUploadImageData(data: Data, isReupload: Bool) case currentUserUpdateTo(url: String, key: Data, filePath: String, sessionProProof: String?) case groupRemove diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 92ab6b1b80..0d99e57e39 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -85,6 +85,7 @@ public extension Profile { displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), + isReuploadingCurrentUserProfilePicture: false, using: dependencies ) Log.info(.profile, "Successfully updated user profile.") @@ -92,7 +93,7 @@ public extension Profile { .mapError { _ in DisplayPictureError.databaseChangesFailed } .eraseToAnyPublisher() - case .currentUserUploadImageData(let data): + case .currentUserUploadImageData(let data, let isReupload): return dependencies[singleton: .displayPictureManager] .prepareAndUploadDisplayPicture(imageData: data) .mapError { $0 as Error } @@ -109,6 +110,7 @@ public extension Profile { sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), + isReuploadingCurrentUserProfilePicture: isReupload, using: dependencies ) @@ -133,6 +135,7 @@ public extension Profile { displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, profileUpdateTimestamp: TimeInterval, + isReuploadingCurrentUserProfilePicture: Bool = false, using dependencies: Dependencies ) throws { let isCurrentUser = (publicKey == dependencies[cache: .general].sessionId.hexString) @@ -213,7 +216,7 @@ public extension Profile { } // Persist any changes - if !profileChanges.isEmpty { + if !profileChanges.isEmpty || isReuploadingCurrentUserProfilePicture { profileChanges.append(Profile.Columns.profileLastUpdated.set(to: profileUpdateTimestamp)) try profile.upsert(db) From f541d4bca73c311434de18dd776fb359ab401ff2 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 11 Sep 2025 16:54:37 +1000 Subject: [PATCH 192/244] wip: fix current user profile picture reupload logic --- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../_014_GenerateInitialUserConfigDumps.swift | 3 ++- .../Config Handling/LibSession+Contacts.swift | 3 ++- .../LibSession+UserProfile.swift | 7 ++++++- .../LibSession+SessionMessagingKit.swift | 21 ++++++++++++++++--- .../MessageSender+Groups.swift | 2 +- .../Utilities/DisplayPictureManager.swift | 4 ++-- .../Utilities/Profile+CurrentUser.swift | 16 +++++++------- 8 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 3e7899fb41..b07d4fcc55 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1768,7 +1768,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob case .groupUploadImageData(let data): /// Show a blocking loading indicator while uploading but not while updating or syncing the group configs return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: data) + .prepareAndUploadDisplayPicture(imageData: data, compression: true) .showingBlockingLoading(in: self?.navigatableState) .map { url, filePath, key, _ -> DisplayPictureManager.Update in .groupUpdateTo(url: url, key: key, filePath: filePath) diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index c81a813e12..6566650c8e 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -64,7 +64,8 @@ enum _014_GenerateInitialUserConfigDumps: Migration { try cache.updateProfile( displayName: (userProfile?["name"] ?? ""), displayPictureUrl: userProfile?["profilePictureUrl"], - displayPictureEncryptionKey: userProfile?["profileEncryptionKey"] + displayPictureEncryptionKey: userProfile?["profileEncryptionKey"], + isReupload: false ) try LibSession.updateNoteToSelf( diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index bbd066678a..573495170c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -512,7 +512,8 @@ internal extension LibSession { try cache.updateProfile( displayName: updatedUserProfile.name, displayPictureUrl: updatedUserProfile.displayPictureUrl, - displayPictureEncryptionKey: updatedUserProfile.displayPictureEncryptionKey + displayPictureEncryptionKey: updatedUserProfile.displayPictureEncryptionKey, + isReupload: (updatedUserProfile.profileLastUpdated == dependencies[cache: .libSession].lastReuploadDisplayPictureTimestamp) ) } } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 366e91e4fc..373c819cb5 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -208,10 +208,15 @@ public extension LibSession.Cache { return String(cString: profileNamePtr) } + public func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) { + lastReuploadDisplayPictureTimestamp = timestamp + } + func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReupload: Bool ) throws { guard let config: LibSession.Config = config(for: .userProfile, sessionId: userSessionId) else { throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: nil) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 150f4f113a..67c6be5766 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -196,6 +196,7 @@ public extension LibSession { public let userSessionId: SessionId public var isEmpty: Bool { configStore.isEmpty } public var allDumpSessionIds: Set { configStore.allIds } + public var lastReuploadDisplayPictureTimestamp: TimeInterval? // MARK: - Initialization @@ -934,6 +935,8 @@ public protocol LibSessionImmutableCacheType: ImmutableCacheType { var allDumpSessionIds: Set { get } func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool + + var lastReuploadDisplayPictureTimestamp: TimeInterval? { get } } /// The majority `libSession` functions can only be accessed via the mutable cache because `libSession` isn't thread safe so if we try @@ -1038,10 +1041,14 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT func set(_ key: Setting.EnumKey, _ value: T?) var displayName: String? { get } + var lastReuploadDisplayPictureTimestamp: TimeInterval? { get } + func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) + func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReupload: Bool ) throws func canPerformChange( @@ -1181,7 +1188,12 @@ public extension LibSessionCacheType { } func updateProfile(displayName: String) throws { - try updateProfile(displayName: displayName, displayPictureUrl: nil, displayPictureEncryptionKey: nil) + try updateProfile( + displayName: displayName, + displayPictureUrl: nil, + displayPictureEncryptionKey: nil, + isReupload: false + ) } var profile: Profile { @@ -1310,13 +1322,16 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { // MARK: - State Access var displayName: String? { return nil } + var lastReuploadDisplayPictureTimestamp: TimeInterval? { return nil } + func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) {} func set(_ key: Setting.BoolKey, _ value: Bool?) {} func set(_ key: Setting.EnumKey, _ value: T?) {} func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReupload: Bool ) throws {} func canPerformChange( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 60e9665c93..3f899a3c4e 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -38,7 +38,7 @@ extension MessageSender { } return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: displayPictureData) + .prepareAndUploadDisplayPicture(imageData: displayPictureData, compression: true) .mapError { error -> Error in error } .map { Optional($0) } .eraseToAnyPublisher() diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 472a45859d..780af03a2e 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -182,7 +182,7 @@ public class DisplayPictureManager { // MARK: - Uploading - public func prepareAndUploadDisplayPicture(imageData: Data) -> AnyPublisher { + public func prepareAndUploadDisplayPicture(imageData: Data, compression: Bool) -> AnyPublisher { return Just(()) .setFailureType(to: DisplayPictureError.self) .tryMap { [dependencies] _ -> (Network.PreparedRequest, String, Data) in @@ -223,7 +223,7 @@ public class DisplayPictureManager { image = image.resized(toFillPixelSize: CGSize(width: DisplayPictureManager.maxDiameter, height: DisplayPictureManager.maxDiameter)) } - guard let data: Data = image.jpegData(compressionQuality: 0.95) else { + guard let data: Data = image.jpegData(compressionQuality: (compression ? 0.95 : 1.0)) else { Log.error(.displayPictureManager, "Updating service with profile failed.") throw DisplayPictureError.writeFailed } diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift index 0d99e57e39..be1a285f39 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+CurrentUser.swift @@ -85,7 +85,6 @@ public extension Profile { displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), - isReuploadingCurrentUserProfilePicture: false, using: dependencies ) Log.info(.profile, "Successfully updated user profile.") @@ -95,10 +94,15 @@ public extension Profile { case .currentUserUploadImageData(let data, let isReupload): return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: data) + .prepareAndUploadDisplayPicture(imageData: data, compression: !isReupload) .mapError { $0 as Error } .flatMapStorageWritePublisher(using: dependencies, updates: { db, result in - let profileUpdateTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + let profileUpdateTimestamp: TimeInterval = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 + if isReupload { + dependencies.mutate(cache: .libSession) { + $0.setLastReuploadDisplayPictureTimestamp(timestamp: profileUpdateTimestamp) + } + } try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, @@ -109,8 +113,7 @@ public extension Profile { filePath: result.filePath, sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), - profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), - isReuploadingCurrentUserProfilePicture: isReupload, + profileUpdateTimestamp: profileUpdateTimestamp, using: dependencies ) @@ -135,7 +138,6 @@ public extension Profile { displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, profileUpdateTimestamp: TimeInterval, - isReuploadingCurrentUserProfilePicture: Bool = false, using dependencies: Dependencies ) throws { let isCurrentUser = (publicKey == dependencies[cache: .general].sessionId.hexString) @@ -216,7 +218,7 @@ public extension Profile { } // Persist any changes - if !profileChanges.isEmpty || isReuploadingCurrentUserProfilePicture { + if !profileChanges.isEmpty { profileChanges.append(Profile.Columns.profileLastUpdated.set(to: profileUpdateTimestamp)) try profile.upsert(db) From 94baa24437f3f7201770b149a357edc814bbb611 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Thu, 11 Sep 2025 14:36:19 +0800 Subject: [PATCH 193/244] Fix keyboard not presenting keyboard on longpress reply --- .../Conversations/ConversationVC+Interaction.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 1330cba707..e5e13f230f 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2250,10 +2250,17 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - if isShowingSearchUI { willManuallyCancelSearchUI() } + // Add delay before doing any ui updates + // Delay added to give time for long press actions to dismiss + let delay = completion == nil ? 0 : ContextMenuVC.dismissDuration - _ = snInputView.becomeFirstResponder() - completion?() + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } + + _ = self?.snInputView.becomeFirstResponder() + + completion?() + } } func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { From bbafaf2d4272d0595ebe9d2363c180cfde1afafa Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 12 Sep 2025 11:07:59 +1000 Subject: [PATCH 194/244] fix current user profile picture reupload logic --- Session.xcodeproj/project.pbxproj | 8 +++---- .../_014_GenerateInitialUserConfigDumps.swift | 2 +- .../Config Handling/LibSession+Contacts.swift | 14 ------------- .../LibSession+UserProfile.swift | 9 ++------ .../LibSession+SessionMessagingKit.swift | 13 +++--------- ...rrentUser.swift => Profile+Updating.swift} | 21 ++++++++++++++----- 6 files changed, 26 insertions(+), 41 deletions(-) rename SessionMessagingKit/Utilities/{Profile+CurrentUser.swift => Profile+Updating.swift} (92%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1072c73d28..32d3dcb89e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -669,7 +669,7 @@ FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; FD3F2EE72DE6CC4100FD6849 /* NotificationsManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3F2EE62DE6CC3B00FD6849 /* NotificationsManagerSpec.swift */; }; FD3F2EF22DF273D900FD6849 /* ThemedAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3F2EF12DF273D100FD6849 /* ThemedAttributedString.swift */; }; - FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */; }; + FD3FAB592ADF906300DC5421 /* Profile+Updating.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */; }; FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */; }; FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */; }; FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB622AEB9A1500DC5421 /* ToastController.swift */; }; @@ -2000,7 +2000,7 @@ FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; FD3F2EE62DE6CC3B00FD6849 /* NotificationsManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManagerSpec.swift; sourceTree = ""; }; FD3F2EF12DF273D100FD6849 /* ThemedAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedAttributedString.swift; sourceTree = ""; }; - FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+CurrentUser.swift"; sourceTree = ""; }; + FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Updating.swift"; sourceTree = ""; }; FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = ""; }; FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; FD3FAB622AEB9A1500DC5421 /* ToastController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastController.swift; sourceTree = ""; }; @@ -3673,7 +3673,7 @@ FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */, C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */, C38EF306255B6DBE007E1867 /* OWSWindowManager.m */, - FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */, + FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */, FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */, C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */, FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */, @@ -6574,7 +6574,7 @@ FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, - FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */, + FD3FAB592ADF906300DC5421 /* Profile+Updating.swift in Sources */, FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */, FDB11A5D2DD300D300BEF49F /* SNProtoContent+Utilities.swift in Sources */, FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */, diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index 6566650c8e..b7cc6e6295 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -65,7 +65,7 @@ enum _014_GenerateInitialUserConfigDumps: Migration { displayName: (userProfile?["name"] ?? ""), displayPictureUrl: userProfile?["profilePictureUrl"], displayPictureEncryptionKey: userProfile?["profileEncryptionKey"], - isReupload: false + isReuploadProfilePicture: false ) try LibSession.updateNoteToSelf( diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 573495170c..63a69e9de1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -505,20 +505,6 @@ internal extension LibSession { existingContactIds.contains($0.id) } - // Update the user profile first (if needed) - if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userSessionId.hexString }) { - try dependencies.mutate(cache: .libSession) { cache in - try cache.performAndPushChange(db, for: .userProfile, sessionId: userSessionId) { _ in - try cache.updateProfile( - displayName: updatedUserProfile.name, - displayPictureUrl: updatedUserProfile.displayPictureUrl, - displayPictureEncryptionKey: updatedUserProfile.displayPictureEncryptionKey, - isReupload: (updatedUserProfile.profileLastUpdated == dependencies[cache: .libSession].lastReuploadDisplayPictureTimestamp) - ) - } - } - } - try dependencies.mutate(cache: .libSession) { cache in try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in try LibSession diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 373c819cb5..3dae469958 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -208,15 +208,11 @@ public extension LibSession.Cache { return String(cString: profileNamePtr) } - public func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) { - lastReuploadDisplayPictureTimestamp = timestamp - } - func updateProfile( displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?, - isReupload: Bool + isReuploadProfilePicture: Bool ) throws { guard let config: LibSession.Config = config(for: .userProfile, sessionId: userSessionId) else { throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: nil) @@ -241,8 +237,7 @@ public extension LibSession.Cache { var profilePic: user_profile_pic = user_profile_pic() profilePic.set(\.url, to: displayPictureUrl) profilePic.set(\.key, to: displayPictureEncryptionKey) - let isReupload: Bool = (displayPictureUrl != oldDisplayPictureUrl) - if isReupload { + if isReuploadProfilePicture { user_profile_set_reupload_pic(conf, profilePic) } else { user_profile_set_pic(conf, profilePic) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 67c6be5766..2ce1303f81 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -196,7 +196,6 @@ public extension LibSession { public let userSessionId: SessionId public var isEmpty: Bool { configStore.isEmpty } public var allDumpSessionIds: Set { configStore.allIds } - public var lastReuploadDisplayPictureTimestamp: TimeInterval? // MARK: - Initialization @@ -935,8 +934,6 @@ public protocol LibSessionImmutableCacheType: ImmutableCacheType { var allDumpSessionIds: Set { get } func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool - - var lastReuploadDisplayPictureTimestamp: TimeInterval? { get } } /// The majority `libSession` functions can only be accessed via the mutable cache because `libSession` isn't thread safe so if we try @@ -1041,14 +1038,12 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT func set(_ key: Setting.EnumKey, _ value: T?) var displayName: String? { get } - var lastReuploadDisplayPictureTimestamp: TimeInterval? { get } - func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) func updateProfile( displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?, - isReupload: Bool + isReuploadProfilePicture: Bool ) throws func canPerformChange( @@ -1192,7 +1187,7 @@ public extension LibSessionCacheType { displayName: displayName, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - isReupload: false + isReuploadProfilePicture: false ) } @@ -1322,8 +1317,6 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { // MARK: - State Access var displayName: String? { return nil } - var lastReuploadDisplayPictureTimestamp: TimeInterval? { return nil } - func setLastReuploadDisplayPictureTimestamp(timestamp: TimeInterval) {} func set(_ key: Setting.BoolKey, _ value: Bool?) {} func set(_ key: Setting.EnumKey, _ value: T?) {} @@ -1331,7 +1324,7 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?, - isReupload: Bool + isReuploadProfilePicture: Bool ) throws {} func canPerformChange( diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+Updating.swift similarity index 92% rename from SessionMessagingKit/Utilities/Profile+CurrentUser.swift rename to SessionMessagingKit/Utilities/Profile+Updating.swift index be1a285f39..c4b4b9772d 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+Updating.swift @@ -98,11 +98,6 @@ public extension Profile { .mapError { $0 as Error } .flatMapStorageWritePublisher(using: dependencies, updates: { db, result in let profileUpdateTimestamp: TimeInterval = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 - if isReupload { - dependencies.mutate(cache: .libSession) { - $0.setLastReuploadDisplayPictureTimestamp(timestamp: profileUpdateTimestamp) - } - } try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, @@ -114,6 +109,7 @@ public extension Profile { sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), profileUpdateTimestamp: profileUpdateTimestamp, + isReuploadCurrentUserProfilePicture: isReupload, using: dependencies ) @@ -138,6 +134,7 @@ public extension Profile { displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, profileUpdateTimestamp: TimeInterval, + isReuploadCurrentUserProfilePicture: Bool = false, using dependencies: Dependencies ) throws { let isCurrentUser = (publicKey == dependencies[cache: .general].sessionId.hexString) @@ -230,6 +227,20 @@ public extension Profile { profileChanges, using: dependencies ) + + + if isCurrentUser, let updatedProfile = try? Profile.fetchOne(db, id: publicKey) { + try dependencies.mutate(cache: .libSession) { cache in + try cache.performAndPushChange(db, for: .userProfile, sessionId: dependencies[cache: .general].sessionId) { _ in + try cache.updateProfile( + displayName: updatedProfile.name, + displayPictureUrl: updatedProfile.displayPictureUrl, + displayPictureEncryptionKey: updatedProfile.displayPictureEncryptionKey, + isReuploadProfilePicture: isReuploadCurrentUserProfilePicture + ) + } + } + } } } } From aa1280e437affa7dc5a86d726cbea47573bf685b Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 12 Sep 2025 11:40:09 +0800 Subject: [PATCH 195/244] Fix previously selected app icon not re-selected on toggle default off --- Session/Settings/AppIconViewModel.swift | 23 ++++++++++++++++++- .../Types/UserDefaultsType.swift | 3 +++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index c0cf0f2da8..5a8386fab5 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -137,6 +137,12 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl lazy var observation: TargetObservation = ObservationBuilderOld .subject(selectedOptionsSubject) .mapWithPrevious { [weak self, dependencies] previous, current -> [SectionModel] in + + if let currentIcon = current { + // Save latest app icon disguise selected + dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] = currentIcon + } + return [ SectionModel( model: .appIcon, @@ -154,7 +160,7 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl onTap: { [weak self] in switch current { case .some: self?.updateAppIcon(nil) - case .none: self?.updateAppIcon(.weather) + case .none: self?.restorePreviousIcon(previous) // Previous is String?? } } ) @@ -189,4 +195,19 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl selectedOptionsSubject.send(icon?.rawValue) } + + private func restorePreviousIcon(_ identifier: String??) { + var previousIcon: AppIcon? { + if let previousIcon = identifier { + // Set previous app icon + return AppIcon(name: previousIcon) + } else if let previousIcon = dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] { + // Handles app close instance to restore previously selected + return AppIcon(name: previousIcon) + } + return .weather + } + + updateAppIcon(previousIcon) + } } diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index e7db0491ee..968b66c2e4 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -228,6 +228,9 @@ public extension UserDefaults.StringKey { /// The id of the thread that a message was just shared to static let lastSharedThreadId: UserDefaults.StringKey = "lastSharedThreadId" + + /// The app-icon name of the previously selected app icon disguise + static let lastSelectedAppIconDisguise: UserDefaults.StringKey = "lastSelectedAppIconDisguise" } // MARK: - Keys From ae10a428c6c75998c1904a6dae82210bf5724e68 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 12 Sep 2025 13:34:05 +0800 Subject: [PATCH 196/244] Fix keyboard not showing when replying from message info --- .../ConversationVC+Interaction.swift | 8 ++++-- Session/Conversations/ConversationVC.swift | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e5e13f230f..e565a4bfbf 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2257,8 +2257,12 @@ extension ConversationVC: DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } - _ = self?.snInputView.becomeFirstResponder() - + if self?.checkIfEventWasTriggerWhileNotVisible() == true { + self?.hasPendingInputKeyboardPresentationEvent = true + } else { + _ = self?.snInputView.becomeFirstResponder() + } + completion?() } } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index ab8471b452..b32a17a57f 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -29,6 +29,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa /// never have disappeared before - this is only needed for value observers since they run asynchronously) private var hasReloadedThreadDataAfterDisappearance: Bool = true + /// This flag indicates that a need for inputview keyboard presentation is needed, this is in events + /// where a delegate action is trigger before poping back into `ConversationVC` + var hasPendingInputKeyboardPresentationEvent: Bool = false + var focusedInteractionInfo: Interaction.TimestampInfo? var focusBehaviour: ConversationViewModel.FocusBehaviour = .none @@ -581,6 +585,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self?.didFinishInitialLayout = true self?.viewIsAppearing = false self?.lastPresentedViewController = nil + + // Show inputview keyboard + if self?.hasPendingInputKeyboardPresentationEvent == true { + self?.makeInputViewFirstResponder() + self?.hasPendingInputKeyboardPresentationEvent = false + } } } @@ -1647,6 +1657,22 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa completion?() } } + + private func makeInputViewFirstResponder() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.inputAccessoryView?.becomeFirstResponder() + } + } + + func checkIfEventWasTriggerWhileNotVisible() -> Bool { + // Delegate reply action triggered by MessageInfoViewController + if let navigationStack = self.navigationController?.viewControllers { + if navigationStack.contains(where: { $0 is ConversationVC }) && navigationStack.last is MessageInfoViewController { + return true + } + } + return false + } // MARK: - UITableViewDataSource From a1bd31d6563292d10bcee0767a2673ddda574941 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Fri, 12 Sep 2025 17:01:47 +0800 Subject: [PATCH 197/244] Improved drag to dismiss keyboard table inset behaviour Added swipe dow from input view to dismiss keyboard --- Session/Conversations/ConversationVC.swift | 3 ++- Session/Conversations/Input View/InputView.swift | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index ab8471b452..b842c0f713 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1567,7 +1567,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // value will break things) let tableViewBottom: CGFloat = (tableView.contentSize.height - tableView.bounds.height + tableView.contentInset.bottom) - if tableView.contentOffset.y < (tableViewBottom - 5) { + // Added `insetDifference > 0` to remove sudden table collapse and overscroll + if tableView.contentOffset.y < (tableViewBottom - 5) && insetDifference > 0 { tableView.contentOffset.y += insetDifference } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 74316f9525..37ece1f334 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -62,6 +62,15 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return result }() + private lazy var swipeGestureRecognizer: UISwipeGestureRecognizer = { + let result: UISwipeGestureRecognizer = UISwipeGestureRecognizer() + result.direction = .down + result.addTarget(self, action: #selector(didSwipeDown)) + result.isEnabled = false + + return result + }() + private var bottomStackView: UIStackView? private lazy var attachmentsButton: ExpandingAttachmentsButton = { let result = ExpandingAttachmentsButton(delegate: delegate) @@ -227,6 +236,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M autoresizingMask = .flexibleHeight addGestureRecognizer(tapGestureRecognizer) + addGestureRecognizer(swipeGestureRecognizer) // Background & blur let backgroundView = UIView() @@ -454,6 +464,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self.accessibilityIdentifier = updatedInputState.accessibility?.identifier self.accessibilityLabel = updatedInputState.accessibility?.label tapGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes == .none) + swipeGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes != .none) inputState = updatedInputState disabledInputLabel.text = (updatedInputState.message ?? "") disabledInputLabel.accessibilityIdentifier = updatedInputState.messageAccessibility?.identifier @@ -630,6 +641,10 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M @objc private func characterLimitLabelTapped() { delegate?.handleCharacterLimitLabelTapped() } + + @objc private func didSwipeDown() { + inputTextView.resignFirstResponder() + } // MARK: - Convenience From e0bcf03b4e92e173cc6f38ca8c4229ccd07bc33e Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 15 Sep 2025 08:37:19 +0800 Subject: [PATCH 198/244] Code clean ups --- .../ConversationVC+Interaction.swift | 19 ++++++++++------- Session/Conversations/ConversationVC.swift | 21 ++++--------------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e565a4bfbf..818a233b59 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2250,19 +2250,24 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) + // If the `MessageInfoViewController` is visible then we want to show the keyboard after + // the pop transition completes (and don't want to delay triggering the completion closure) + let messageInfoScreenVisible: Bool = (self.navigationController?.viewControllers.last is MessageInfoViewController) + + guard !messageInfoScreenVisible else { + if self.isShowingSearchUI == true { self.willManuallyCancelSearchUI() } + self.hasPendingInputKeyboardPresentationEvent = true + completion?() + return + } + // Add delay before doing any ui updates // Delay added to give time for long press actions to dismiss let delay = completion == nil ? 0 : ContextMenuVC.dismissDuration DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } - - if self?.checkIfEventWasTriggerWhileNotVisible() == true { - self?.hasPendingInputKeyboardPresentationEvent = true - } else { - _ = self?.snInputView.becomeFirstResponder() - } - + _ = self?.snInputView.becomeFirstResponder() completion?() } } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b32a17a57f..fd52a451dc 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -588,7 +588,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Show inputview keyboard if self?.hasPendingInputKeyboardPresentationEvent == true { - self?.makeInputViewFirstResponder() + // Added 0.1 delay to remove inputview stutter animation glitch while keyboard is animating up + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + _ = self?.snInputView.becomeFirstResponder() + } self?.hasPendingInputKeyboardPresentationEvent = false } } @@ -1657,22 +1660,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa completion?() } } - - private func makeInputViewFirstResponder() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.inputAccessoryView?.becomeFirstResponder() - } - } - - func checkIfEventWasTriggerWhileNotVisible() -> Bool { - // Delegate reply action triggered by MessageInfoViewController - if let navigationStack = self.navigationController?.viewControllers { - if navigationStack.contains(where: { $0 is ConversationVC }) && navigationStack.last is MessageInfoViewController { - return true - } - } - return false - } // MARK: - UITableViewDataSource From c90b57dba3a1c9ac24ced6876d9b71e687cf85ef Mon Sep 17 00:00:00 2001 From: mpretty-cyro <15862619+mpretty-cyro@users.noreply.github.com> Date: Mon, 15 Sep 2025 00:41:40 +0000 Subject: [PATCH 199/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 5536 ++++++++++++++++- 1 file changed, 5512 insertions(+), 24 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 5e0dfa04f5..31da116616 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -18625,6 +18625,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی پرۆمۆشنی ئەدمین" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی پرۆمۆشنی ئەدمین" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -18653,6 +18681,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender adminforfremmelse" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender adminforfremmelser" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -20930,11 +20986,65 @@ "value" : "Avto-qaranlıq rejimi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatický tmavý režim" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatischer Dunkler Modus" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto Dark Mode" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mode sombre automatique" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatische nachtmodus" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mod întunecat automat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматический тёмный режим" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk mörkt läge" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik karanlık tema" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматичний темний режим" + } } } }, @@ -28195,7 +28305,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Ícono de la app" + "value" : "Ícono de la Aplicación" } }, "es-ES" : { @@ -28252,6 +28362,18 @@ "value" : "앱 아이콘" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆنی ئەپ" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆنی ئەپ" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -28666,7 +28788,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "El ícono y nombre alternativos de la app se muestran en la pantalla principal y el cajón de aplicaciones." + "value" : "Ícono y nombre alternativos para la aplicación se muestran en la pantalla de inicio y en el menú de aplicaciones." } }, "es-ES" : { @@ -29143,7 +29265,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "El ícono alternativo de la app se muestra en la pantalla principal y en la biblioteca de apps. El nombre de la app seguirá apareciendo como \"{app_name}\"." + "value" : "El ícono alternativo para la aplicación se muestra en la pantalla de inicio y en el menú de aplicaciones. El nombre de la aplicación seguirá apareciendo como '{app_name}'." } }, "es-ES" : { @@ -29304,7 +29426,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar ícono alternativo de la app" + "value" : "Usar un ícono alternativo para la aplicación" } }, "es-ES" : { @@ -29477,7 +29599,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar ícono y nombre alternativos de la app" + "value" : "Usar un ícono y nombre alternativos para la aplicación" } }, "es-ES" : { @@ -29650,7 +29772,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccionar ícono alternativo de la app" + "value" : "Seleccione un ícono alternativo para la aplicación" } }, "es-ES" : { @@ -29701,6 +29823,18 @@ "value" : "대체 앱 아이콘을 선택" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆن ئەپی جێگرەوە هەڵبژێرە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆن ئەپی جێگرەوە هەڵبژێرە" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -30163,7 +30297,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "MeetingSE" + "value" : "Eventos" } }, "es-ES" : { @@ -30971,11 +31105,35 @@ "value" : "{app_pro} nişanı" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznak {app_pro}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Abzeichen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Badge" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Badge" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Значок {app_pro}" + } } } }, @@ -63414,6 +63572,12 @@ "value" : "Dadrwystro'r cyswllt hwn i anfon neges" } }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fjern blokering af denne kontakt for at sende en besked" + } + }, "de" : { "stringUnit" : { "state" : "translated", @@ -63432,6 +63596,12 @@ "value" : "Unblock this contact to send a message" } }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malbloki tiun kontakton por sendi mesaĝon" + } + }, "es-419" : { "stringUnit" : { "state" : "translated", @@ -63444,24 +63614,60 @@ "value" : "Desbloquea este contacto para enviar mensajes." } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumi saatmiseks eemalda selle kontakti blokeering" + } + }, "eu" : { "stringUnit" : { "state" : "translated", "value" : "Kontaktu hau desblokeatu mezu bat bidaltzeko" } }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "برای ارسال پیام،‌ ابتدا این مخاطب را از مسدود بودن درآورید!" + } + }, "fi" : { "stringUnit" : { "state" : "translated", "value" : "Lähettääksesi viestin tälle yhteystiedolle sinun tulee ensin poistaa asettamasi esto." } }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "I-unblock ang contact na ito para magpadala ng mensahe" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Débloquez ce contact pour envoyer un message" } }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviar unha mensaxe" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cire katanga wannan saduwa don aika saƙo" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בטל חסימה של איש קשר זה כדי לשלוח הודעה" + } + }, "hi" : { "stringUnit" : { "state" : "translated", @@ -63510,6 +63716,18 @@ "value" : "შეტყობინების გაგზავნისთვის ბლოკი მოხსენით" } }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "ដោះការហាមឃាត់លេខទំនាក់ទំនងនេះ ដើម្បីផ្ញើសារ" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಸಂದೇಶವೊಂದನ್ನು ಕಳುಹಿಸಲು ಈ ಸಂಪರ್ಕವನ್ನು ಬ್ಲಾಕ್ ಮಾಡಿ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -63525,7 +63743,31 @@ "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "ئەم پەیوەندە لابردن بۆ بریتیە لە ناردنی پەیامێک." + "value" : "Ji bo şandina peyamê vê bloka vî kontaktê rake" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sazaamu omukozesa kuno okusindika obubaka" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atblokuokite šį kontaktą, kad išsiųstumėte žinutę" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atbloķējiet šo kontaktu, lai nosūtītu ziņojumu" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирај го овој контакт за да испратиш порака" } }, "mn" : { @@ -63558,6 +63800,12 @@ "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." } }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "सन्देश पठाउन यो सम्पर्क अनब्लक गर्नुहोस्।" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -63567,7 +63815,7 @@ "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." + "value" : "Opphev blokkeringen på denne kontakten for å sende en melding" } }, "ny" : { @@ -63576,12 +63824,24 @@ "value" : "Pokankha Lamulo Llitsa lemba uthenga" } }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਸੁਨੇਹਾ ਭੇਜਣ ਲਈ ਇਸ ਸੰਪਰਕ ਨੂੰ ਅਨਬਲੌਕ ਕਰੋ।" + } + }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odblokuj ten kontakt, aby wysłać wiadomość" } }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د پیغام استولو لپاره له دې اړیکې بې بندیز وکړئ" + } + }, "pt-BR" : { "stringUnit" : { "state" : "translated", @@ -63606,24 +63866,72 @@ "value" : "Разблокируйте этот контакт, чтобы отправить сообщение" } }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odblokirajte ovog kontakta da biste poslali poruku" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "පණිවිඩය යැවීමට මෙම සබඳතාවය අනවහිර කරන්න" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pre odoslanie správy kontakt odblokujte" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Za pošiljanje sporočila morate najprej odblokirati ta stik" + } + }, "sq" : { "stringUnit" : { "state" : "translated", "value" : "Që t’i dërgohet një mesazh, zhbllokojeni këtë kontakt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирајте дописника да би послали поруку" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирајте дописника да би послали поруку" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Avblockera denna kontakt för att skicka meddelanden." } }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ondolea kizuizi kwa mawasiliano haya kutuma ujumbe" + } + }, "ta" : { "stringUnit" : { "state" : "translated", "value" : "ஒரு செய்தியை அனுப்ப இந்த தொடர்பை விடுவிக்கவும்." } }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "సందేశాన్ని పంపడానికి ఈ పరిచయాన్ని అనుమతించు" + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -63636,6 +63944,24 @@ "value" : "Розблокувати контакт для надсилання повідомлення." } }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیغام بھیجنے کے لیے اس رابطے کو ان بلاک کریں" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xabar yuborish uchun ushbu kontaktni blokdan chiqaring" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở khóa người (liên lạc) này để gởi thông báo" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -65108,11 +65434,47 @@ "value" : "Əngəllənmiş kontaktları görün və idarə edin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit a spravovat blokované kontakty." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blockierte Kontakte anzeigen und verwalten." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "View and manage blocked contacts." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekijk en beheer geblokkeerde contacten." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Просматривайте и управляйте списком заблокированных контактов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa och hantera blockerade kontakter." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переглядайте та керуйте заблокованими контактами." + } } } }, @@ -78530,11 +78892,41 @@ "value" : "Beta zənglərini istifadə edərkən IP-niz zəng tərəfdaşınıza və {session_foundation} serverinə görünür." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkce hlasových hovorů, která je nyní ve vývojové fázi (beta), odhalí vaši IP adresu těm, se kterými si voláte a také {session_foundation} serveru." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uw IP is zichtbaar voor uw oproep partner en een {session_foundation} server tijdens het gebruik van bètagesprekken." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Din IP är synlig för din samtalspartner och en {session_foundation}-server när du använder beta-samtal." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Під час здійснення бета-викликів Ваш IP може побачити співрозмовник та сервер {session_foundation}." + } } } }, @@ -83361,11 +83753,29 @@ "value" : "Planı ləğv et" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zrušit tarif" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Cancel Plan" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement annuleren" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скасувати тарифний план" + } } } }, @@ -83378,11 +83788,53 @@ "value" : "Dəyişdir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změnit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändern" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzigen" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimba" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити" + } } } }, @@ -83874,11 +84326,41 @@ "value" : "{app_name} üçün parolunuzu dəyişdirin. Daxili olaraq saxlanılmış verilər, yeni parolunuzla təkrar şifrələnəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změňte své heslo pro {app_name}. Lokálně uložená data budou znovu zašifrována pomocí vašeho nového hesla." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig je wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met je nieuwe wachtwoord." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Измените пароль для {app_name}. Локально сохранённые данные будут повторно зашифрованы с использованием нового пароля." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra ditt lösenord för {app_name}. Lokalt lagrad data kommer att krypteras om med ditt nya lösenord." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити ваш пароль для {app_name}. Локально збережені дані будуть наново шифровані з застосуванням нового паролю." + } } } }, @@ -98509,12 +98991,24 @@ "value" : "Introdu o descriere a comunității" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите описание сообщества" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Ange en communitybeskrivning" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введіть опис спільноти" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -105352,12 +105846,24 @@ "value" : "Introdu numele comunității" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите название сообщества" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Ange ett communitynamn" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введіть назву спільноти" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -105465,12 +105971,24 @@ "value" : "Te rugăm să introduci un nume al comunității" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, введите название сообщества" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Vänligen ange ett communitynamn" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть назву спільноти" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -113206,11 +113724,41 @@ "value" : "Yeni bir mesaj alındıqda daxili bildirişlərdə nümayiş olunacaq məzmunu seçin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyberte obsah, který se zobrazí v místních upozorněních při přijetí zprávy." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Choose the content displayed in local notifications when an incoming message is received." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies de inhoud die wordt weergegeven in lokale meldingen wanneer een inkomend bericht wordt ontvangen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выберите содержимое, отображаемое в локальных уведомлениях при получении входящего сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Välj vilket innehåll som ska visas i lokala aviseringar när ett inkommande meddelande tas emot." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обирайте, який вміст показуватиметься у сповіщеннях після отримання повідомлення." + } } } }, @@ -119037,11 +119585,47 @@ "value" : "Enter və Shift+Enter düymələrinin danışıqlarda necə işləyəcəyini təyin edin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definujte, jak budou fungovat klávesy Enter a Shift+Enter v konverzacích." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definiere, wie Eingabe- und Umschalttaste in Konversationen funktionieren." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Define how the Enter and Shift+Enter keys function in conversations." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel in hoe de toetsen Enter en Shift+Enter functioneren in gesprekken." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Определите, как будут работать клавиши Enter и Shift+Enter в переписке." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definiera hur Enter- och Skift+Enter-tangenterna fungerar i konversationer." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування дій клавіш Enter та Shift+Enter у розмовах." + } } } }, @@ -119054,11 +119638,41 @@ "value" : "SHIFT + ENTER mesajı göndərir, ENTER yeni sətrə keçir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER odešle zprávu, ENTER začne nový řádek." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "SHIFT + ENTER sends a message, ENTER starts a new line." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER verzendt een bericht, ENTER begint een nieuwe regel." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER отправляет сообщение, ENTER начинает новую строку." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER skickar ett meddelande, ENTER startar en ny rad." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER надсилає повідомлення, ENTER починає новий рядок." + } } } }, @@ -119071,11 +119685,53 @@ "value" : "ENTER mesajı göndərir, SHIFT + ENTER yeni sətrə keçir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmáčknutím ENTER se zpráva odešle, SHIFT + ENTER vytvoří nový řádek." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "ENTER sends a message, SHIFT + ENTER starts a new line." } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER verzendt een bericht, SHIFT + ENTER begint een nieuwe regel." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER для отправки, SHIFT + ENTER для новой строки." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER skickar ett meddelande, SHIFT + ENTER påbörjar en ny rad." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER надсилає повідомлення, SHIFT + ENTER починає новий рядок." + } } } }, @@ -120525,11 +121181,47 @@ "value" : "2,000-dən çox mesajı olan icmalarda 6 aydan köhnə mesajları avtomatik sil." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Z komunit automaticky mazat zprávy starší než 6 měsíců, pokud jich je více než 2000." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten älter als 6 Monate in Communities mit mehr als 2000 Nachrichten automatisch löschen." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto-delete messages older than 6 months in communities with 2000+ messages." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten ouder dan 6 maanden automatisch verwijderen in community's met meer dan 2000 berichten." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалять сообщения старше 6 месяцев в сообществах с более чем 2000 сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta bort meddelanden som är äldre än 6 månader i gemenskaper med 2000+ meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автовидалення повідомлень старших за 6 місяців у спільнотах з 2000+ повідомлень." + } } } }, @@ -121500,11 +122192,65 @@ "value" : "Enter ilə göndər" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslat klávesou Enter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit Eingabetaste senden" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Send with Enter" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter para enviar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter para enviar" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verzenden met Enter" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apasă Enter pentru a trimite" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправлять по Enter" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka med Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надіслати з Enter" + } } } }, @@ -121996,11 +122742,47 @@ "value" : "Shift+Enter ilə göndər" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslat klávesou Shift+Enter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit Umschalt- und Eingabetaste senden" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Send with Shift+Enter" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verzenden met Shift+Enter" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить с Shift+Enter" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka med Shift+Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надіслати з Shift+Enter" + } } } }, @@ -125563,11 +126345,35 @@ "value" : "Hazırkı parol" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuální heslo" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Current Password" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidig wachtwoord" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuvarande lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поточний пароль" + } } } }, @@ -125580,11 +126386,29 @@ "value" : "Hazırkı plan" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Současný tarif" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Current Plan" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidig abonnement" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поточна передплата" + } } } }, @@ -126082,11 +126906,53 @@ "value" : "Qaranlıq rejim" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tmavý režim" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dunkelmodus" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Dark Mode" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donkere modus" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mod întunecat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тёмный режим" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mörkt läge" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Темний режим" + } } } }, @@ -126326,6 +127192,12 @@ "value" : "데이터베이스 오류가 발생했습니다.

    문제 해결을 위해 애플리케이션 로그를 내보내서 공유하십시오. 실패할 경우, {app_name}을 다시 설치하고 계정을 복원하십시오." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En databasefeil har oppstått.

    Eksporter dine applikasjon logger for å dele feilsøkingen. Hvis dette ikke vellykkes, installer {app_name} på nytt og gjenopprett kontoen din." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -139724,6 +140596,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "دڵنیایت دەتەوێت ئەم پەیامە بسڕیتەوە؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "دڵنیایت دەتەوێت ئەم پەیامانە بسڕیتەوە؟" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -139752,6 +140652,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på at du vil slette denne meldingen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på at du vil slette disse meldingene?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -143834,6 +144762,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایا دڵنیای کە دەتەوێت ئەم پەیامە تەنها لەم ئامێرە بسڕیتەوە؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایا دڵنیای کە دەتەوێت ئەم پەیامە تەنها لەم ئامێرە بسڕیتەوە؟" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -148918,6 +149874,34 @@ } } }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "پرشین" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "برخی از پیام‌هایی که انتخاب کرده‌اید، نمی‌توانند از همهٔ دستگاه‌های شما پاک شوند" + } + } + } + } + } + } + }, "fi" : { "stringUnit" : { "state" : "translated", @@ -149152,6 +150136,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیامە ناتوانرێت لە هەموو ئامێرەکانت بسڕدرێتەوە" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هەندێک لەو نامانەی کە هەڵتبژاردووە ناتوانرێت لە هەموو ئامێرەکانت بسڕدرێنەوە" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -149230,6 +150242,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denne meldingen kan ikke bli slettet fra alle dine enheter" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noen av meldingene du valgte kan ikke bli slettet fra alle dine enheter" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -150766,6 +151806,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیامە بۆ هەموو کەسێک ناسڕدرێتەوە" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هەندێک لەو نامانەی کە هەڵتبژاردووە ناتوانرێت بۆ هەموو کەسێک بسڕدرێتەوە" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -150858,13 +151926,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Diese Nachricht kann nicht gelöscht werden" + "value" : "Denne meldingen kan ikke bli slettet for alle" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ein paar Nachrichten könnten nicht gelöscht werden" + "value" : "Noen av meldingene du valgte kan ikke bli slettet for alle" } } } @@ -169086,11 +170154,47 @@ "value" : "Nümayiş" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Display" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weergave" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дисплей" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skärm" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зовнішній вигляд" + } } } }, @@ -187360,11 +188464,47 @@ "value" : "Yeni mesaj aldığınız zaman bildirişlər göstərilsin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit upozornění při přijetí nových zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benachrichtigungen anzeigen, wenn du neue Nachrichten erhältst." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show notifications when you receive new messages." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon meldingen wanneer je nieuwe berichten ontvangt." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать уведомления при получении новых сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa aviseringar när du får nya meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати сповіщення, коли ви отримуєте нові повідомлення." + } } } }, @@ -187467,6 +188607,12 @@ "value" : "Gillar du {app_name}?" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}'i beğendiniz mi?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -187592,6 +188738,12 @@ "value" : "Behöver förbättras {emoji}" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geliştirilmesi Gerekiyor {emoji}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -187717,6 +188869,12 @@ "value" : "Det är fantastiskt {emoji}" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Harika {emoji}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -187871,11 +189029,47 @@ "value" : "Daxil ol" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vstoupit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestätigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enter" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verder" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Войти" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увійти" + } } } }, @@ -190623,11 +191817,47 @@ "value" : "Əks-əlaqə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpětná vazba" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Feedback" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отзыв" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відгук" + } } } }, @@ -190640,11 +191870,47 @@ "value" : "Qısa anketi dolduraraq {app_name} ilə təcrübənizi paylaşın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podělte se o své zkušenosti s {app_name} vyplněním krátkého dotazníku." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teile deine Erfahrungen mit {app_name}, indem du eine kurze Umfrage ausfüllst." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Share your experience with {app_name} by completing a short survey." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deel je ervaring met {app_name} door een korte enquête in te vullen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поделитесь своим опытом использования {app_name}, пройдя короткий опрос." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dela med dig av din upplevelse med {app_name} genom att fylla i en kort undersökning." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поділіться вашим досвідом використання {app_name} пройшовши коротке опитування." + } } } }, @@ -191615,11 +192881,53 @@ "value" : "Sistem ayarlarını izlə." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Použít nastavení systému." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Systemeinstellungen übernehmen." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Follow system settings." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Systeeminstellingen volgen." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Urmărește setările sistemului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Использовать настройки системы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Följ systeminställningen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Використовувати системні налаштування." + } } } }, @@ -196474,6 +197782,12 @@ "value" : "정말로 {group_name}을 제거하시겠습니까?

    모든 멤버가 제거되고 모든 그룹 컨텐츠가 삭제됩니다." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på at du vil slette {group_name}?

    Dette vil fjerne alle medlemmer og slette alt av innholdet i gruppen." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -203862,6 +205176,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی بانگ" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی بانگه کان" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -203890,6 +205232,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender invitasjon" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender invitasjoner" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -204836,6 +206206,12 @@ "value" : "초대 상태를 알 수 없습니다" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status på invitasjonen er ukjent" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -214265,6 +215641,12 @@ "value" : "あなた{other_name} はグループに招待されました。チャット履歴が共有されました。" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "당신{other_name}이 그룹에 초대 되었습니다. 대화 내용이 공유 되었습니다." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -231952,6 +233334,18 @@ "value" : "연결 후보 처리 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "مامەڵەکردن لەگەڵ کاندیدەکانی پەیوەندی" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "مامەڵەکردن لەگەڵ کاندیدەکانی پەیوەندی" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -232508,11 +233902,47 @@ "value" : "Ümumi suallara cavab tapmaq üçün {app_name} TVS-yə baxın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odpovědi na časté otázky najdete v sekci FAQ {app_name}." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sieh dir die {app_name}-FAQ an, um Antworten auf häufig gestellte Fragen zu erhalten." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Check the {app_name} FAQ for answers to common questions." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekijk de {app_name} FAQ voor antwoorden op veelgestelde vragen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ознакомьтесь с часто задаваемыми вопросами {app_name}, чтобы найти ответы на распространённые вопросы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kolla in FAQ på {app_name} för svar på vanliga frågor." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перегляд ЧЗП {app_name} для перегляду відповідей на часті запитання." + } } } }, @@ -232998,11 +234428,59 @@ "value" : "Bir xəta bildir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nahlásit chybu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Fehler melden" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Report a Bug" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meld een bug" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raportează o eroare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщить об ошибке" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rapportera ett fel" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata Bildir" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомити про помилку" + } } } }, @@ -234943,11 +236421,47 @@ "value" : "Bu faylı saxlayın, sonra onu {app_name} gəlişdiriciləri ilə paylaşın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uložte tento soubor, poté jej sdílejte s vývojáři aplikace {app_name}." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speichere diese Datei und teile sie dann mit den {app_name} Entwicklern." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Save this file, then share it with {app_name} developers." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sla dit bestand op en deel het vervolgens met de {app_name} ontwikkelaars." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохраните этот файл, затем поделитесь им с разработчиками {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spara denna fil, dela den sedan med {app_name} utvecklarna." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збережіть цей файл, а потім надішліть його розробникам {app_name}." + } } } }, @@ -235439,11 +236953,47 @@ "value" : "{app_name} tətbiqini 80-dən çox dildə tərcümə etməyə kömək edin!" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomozte přeložit {app_name} do více než 80 jazyků!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hilf mit, {app_name} in über 80 Sprachen zu übersetzen!" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Help translate {app_name} into over 80 languages!" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Help met het vertalen van {app_name} in meer dan 80 talen!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помогите перевести {app_name} на более чем 80 языков!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hjälp till att översätta {app_name} till över 80 språk!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Допоможіть перекласти {app_name} на більше ніж 80 мов!" + } } } }, @@ -236414,11 +237964,41 @@ "value" : "Sistem menyu çubuğunun görünməsini dəyişdir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přepínač viditelnosti lišty systémového menu." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Toggle system menu bar visibility." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zichtbaarheid systeem-menubalk in-/uitschakelen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Спрятать или показать системное меню." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Växla synlighet för systemmenyraden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видимість панелі меню." + } } } }, @@ -237711,11 +239291,29 @@ "value" : "Vacib" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Důležité" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Important" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belangrijk" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Важливо" + } } } }, @@ -240108,6 +241706,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتکردن شکستی هێنا" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتەکان شکستی هێنا" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -240136,6 +241762,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjon mislykket" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjoner mislykket" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -240943,6 +242597,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتنامەکە نەتوانرا بنێردرێت. حەز دەکەیت هەوڵی دووبارە بدەیتەوە؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتنامەکان نەتوانرا بنێردرێت. حەز دەکەیت هەوڵی دووبارە بدەیتەوە؟" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -240971,6 +242653,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjon kunne ikke bli sent. Vil du prøve på nytt?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjoner kunne ikke bli sent. Vil du prøve på nytt?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -242277,6 +243987,12 @@ "state" : "translated", "value" : "Launch {app_name} automatically when your computer starts up." } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматично запускати {app_name} під час увімкнення компʼютера." + } } } }, @@ -242289,11 +244005,23 @@ "value" : "Açılışda başlat" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spuštění při startu systému" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Launch on Startup" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автозапуск при старті системи" + } } } }, @@ -242311,6 +244039,12 @@ "state" : "translated", "value" : "This setting is managed by your system on Linux. To enable automatic startup, add {app_name} to your startup applications in system settings." } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цим параметром у Linux керує ваша система. Щоб увімкнути автоматичний запуск, додайте {app_name} до програм автозапуску в системних параметрах." + } } } }, @@ -244497,7 +246231,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "El historial de chat no se transferirá al nuevo grupo. Todavía puedes ver todo el historial de chat en tu grupo antiguo." + "value" : "El historial de chat no se transferirá al nuevo grupo. Aún puedes ver todo el historial de chat en tu antiguo grupo." } }, "es-ES" : { @@ -252277,11 +254011,35 @@ "value" : "Keçidlər" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odkazy" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Links" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koppelingen" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Länkar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посилання" + } } } }, @@ -258054,11 +259812,35 @@ "value" : "Log-lar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logy" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Logs" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logboeken" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loggar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Журнали" + } } } }, @@ -258232,11 +260014,29 @@ "value" : "{pro} - idarə et" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spravovat {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Manage {pro}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} beheren" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування {pro}" + } } } }, @@ -268468,11 +270268,47 @@ "value" : "Menyu çubuğu" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panel menu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menüleiste" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Menu Bar" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menubalk" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Панель меню" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyrad" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Панель меню" + } } } }, @@ -269101,11 +270937,47 @@ "value" : "Mesajı kopyala" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovat zprávu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht kopieren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Copy Message" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bericht kopiëren" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копировать текст сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiera meddelande" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копіювати повідомлення" + } } } }, @@ -278084,6 +279956,24 @@ } } }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har fått en ny melding i {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har fått %lld nye meldinger i {group_name}." + } + } + } + } + }, "nl" : { "variations" : { "plural" : { @@ -292386,6 +294276,24 @@ } } }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meldinger har en tegngrense på {limit} tegn. Du har %lld tegn igjen." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meldinger har en tegngrense på {limit} tegn. Du har %lld tegn igjen." + } + } + } + } + }, "nl" : { "variations" : { "plural" : { @@ -293292,11 +295200,35 @@ "value" : "Yeni parol" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nové heslo" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "New Password" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieuw wachtwoord" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nytt Lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Новий пароль" + } } } }, @@ -293788,11 +295720,29 @@ "value" : "Növbəti addımlar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Další kroky" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Next Steps" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volgende stappen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подальші кроки" + } } } }, @@ -299301,11 +301251,47 @@ "value" : "Bildiriş nümayişi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazení upozornění" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notification Display" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificatie weergave" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vizualizare notificări" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отображение уведомлений" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviseringsvisning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сповіщення" + } } } }, @@ -302198,11 +304184,47 @@ "value" : "Mesajın göndərənin adı və mesaj məzmununun bir önizləməsi nümayiş olunsun." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit jméno odesílatele a náhled obsahu zprávy." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeigt den Namen des Absenders und eine Vorschau des Nachrichteninhalts an." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Display the sender's name and a preview of the message content." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon de naam van de afzender en een voorbeeld van de berichtinhoud." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать имя отправителя и предварительный просмотр содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa avsändarens namn och en förhandsvisning av meddelandets innehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати ім’я відправника та стислий вміст повідомлення." + } } } }, @@ -302215,11 +304237,41 @@ "value" : "Heç bir mesaj məzmunu olmadan yalnız mesajı göndərənin adı nümayiş olunsun." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit pouze jméno odesílatele bez obsahu zprávy." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Display only the sender's name without any message content." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon alleen de naam van de afzender zonder enige berichtinhoud." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отображать только имя отправителя без содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa endast avsändarens namn utan något meddelandeinnehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати лише ім'я відправника без вмісту повідомлення." + } } } }, @@ -303824,11 +305876,47 @@ "value" : "Mesajı göndərənin adı və ya mesajın məzmunu olmadan ümumi {app_name} bildirişi nümayiş olunsun." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit obecné oznámení {app_name} bez jména odesílatele a obsahu zprávy." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige eine allgemeine {app_name}-Benachrichtigung ohne Namen des Absenders oder Nachrichteninhalt an." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Display a generic {app_name} notification without the sender's name or message content." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon een algemene {app_name} melding zonder de naam van de afzender of de inhoud van het bericht." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать общие уведомления {app_name} без имени отправителя и содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa en generell {app_name}-avisering utan avsändarens namn eller meddelandets innehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати типове сповіщення {app_name} без імені відправника або вмісту повідомлення." + } } } }, @@ -307164,11 +309252,47 @@ "value" : "Yeni mesaj aldığınız zaman bir səs oxudulsun." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přehrát zvuk při přijetí nových zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Ton abspielen, wenn neue Nachrichten empfangen werden." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Play a sound when you receive receive new messages." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speel een geluid af wanneer je nieuwe berichten ontvangt." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Воспроизводить звук при получении новых сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spela upp ett ljud när du får nya meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відтворювати звук, коли ви отримуєте нові повідомлення." + } } } }, @@ -324280,11 +326404,23 @@ "value" : "{device_type} cihazınızda" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Na vašem zařízení {device_type}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "On your {device_type} device" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Op je {device_type} apparaat" + } } } }, @@ -324297,11 +326433,23 @@ "value" : "Başda qeydiyyatdan keçdiyiniz {platform_account} hesabına giriş etdiyiniz {device_type} cihazından bu {app_name} hesabını açın. Sonra planınızı {app_pro} ayarları vasitəsilə dəyişdirin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřete tento účet {app_name} na zařízení {device_type}, které je přihlášeno do účtu {platform_account}, pomocí kterého jste se původně zaregistrovali. Poté změňte svůj tarif v nastavení {app_pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the {app_pro} settings." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open dit {app_name} account op een {device_type} apparaat waarop je bent aangemeld met het {platform_account} waarmee je je oorspronkelijk hebt geregistreerd. Wijzig vervolgens je abonnement via de instellingen van {app_pro}." + } } } }, @@ -328643,11 +330791,23 @@ "value" : "{platform_store} veb saytını aç" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřít webovou stránku {platform_store}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open {platform_store} Website" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open de {platform_store} website" + } } } }, @@ -329264,11 +331424,53 @@ "value" : "Parol" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heslo" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Password" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parolă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль" + } } } }, @@ -329766,11 +331968,65 @@ "value" : "Parolunuz dəyişdirilib. Lütfən onu güvəndə saxlayın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password has been changed. Please keep it safe." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Passwort wurde geändert. Bitte bewahre es sicher auf." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been changed. Please keep it safe." } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu contraseña ha sido cambiada. Por favor, guárdala en un lugar seguro." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu contraseña ha sido cambiada. Por favor, guárdala en un lugar seguro." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uw wachtwoord is gewijzigd. Hou het veilig." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parola ta a fost modificata. Securizați-va parola." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль был изменен. Пожалуйста, храните его в безопасном месте." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt lösenord har ändrats. Håll det säkert." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль змінено. Будь ласка, зберігайте його надійно." + } } } }, @@ -329783,11 +332039,47 @@ "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu dəyişdir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změnit heslo pro odemykání {app_name}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change the password required to unlock {app_name}." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obligation de changer le mot de passe pour déverrouiller {app_name}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig het wachtwoord dat nodig is om {app_name} te ontgrendelen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Измените пароль, необходимый для разблокировки {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra lösenordet som krävs att låsa upp {app_name}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити пароль, необхідний для розблокування {app_name}." + } } } }, @@ -330285,11 +332577,59 @@ "value" : "Parol yarat" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vytvořit heslo" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort erstellen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Create Password" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Créer un mot de passe" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord aanmaken" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Создать пароль" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skapa lösenord" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şifre Oluştur" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Створити пароль" + } } } }, @@ -332781,12 +335121,24 @@ "value" : "Parola trebuie să aibă între {min} și {max} caractere." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль должен содержать от {min} до {max} символов" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Lösenordet måste vara mellan {min} och {max} tecken långt" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль має містити від {min} до {max} символів" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -334247,11 +336599,53 @@ "value" : "Yeni parolu təsdiqlə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potvrďte nové heslo" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neues Passwort wiederholen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Confirm New Password" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevestig nieuwe wachtwoord" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmați noua parolă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтвердите новый пароль" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekräfta nytt lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підтвердити новий пароль" + } } } }, @@ -334743,11 +337137,53 @@ "value" : "Parolunuz silinib." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše heslo bylo odebráno." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Passwort wurde entfernt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been removed." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uw wachtwoord is verwijderd." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parola ta a fost ștearsă." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль удалён." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt lösenord har tagits bort." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль видалено." + } } } }, @@ -334760,11 +337196,41 @@ "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu sil" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odebrat heslo pro odemykání {app_name}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remove the password required to unlock {app_name}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder het wachtwoord dat nodig is om {app_name} te ontgrendelen" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить пароль, необходимый для разблокировки {app_name}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta bort lösenordet som krävs för att låsa upp {app_name}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видалити пароль, потрібний для розблокування {app_name}" + } } } }, @@ -334777,11 +337243,53 @@ "value" : "Parollar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesla" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Passwords" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parole" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паролі" + } } } }, @@ -335273,11 +337781,53 @@ "value" : "Parolunuz təyin edilib. Lütfən onu güvəndə saxlayın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše heslo bylo nastaveno. Pečlivě si jej uložte." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Passwort wurde festgelegt. Bitte bewahre es sicher auf." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been set. Please keep it safe." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uw wachtwoord is ingesteld. Hou het veilig." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parola ta a fost setata. Securizați-va parola." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль установлен. Пожалуйста, храните его в безопасном месте." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt lösenord har angetts. Håll det säkert." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль встановлено. Будь ласка, зберігайте його надійно." + } } } }, @@ -335290,11 +337840,41 @@ "value" : "Açılışda {app_name} kilidini açmaq üçün parol tələb edilsin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyžadovat heslo k odemknutí {app_name} při spuštění." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Require password to unlock {app_name} on startup." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord vereisen om {app_name} bij het opstarten te ontgrendelen." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требовать пароль для разблокировки {app_name} при запуске." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kräv ett lösenord för ett låsa upp {app_name} vid start." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимагати пароль для розблокування {app_name} при вході." + } } } }, @@ -335307,11 +337887,53 @@ "value" : "12 xarakterdən uzun" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delší než 12 znaků" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Länger als 12 Zeichen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Longer than 12 characters" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langer dan 12 tekens" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mai mare de 12 caractere" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Длина больше 12 символов" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Längre än 12 tecken" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Довший 12 символів" + } } } }, @@ -335324,11 +337946,53 @@ "value" : "Bir rəqəm ehtiva etməlidir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obsahuje číslici" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enthält eine Zahl" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Includes a number" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevat een cijfer" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Include un număr" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Содержит цифру" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inkluderar en siffra" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Містить цифру" + } } } }, @@ -335341,11 +338005,53 @@ "value" : "Bir kiçik hərf ehtiva etməlidir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obsahuje malé písmeno" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enthält einen Kleinbuchstaben" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Includes a lowercase letter" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevat een kleine letter" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Include o literă mică" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Содержит строчную букву" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inkluderar en liten bokstav" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Містить літеру нижнього регістру" + } } } }, @@ -335358,11 +338064,23 @@ "value" : "Bir simvol daxildir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obsahuje symbol" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Includes a symbol" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevat een symbool" + } } } }, @@ -335375,11 +338093,53 @@ "value" : "Bir böyük hərf ehtiva etməlidir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obsahuje velké písmeno" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enthält einen Großbuchstaben" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Includes a uppercase letter" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevat een hoofdletter" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Include o literă mare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Содержит заглавную букву" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inkluderar en stor bokstav" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Містить літеру верхнього регістру" + } } } }, @@ -335392,11 +338152,53 @@ "value" : "Parol gücü göstəricisi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indikátor síly hesla" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwortstärke-Anzeige" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Password Strength Indicator" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoordsterkte indicator" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indicator de parolă puternică" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Индикатор надёжности пароля" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indikator för lösenordsstyrka" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Індикатор надійності паролю" + } } } }, @@ -335409,11 +338211,47 @@ "value" : "Güclü bir parol təyin etmək, cihazınız itsə və ya oğurlansa belə mesajlarınızı və qoşmalarınızı qorumağa kömək edir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavení silného hesla pomáhá chránit vaše zprávy a přílohy v případě ztráty nebo odcizení zařízení." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Setting a strong password helps protect your messages and attachments if your device is ever lost or stolen." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Een sterk wachtwoord helpt je berichten en bijlagen te beschermen als je apparaat ooit verloren raakt of wordt gestolen." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setarea unei parole puternice ajută la protejarea mesajelor și fișierelor în cazul pierderii sau furtului dispozitivului dumneavoastră." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надёжный пароль помогает защитить ваши сообщения и вложения в случае утери или кражи устройства." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Att skapa ett starkt lösenord hjälper till att skydda dina meddelanden och bilagor om din enhet skulle gå förlorad eller bli stulen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Встановлення надійного пароля допомагає захистити ваші повідомлення та вкладення у разі втрати або крадіжки пристрою." + } } } }, @@ -335950,7 +338788,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Cambio de permiso" + "value" : "Cambiar Permisos" } }, "es-ES" : { @@ -339908,11 +342746,65 @@ "value" : "Pəncərəni bağladığınız zaman {app_name} arxaplanda çalışmağa davam edir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} pokračuje v běhu na pozadí, když zavřete okno." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} läuft im Hintergrund weiter, wenn du das Fenster schließt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} continues running in the background when you close the window." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} continue à fonctionner en arrière-plan lorsque vous fermez la fenêtre." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} blijft op de achtergrond draaien wanneer je het venster sluit." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} продолжит работать в фоновом режиме даже после закрытия окна." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} fortsätter köras i bakgrunden när du stänger fönstret." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} pencereyi kapattığınızda arka planda çalışmaya devam eder." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} продовжує працювати у фоновому режимі, коли ви його згортає." + } } } }, @@ -340443,7 +343335,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Se requiere acceso a la red local para facilitar las llamadas. Activa el permiso de \"Red local\" en Configuración para continuar." + "value" : "Se requiere acceso a la Red Local para realizar llamadas. En Configuración, active el permiso de \"Red Local\" para continuar." } }, "es-ES" : { @@ -340914,7 +343806,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Permitir acceso a la red local para facilitar llamadas de voz y video." + "value" : "Permitir acceso a la red local para realizar llamadas de voz y video." } }, "es-ES" : { @@ -341075,7 +343967,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Red local" + "value" : "Red Local" } }, "es-ES" : { @@ -349469,11 +352361,29 @@ "value" : "Üstəgəl daha çoxu gəlir..." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus načte další..." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Plus Loads More..." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus laad meer..." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Та багато іншого..." + } } } }, @@ -349486,11 +352396,29 @@ "value" : "{pro} üçün yeni özəlliklər tezliklə gəlir. {icon} {pro} Yol Xəritəsində yenilikləri kəşf edin" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nové funkce {pro} již brzy. Podívejte se, co chystáme, na plánu vývoje {pro} {icon}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "New features coming soon to {pro}. Discover what's next on the {pro} Roadmap {icon}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieuwe functies komen binnenkort naar {pro}. Ontdek wat er komt op de {pro} Roadmap {icon}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нові можливості незабаром виникнуть у {pro}. Пізнай, що буде далі, у дороговказі {pro} {icon}" + } } } }, @@ -349503,11 +352431,65 @@ "value" : "Tərcihlər" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Předvolby" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Preferences" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencias" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencias" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorkeuren" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferințe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпочтения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inställningar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування" + } } } }, @@ -349999,11 +352981,47 @@ "value" : "Bildirişi önizlə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Náhled upozornění" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benachrichtigungsvorschau" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Preview Notification" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorbeeldmelding" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпросмотр уведомления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förhandsgranska avisering" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попередній перегляд сповіщень" + } } } }, @@ -350141,11 +353159,29 @@ "value" : "Hər şey hazırdır!" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vše je nastaveno!" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You're all set!" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alles is geregeld!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово!" + } } } }, @@ -350158,11 +353194,29 @@ "value" : "{app_pro} planınız güncəlləndi! Hazırkı {pro} planınız avtomatik olaraq {date} tarixində yeniləndiyi zaman ödəniş haqqı alınacaq." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} byl aktualizován! Účtování proběhne při automatickém obnovení vašeho aktuálního tarifu {pro} dne {date}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan was updated! You will be billed when your current {pro} plan is automatically renewed on {date}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is bijgewerkt! Je wordt gefactureerd wanneer je huidige {pro} abonnement automatisch wordt verlengd op {date}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Твою підписку {app_pro} оновлено. {date} коли підписку {pro} буде подовжено, тоді й стягнуть гроші." + } } } }, @@ -350652,12 +353706,24 @@ "value" : "Poză de profil animată" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Анимированное изображение профиля" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Animerad visningsbild" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil Onur Seçin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -350818,11 +353884,29 @@ "value" : "Animasiyalı ekran şəkilləri" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animované zobrazované obrázky" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Animated Display Pictures" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geanimeerde profielfoto's" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Анімовані зображення облікового запису" + } } } }, @@ -350835,11 +353919,29 @@ "value" : "Animasiyalı GIF və WebP təsvirlərini ekran şəklini olaraq təyin edin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavte si animované obrázky GIF a WebP jako svůj zobrazovaný profilový obrázek." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Set animated GIFs and WebP images as your display picture." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel geanimeerde GIF's en WebP-afbeeldingen in als je profielfoto." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Встановлювати анімовані зображення GIF та WebP як ваше зображення облікового запису." + } } } }, @@ -350983,11 +354085,29 @@ "value" : "{pro}, {time} tarixində avto-yenilənir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} se automaticky obnoví za {time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} auto-renewing in {time}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} wordt automatisch verlengd over {time}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} автоматично оновиться за {time}" + } } } }, @@ -351000,11 +354120,29 @@ "value" : "{pro} nişanı" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznak {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Badge" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} badge" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Значок {pro}" + } } } }, @@ -351017,11 +354155,29 @@ "value" : "Nişanlar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaky" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Badges" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badges" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позначки" + } } } }, @@ -351034,11 +354190,29 @@ "value" : "Ekran adınızın yanında eksklüziv bir nişanla {app_name} tətbiqini dəstəklədiyinizi göstərin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyjádřete svou podporu {app_name} pomocí exkluzivního odznaku vedle svého zobrazovaného jména." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show your support for {app_name} with an exclusive badge next to your display name." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon je steun voor {app_name} met een exclusieve badge naast je schermnaam." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Продемонструйте свою підтримку {app_name} з ексклюзивним значком поруч з власним іменем." + } } } }, @@ -351073,6 +354247,46 @@ } } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaky odeslány" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaků odesláno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznak odeslán" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaků odesláno" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -351100,6 +354314,34 @@ } } } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Merke Sendt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Merker Sendt" + } + } + } + } + } + } } } }, @@ -351112,11 +354354,29 @@ "value" : "{app_pro} nişanını digər istifadəçilərə göstər" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit odznak {app_pro} ostatním uživatelům" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show {app_pro} badge to other users" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon het {app_pro} badge aan andere gebruikers" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати значок {app_pro} іншим користувачам" + } } } }, @@ -351129,11 +354389,29 @@ "value" : "{price} - illik haqq" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} účtováno ročně" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{price} Billed Annually" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} Jaarlijks gefactureerd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} сплата щорічно" + } } } }, @@ -351146,11 +354424,29 @@ "value" : "{price} - aylıq haqq" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} účtováno měsíčně" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{price} Billed Monthly" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} Maandelijks gefactureerd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} сплата щомісячно" + } } } }, @@ -351163,11 +354459,29 @@ "value" : "{price} - rüblük haqq" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} účtováno čtvrtletně" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{price} Billed Quarterly" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} per kwartaal gefactureerd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} сплата щоквартально" + } } } }, @@ -351579,11 +354893,23 @@ "value" : "{platform_account} geri ödəmə tələbinizi emal edir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} zpracovává vaši žádost o vrácení peněz" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{platform_account} is processing your refund request" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} verwerkt je restitutieverzoek" + } } } }, @@ -351596,11 +354922,29 @@ "value" : "Hazırkı planınızda artıq\r\ntam {app_pro} qiymətinin {percent}% endirimi mövcuddur." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš aktuální tarif je již zlevněn o {percent} % z plné ceny {app_pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your current plan is already discounted by {percent}% of the full {app_pro} price." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je huidige abonnement is al met {percent}% korting ten opzichte van de volledige {app_pro} prijs." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "На поточну підписку ти вже маєш знижку {percent}% від загальної ціни {app_pro}." + } } } }, @@ -351613,11 +354957,29 @@ "value" : "Müddəti bitib" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Platnost vypršela" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Expired" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlopen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підписка сплила" + } } } }, @@ -351630,11 +354992,29 @@ "value" : "Təəssüf ki, {pro} planınızın müddəti bitib. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün yeniləyin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bohužel, váš tarif {pro} vypršel. Obnovte jej, abyste nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unfortunately, your {pro} plan has expired. Renew to keep accessing the exclusive perks and features of {app_pro}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Helaas is je {pro} abonnement verlopen. Verleng om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "На жаль, підписка {pro} сплила. Онови її задля збереження переваг і можливостей {app_pro}." + } } } }, @@ -351647,11 +355027,29 @@ "value" : "Tezliklə bitir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brzy vyprší" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Expiring Soon" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verloopt binnenkort" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Невдовзі спливе підписка" + } } } }, @@ -351664,11 +355062,29 @@ "value" : "{pro} planınızın müddəti {time} vaxtında bitir. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün planınızı güncəlləyin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {pro} vyprší za {time}. Aktualizujte si tarif, abyste i nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {pro} plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of {app_pro}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {pro} abonnement verloopt over {time}. Werk je abonnement bij om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підписка {pro} спливе {time}. Онови підписку задля збереження переваг і можливостей {app_pro}." + } } } }, @@ -351681,11 +355097,23 @@ "value" : "{pro}, {time} vaxtında başa çatır" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} vyprší za {time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} expiring in {time}" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} спливає за {time}" + } } } }, @@ -351698,11 +355126,29 @@ "value" : "{pro} TVS" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} FAQ" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} FAQ" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} FAQ" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} ЧАП" + } } } }, @@ -351715,11 +355161,29 @@ "value" : "{app_name} TVS-da tez-tez verilən suallara cavab tapın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Najděte odpovědi na časté dotazy v nápovědě {app_name}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Find answers to common questions in the {app_name} FAQ." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vind antwoorden op veelgestelde vragen in de {app_name} FAQ." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відповіді на загальні запитання знайдеш у ЧаПи {app_name}." + } } } }, @@ -352399,11 +355863,29 @@ "value" : "{pro} özəllikləri" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkce {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Features" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} functies" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Можливості {pro}" + } } } }, @@ -355380,6 +358862,12 @@ "value" : "Grup activat" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Группа активирована" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -355499,6 +358987,12 @@ "value" : "Acest grup are capacitate extinsă! Poate susține până la 300 de membri deoarece un administrator de grup are" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У этой группы увеличена вместимость! Теперь она поддерживает до 300 участников, потому что администратор группы активировал" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -355556,6 +359050,46 @@ } } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupiny navýšeny" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupin navýšeno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupina navýšena" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupin navýšeno" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355583,6 +359117,34 @@ } } } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Gruppe Oppgradert" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Grupper Oppgradert" + } + } + } + } + } + } } } }, @@ -355595,11 +359157,29 @@ "value" : "Geri ödəniş tələbi qətidir. Əgər təsdiqlənsə, {pro} planınız dərhal ləğv ediləcək və bütün {pro} özəlliklərinə erişimi itirəcəksiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádost o vrácení peněz je konečné. Pokud bude schváleno, váš tarif {pro} bude ihned zrušen a ztratíte přístup ke všem funkcím {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Requesting a refund is final. If approved, your {pro} plan will be canceled immediately and you will lose access to all {pro} features." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het aanvragen van een terugbetaling is definitief. Indien goedgekeurd, wordt je {pro} abonnement onmiddellijk geannuleerd en verlies je de toegang tot alle {pro} functies." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимагання повернення грошей закінчено. В разі схвалення твою підписку {pro} негайно скасують і ти втратиш всі можливості {pro}." + } } } }, @@ -355696,6 +359276,12 @@ "value" : "Dimensiune mărită a atașamentului" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увеличенный размер вложений" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -355815,6 +359401,12 @@ "value" : "Lungime extinsă a mesajului" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увеличенная длина сообщения" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -355850,11 +359442,29 @@ "value" : "Daha böyük qruplar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Větší skupiny" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Larger Groups" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grotere groepen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Більші групи" + } } } }, @@ -355867,11 +359477,29 @@ "value" : "Admin olduğunuz qruplar, avtomatik olaraq 300 üzvü dəstəkləmək üçün təkmilləşdirilir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupiny, ve kterých jste správcem, jsou automaticky navýšeny na kapacitu až 300 členů." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Groups you are an admin in are automatically upgraded to support 300 members." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groepen waarvan jij beheerder bent, worden automatisch geüpgraded om 300 leden te ondersteunen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групи, у яких ви є адміністратором, автоматично оновлюються для підтримки до 300 учасників." + } } } }, @@ -355884,11 +359512,29 @@ "value" : "Daha uzun mesajlar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delší zprávy" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Longer Messages" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langere berichten" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Довші повідомлення" + } } } }, @@ -355901,11 +359547,29 @@ "value" : "Bütün danışıqlarda 10,000 xarakterə qədər mesaj göndərə bilərsiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve všech konverzacích můžete posílat zprávy až o délce 10 000 znaků." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You can send messages up to 10,000 characters in all conversations." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt berichten tot 10.000 tekens verzenden in alle gesprekken." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви можете надсилати повідомлення до 10 000 символів у всіх розмовах." + } } } }, @@ -355940,6 +359604,46 @@ } } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delší zprávy odeslány" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delších zpráv odesláno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delší zpráva odeslána" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delších zpráv odesláno" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355967,6 +359671,34 @@ } } } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Lengre Melding Sendt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Lengre Meldinger Sendt" + } + } + } + } + } + } } } }, @@ -356063,6 +359795,12 @@ "value" : "Acest mesaj a folosit următoarele funcționalități {app_pro}:" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это сообщение использовало следующие функции {app_pro}:" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -357041,6 +360779,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelse mislykket" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelser mislykket" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -357848,6 +361614,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelsen kunne ikke bli påført. Vil du prøve på nytt?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelser kunne ikke bli påført. Vil du prøve på nytt?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -358191,11 +361985,23 @@ "value" : "{percent}% endirim" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sleva {percent} %" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{percent}% Off" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% korting" + } } } }, @@ -358230,6 +362036,46 @@ } } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnuté konverzace" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutých konverzací" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutá konverzace" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutých konverzací" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -358257,6 +362103,34 @@ } } } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Samtale Festet" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Samtaler Festet" + } + } + } + } + } + } } } }, @@ -358269,11 +362143,29 @@ "value" : "{app_pro} planınız aktivdir!

    Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək. Planınıza edilən güncəlləmələr növbəti {pro} yenilənməsi zamanı qüvvəyə minəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} je aktivní!

    Váš tarif se automaticky obnoví na další {current_plan} dne {date}. Změny tarifu se projeví při příštím obnovení {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when {pro} is next renewed." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is actief!

    Je abonnement wordt automatisch verlengd voor een nieuw {current_plan} op {date}. Wijzigingen aan je abonnement gaan in wanneer {pro} de volgende keer wordt verlengd." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для тебе діє підписка {app_pro}.

    {date} твою підписку буде самодійно поновлено як {current_plan}. Оновлення підписки настане під час наступного оновлення {pro}." + } } } }, @@ -358286,11 +362178,29 @@ "value" : "{app_pro} planınız aktivdir!

    Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} je aktivní!

    Tarif se automaticky obnoví na další {current_plan} dne {date}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is actief!

    Je abonnement wordt automatisch verlengd met een {current_plan} op {date}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для тебе діє підписка {app_pro}.

    {date} твою підписку буде самодійно поновлено як {current_plan}." + } } } }, @@ -358303,11 +362213,29 @@ "value" : "{app_pro} planınızın müddəti {date} tarixində bitir.

    Eksklüziv Pro özəlliklərinə kəsintisiz erişimi təmin etmək üçün planınızı indi güncəlləyin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} vyprší dne {date}.

    Aktualizujte si tarif nyní, abyste měli i nadále nepřerušený přístup k exkluzivním funkcím Pro." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan will expire on {date}.

    Update your plan now to ensure uninterrupted access to exclusive Pro features." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement verloopt op {date}.

    Werk je abonnement nu bij om ononderbroken toegang te behouden tot exclusieve Pro functies." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Твоя підписка {app_pro} спливе {date}.

    Для збереження особливих можливостей подовж свою підписку." + } } } }, @@ -358320,11 +362248,29 @@ "value" : "{app_pro} planınızın müddəti {date} tarixində bitir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} vyprší dne {date}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan will expire on {date}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement verloopt op {date}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підписка {app_pro} спливе {date}." + } } } }, @@ -358337,11 +362283,29 @@ "value" : "{pro} planı tapılmadı" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} nebyl nalezen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Plan Not Found" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement niet gevonden" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Передплата {pro} не знайдена" + } } } }, @@ -358354,11 +362318,23 @@ "value" : "Hesabınız üçün heç bir aktiv plan tapılmadı. Bunun bir səhv olduğunu düşünürsünüzsə, lütfən kömək üçün {app_name} dəstəyi ilə əlaqə saxlayın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro váš účet nebyl nalezen žádný aktivní tarif. Pokud si myslíte, že se jedná o chybu, kontaktujte podporu {app_name} a požádejte o pomoc." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No active plan was found for your account. If you believe this is a mistake, please reach out to {app_name} support for assistance." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er is geen actief abonnement gevonden voor je account. Als je denkt dat dit een vergissing is, neem dan contact op met de ondersteuning van {app_name} voor hulp." + } } } }, @@ -358371,11 +362347,23 @@ "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri ödəmə tələbini göndərmək üçün eyni {platform_account} hesabını istifadə etməlisiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protože jste se původně zaregistrovali do {app_pro} přes obchod {platform_store}, budete muset pro žádost o vrácení peněz použít stejný účet {platform_account}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use the same {platform_account} to request a refund." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, moet je hetzelfde {platform_account} gebruiken om een terugbetaling aan te vragen." + } } } }, @@ -358388,11 +362376,23 @@ "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

    Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəmə tələbinizi göndərin.

    {app_name} Dəstək komandası, geri ödəmə tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protože jste si původně zakoupili {app_pro} přes obchod {platform_store}, váš požadavek na vrácení peněz bude zpracován podporou {app_name}.

    Požádejte o vrácení peněz kliknutím na tlačítko níže a vyplněním formuláře žádosti o vrácení peněz.

    Ačkoliv se podpora {app_name} snaží zpracovat žádosti o vrácení peněz během 24–72 hodin, může zpracování trvat i déle, pokud dochází k vysokému počtu žádostí." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, your refund request will be processed by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, wordt je restitutieverzoek afgehandeld door {app_name} Support.

    Vraag een restitutie aan door op de knop hieronder te drukken en het restitutieformulier in te vullen.

    Hoewel {app_name} Support ernaar streeft om restitutieverzoeken binnen 24-72 uur te verwerken, kan het tijdens drukte langer duren" + } } } }, @@ -358405,11 +362405,29 @@ "value" : "{pro} planını geri qaytar" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Znovu nabýt tarif {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Recover {pro} Plan" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement herstellen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відновити передплату {pro}" + } } } }, @@ -358422,11 +362440,29 @@ "value" : "{pro} planını yenilə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovit tarif {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Renew {pro} Plan" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement verlengen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити підписку {pro}" + } } } }, @@ -358439,11 +362475,23 @@ "value" : "Hazırda, {pro} planları, yalnızca {platform_store} və {platform_store} Mağazaları vasitəsilə satın alına və yenilənə bilər. {app_name} Masaüstü istifadə etdiyinizə görə planınızı burada yeniləyə bilməzsiniz.

    {app_pro} gəlişdiriciləri, istifadəçilərin {pro} planlarını {platform_store} və {platform_store} Mağazalarından kənarda almağına imkan verəcək alternativ ödəniş variantları üzərində ciddi şəkildə çalışırlar. {pro} Yol Xəritəsi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V současnosti lze tarify {pro} zakoupit a obnovit pouze prostřednictvím obchodů {platform_store} nebo {platform_store}. Protože používáte {app_name} Desktop, nemůžete zde svůj plán obnovit.

    Vývojáři {app_pro} intenzivně pracují na alternativních platebních možnostech, které by uživatelům umožnily zakoupit tarify {pro} mimo obchody {platform_store} a {platform_store}. Plán vývoje {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store} Stores. Because you are using {app_name} Desktop, you're not able to renew your plan here.

    {app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store} Stores. {pro} Roadmap" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Momenteel kunnen {pro} abonnementen alleen worden gekocht en verlengd via de {platform_store}- of {platform_store} winkels. Omdat je {app_name} Desktop gebruikt, kun je je abonnement hier niet verlengen.

    De ontwikkelaars van {app_pro} werken hard aan alternatieve betaalmogelijkheden, zodat gebruikers {pro} abonnementen buiten de {platform_store}- en {platform_store} winkels kunnen aanschaffen. {pro} Routekaart" + } } } }, @@ -358456,11 +362504,23 @@ "value" : "{platform_store} və ya {platform_store} Mağazaları vasitəsilə planınızı {app_name} quraşdırılmış və əlaqələndirilmiş cihazda {app_pro} ayarlarında yeniləyin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovte svůj tarif v nastavení {app_pro} na propojeném zařízení s nainstalovanou aplikací {app_name} prostřednictvím obchodu {platform_store} nebo {platform_store}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Renew your plan in the {app_pro} settings on a linked device with {app_name} installed via the {platform_store} or {platform_store} Store." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verleng je abonnement in de {app_pro} instellingen op een gekoppeld apparaat met {app_name} geïnstalleerd via de {platform_store} of {platform_store} winkel." + } } } }, @@ -358473,27 +362533,33 @@ "value" : "{pro} üçün qeydiyyatdan keçdiyiniz {platform_account} hesabınızla {platform_store} veb saytında planınızı yeniləyin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovte svůj tarif na webu {platform_store} pomocí účtu {platform_account}, se kterým jste si pořídili {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Renew your plan on the {platform_store} website using the {platform_account} you signed up for {pro} with." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verleng je abonnement op de {platform_store} website met het {platform_account} waarmee je je voor {pro} hebt aangemeld." + } } } }, "proPlanRenewStart" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Güclü {app_pro} özəlliklərini yenidən istifadə etməyə başlamaq üçün {app_pro} planınızı yeniləyin." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Renew your {app_pro} plan to start using powerful {app_pro} features again." + "value" : "Renew your {app_pro} plan to start using powerful {app_pro} Beta features again." } } } @@ -358507,11 +362573,23 @@ "value" : "{app_pro} planınız yeniləndi! {network_name} dəstək verdiyiniz üçün təşəkkürlər." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} byl obnoven! Děkujeme, že podporujete síť {network_name}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {app_pro} plan has been renewed! Thank you for supporting the {network_name}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is verlengd! Bedankt voor je steun aan de {network_name}." + } } } }, @@ -358524,11 +362602,29 @@ "value" : "{pro} planı bərpa edildi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} obnoven" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Plan Restored" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement hersteld" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "План {pro} відновлено" + } } } }, @@ -358541,11 +362637,23 @@ "value" : "{app_pro} üçün yararlı bir plan aşkarlandı və {pro} statusunuz bərpa edildi!" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byl rozpoznán platný tarif {app_pro} a váš stav {pro} byl obnoven!" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "A valid plan for {app_pro} was detected and your {pro} status has been restored!" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Een geldig abonnement voor {app_pro} is gedetecteerd en je {pro} status is hersteld!" + } } } }, @@ -358558,11 +362666,23 @@ "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protože jste se původně zaregistrovali do {app_pro} přes {platform_store}, je třeba abyste pro aktualizaci vašeho tarifu použili svůj {platform_account}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use your {platform_account} to update your plan." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, moet je je {platform_account} gebruiken om je abonnement bij te werken." + } } } }, @@ -358575,11 +362695,29 @@ "value" : "1 ay - {monthly_price}/ay" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 měsíc – {monthly_price} / měsíc" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "1 Month - {monthly_price} / Month" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 maand - {monthly_price} / maand" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 місяць — {monthly_price} / місяць" + } } } }, @@ -358592,11 +362730,29 @@ "value" : "3 ay - {monthly_price}/ay" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 měsíce – {monthly_price} / měsíc" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "3 Months - {monthly_price} / Month" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 maanden - {monthly_price} / maand" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 місяці — {monthly_price} / місяць" + } } } }, @@ -358609,11 +362765,29 @@ "value" : "12 ay - {monthly_price}/ay" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 měsíců – {monthly_price} / měsíc" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "12 Months - {monthly_price} / Month" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 maanden - {monthly_price} / maand" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 місяців – {monthly_price} / місяць" + } } } }, @@ -358626,11 +362800,29 @@ "value" : "Getməyinizə məyus olduq. Geri ödəmə tələb etməzdən əvvəl bilməli olduğunuz şeylər." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mrzí nás, že to rušíte. Než požádáte o vrácení peněz, přečtěte si informace, které byste měli vědět." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "We’re sorry to see you go. Here's what you need to know before requesting a refund." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het spijt ons dat je vertrekt. Dit moet je weten voordat je een terugbetaling aanvraagt." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шкода, же ти передумав(ла). Перед вимогою повернення грошей ти мусиш знати ось що." + } } } }, @@ -358643,11 +362835,29 @@ "value" : "{pro} geri ödəməsi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vracení peněz za {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Refunding {pro}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetalen {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повернення грошей за {pro}" + } } } }, @@ -358660,11 +362870,23 @@ "value" : "{app_pro} planları üçün geri ödəmələr yalnız {platform_store} Mağazası vasitəsilə {platform_account} tərəfindən həyata keçirilir.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrácení peněz za tarify {app_pro} je vyřizováno výhradně prostřednictvím {platform_account} v obchodě {platform_store}.

    Vzhledem k pravidlům vracení peněz služby {platform_account} nemají vývojáři {app_name} žádný vliv na výsledek žádostí o vrácení peněz. To zahrnuje i rozhodnutí, zda bude žádost schválena nebo zamítnuta, a také, zda bude vrácena část peněz, nebo všechny peníze." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Refunds for {app_pro} plans are handled exclusively by {platform_account} through the {platform_store} Store.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetalingen voor {app_pro} abonnementen worden uitsluitend afgehandeld door {platform_account} via de {platform_store} Store.

    Vanwege het restitutiebeleid van {platform_account} hebben ontwikkelaars van {app_name} geen invloed op de uitkomst van terugbetalingsverzoeken. Dit geldt zowel voor de goedkeuring of afwijzing van het verzoek als voor het wel of niet toekennen van een volledige of gedeeltelijke terugbetaling." + } } } }, @@ -358677,11 +362899,23 @@ "value" : "{platform_account} hazırda geri ödəniş tələbinizi emal edir. Bu, adətən 24-48 saat çəkir. Onların qərarından asılı olaraq, {app_name} tətbiqində {pro} statusunuzun dəyişdiyini görə bilərsiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} nyní zpracovává vaši žádost o vrácení peněz. Obvykle to trvá 24–48 hodin. V závislosti na jejich rozhodnutí se může váš stav {pro} v aplikaci {app_name} změnit." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{platform_account} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your {pro} status change in {app_name}." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform_account} verwerkt nu je terugbetalingsverzoek. Dit duurt meestal 24-48 uur. Afhankelijk van hun beslissing kan je {pro} status wijzigen in {app_name}." + } } } }, @@ -358694,11 +362928,23 @@ "value" : "Geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

    Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəniş tələbinizi göndərin.

    {app_name} Dəstək komandası, geri ödəniş tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš požadavek na vrácení peněz bude zpracován podporou {app_name}.

    Požádejte o vrácení peněz kliknutím na tlačítko níže a vyplněním formuláře žádosti o vrácení peněz.

    Ačkoliv se podpora {app_name} snaží zpracovat žádosti o vrácení peněz během 24–72 hodin, může zpracování trvat i déle, v případě že je vyřizováno mnoho žádostí." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your refund request will be handled by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je restitutieverzoek wordt afgehandeld door {app_name} Support.

    Vraag een restitutie aan door op de knop hieronder te drukken en het restitutieformulier in te vullen.

    Hoewel {app_name} Support ernaar streeft om restitutieverzoeken binnen 24-72 uur te verwerken, kan het tijdens drukte langer duren." + } } } }, @@ -358711,11 +362957,23 @@ "value" : "Geri ödəniş tələbiniz yalnız {platform_account} veb saytında {platform_account} hesabı üzərindən icra olunacaq.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaši žádost o vrácení peněz bude vyřizovat výhradně {platform_account} prostřednictvím webových stránek {platform_account}.

    Vzhledem k pravidlům vracení peněz {platform_account} nemají vývojáři {app_name} žádný vliv na výsledek žádostí o vrácení peněz. To zahrnuje i rozhodnutí, zda bude žádost schválena nebo zamítnuta, a také, zda bude vrácena část peněz, nebo všechny peníze." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your refund request will be handled exclusively by {platform_account} through the {platform_account} website.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je terugbetalingsverzoek wordt uitsluitend afgehandeld door {platform_account} via de website van {platform_account}.

    Vanwege het restitutiebeleid van {platform_account} hebben ontwikkelaars van {app_name} geen invloed op de uitkomst van terugbetalingsverzoeken. Dit geldt zowel voor de goedkeuring of afwijzing van het verzoek als voor het wel of niet toekennen van een volledige of gedeeltelijke terugbetaling." + } } } }, @@ -358728,11 +362986,23 @@ "value" : "Geri ödəmə tələbinizlə bağlı daha çox güncəlləmə üçün lütfən {platform_account} ilə əlaqə saxlayın. {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz.

    {platform_store} Geri ödəmə dəstəyi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro další informace o vaší žádosti o vrácení peněz kontaktujte prosím {platform_account}. Vzhledem k zásadám pro vrácení peněz {platform_account} nemají vývojáři aplikace {app_name} žádnou možnost ovlivnit výsledek žádosti o vrácení.

    Podpora vrácení peněz {platform_store}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Please contact {platform_account} for further updates on your refund request. Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests.

    {platform_store} Refund Support" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neem contact op met {platform_account} voor verdere updates over je restitutieverzoek. Vanwege het restitutiebeleid van {platform_account} hebben de ontwikkelaars van {app_name} geen invloed op de uitkomst van restitutieverzoeken.

    {platform_store} Terugbetalingsondersteuning" + } } } }, @@ -358745,11 +363015,29 @@ "value" : "Geri ödəmə tələb edildi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádost o vrácení peněz" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Refund Requested" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetaling aangevraagd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимогу повернення грошей надіслано" + } } } }, @@ -358899,11 +363187,29 @@ "value" : "{pro} ayarları" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavení {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Settings" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} instellingen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування {pro}" + } } } }, @@ -358916,11 +363222,29 @@ "value" : "{pro} statistikalarınız" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše statistiky {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {pro} Stats" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {pro} statistieken" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша статистика {pro}" + } } } }, @@ -358933,11 +363257,29 @@ "value" : "{pro} statistikaları, bu cihazdakı istifadəni əks-etdirir və əlaqələndirilmiş cihazlarda fərqli görünə bilər." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statistiky {pro} ukazují používání na tomto zařízení a mohou se lišit na jiných propojených zařízeních" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} stats reflect usage on this device and may appear differently on linked devices" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistieken weerspiegelen het gebruik op dit apparaat en kunnen anders weergegeven worden op gekoppelde apparaten" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звіти підписки {pro} відображають використання лише цього пристрою, тож, мабуть, матимуть иншого вигляду на инших пристроях" + } } } }, @@ -358950,11 +363292,29 @@ "value" : "{pro} planınızla bağlı kömək lazımdır? Dəstək komandamıza müraciət edin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potřebujete pomoc se svým tarifem {pro}? Pošlete žádost týmu podpory." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Need help with your {pro} plan? Submit a request to the support team." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hulp nodig met je {pro} abonnement? Dien een verzoek in bij het ondersteuningsteam." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Якщо потребуєш допомоги щодо підписки {pro}, надійшли звернення до відділу підтримки." + } } } }, @@ -358967,11 +363327,29 @@ "value" : "Güncəlləyərək, {app_pro} Xidmət Şərtləri {icon} və Məxfilik Siyasəti {icon} ilə razılaşırsınız" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizací souhlasíte s Podmínkami služby {icon} a Zásadami ochrany osobních údajů {icon} {app_pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "By updating, you agree to the {app_pro} Terms of Service {icon} and Privacy Policy {icon}" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Door bij te werken ga je akkoord met de {app_pro} Gebruiksvoorwaarden {icon} en het Privacybeleid {icon}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цією дією ти надаси згоду щодо дотримання Правил послуги {app_pro} {icon} і Ставлення до особистих відомостей {icon}" + } } } }, @@ -358984,11 +363362,29 @@ "value" : "Limitsiz sancma" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neomezený počet připnutí" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unlimited Pins" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbeperkte Pins" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Необмежена кількість закріплених бесід" + } } } }, @@ -359001,11 +363397,29 @@ "value" : "Limitsiz sancılmış danışıqla bütün söhbətlərinizi təşkil edin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organizujte si komunikaci pomocí neomezeného počtu připnutých konverzací." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Organize all your chats with unlimited pinned conversations." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organiseer al je chats met onbeperkt vastgezette gesprekken." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закріплення необмеженої кількості співрозмовників в головному переліку." + } } } }, @@ -359018,11 +363432,23 @@ "value" : "Hazırda {current_plan} Planı üzərindəsiniz. {selected_plan} Planınana keçmək istədiyinizə əminsiniz?

    Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} {pro} erişimi üçün avtomatik yenilənəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V současné době jste na tarifu {current_plan}. Jste si jisti, že chcete přepnout na tarif {selected_plan}?

    Po aktualizaci bude váš tarif automaticky obnoven {date} na další {selected_plan} přístupu {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of {pro} access." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je zit momenteel op het {current_plan} abonnement. Weet je zeker dat je wilt overschakelen naar het {selected_plan} abonnement?

    Als je dit bijwerkt, wordt je abonnement op {date} automatisch verlengd met {selected_plan} {pro} toegang." + } } } }, @@ -359035,11 +363461,23 @@ "value" : "Planınız {date} tarixində bitəcək.

    Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} Pro erişimi üçün avtomatik olaraq yenilənəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif vyprší {date}.

    Po aktualizaci se váš tarif automaticky obnoví {date} na další {selected_plan} přístupu Pro." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your plan will expire on {date}.

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je abonnement verloopt op {date}.

    Door bij te werken wordt je abonnement automatisch verlengd op {date} voor een extra {selected_plan} aan Pro-toegang." + } } } }, @@ -359136,12 +363574,24 @@ "value" : "Vrei să profiți mai mult de {app_name}? Fă upgrade la {app_pro} pentru o experiență de mesagerie mai puternică." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хотите больше возможностей от {app_name}? Перейдите на {app_pro}, чтобы получить более мощный опыт обмена сообщениями." + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Vill du få ut mer av {app_name}? Uppgradera till {app_pro} för en kraftfullare meddelandeupplevelse." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}'den daha fazla yararlanmak ister misiniz? Daha güçlü bir mesajlaşma deneyimi için {app_pro}'ya yükseltin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -365390,6 +369840,18 @@ "value" : "응답 수신됨" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "وەڵامی وەرگیراو" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "وەڵامی وەرگیراو" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -365575,6 +370037,18 @@ "value" : "통화 제안 받는 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پێشنیاری پەیوەندی بنێرە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "پێشنیاری پەیوەندی بنێرە" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -367244,11 +371718,53 @@ "value" : "Hesabınızı yeni cihazlara yükləmək üçün geri qaytarma parolunuzu istifadə edin.

    Geri qaytarma parolunuz olmadan hesabınız geri qaytarıla bilməz. Parolu təhlükəsiz və etibarlı yerdə saxladığınıza əmin olun və heç kəslə paylaşmayın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Použijte své heslo pro obnovení pro načtení účtu na nových zařízeních.

    Bez hesla pro obnovení nelze obnovit účet. Ujistěte se, že je uložené na bezpečném místě — a nesdílejte ho s nikým." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwende dein Wiederherstellungspasswort, um deinen Account auf neue Geräten zu laden.

    Dein Account kann ohne dein Wiederherstellungspasswort nicht wiederhergestellt werden. Stelle sicher, dass es an einem sicheren Ort aufbewahrt ist – und teile es niemandem mit." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Use your recovery password to load your account on new devices.

    Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gebruik uw herstelwachtwoord om uw account op nieuwe apparaten te laden.

    Uw account kan niet worden hersteld zonder uw herstelwachtwoord. Zorg ervoor dat het ergens veilig is opgeslagen – en deel het met niemand." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Используйте пароль восстановления, чтобы загрузить свою учетную запись на новых устройствах.

    Ваша учетная запись не может быть восстановлена без пароля восстановления. Убедитесь, что он хранится в безопасном месте — и не делитесь им ни с кем." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Använd ditt återställningslösenord för att ladda ditt konto på nya enheter.

    Ditt konto kan inte återställas utan ditt återställningslösenord. Se till att det lagras på en säker och trygg plats — och dela det inte med någon." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurtarma şifrenizi kullanarak hesabınızı yeni cihazlara yükleyin.

    Kurtarma şifreniz olmadan hesabınız kurtarılamaz. Şifrenizi güvenli bir yerde sakladığınızdan emin olun ve kimseyle paylaşmayın." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Використовуйте пароль для відновлення для завантаження свого облікового запису на нових пристроях.

    Ваш обліковий запис не може бути відновлений без пароля для відновлення. Переконайтеся, що він зберігається у надійному місці та не передавайте його нікому." + } } } }, @@ -371266,11 +375782,65 @@ "value" : "Geri qaytarma parolunuzu bu cihazdan həmişəlik gizlətmək istədiyinizə əminsiniz?

    Bunun geri dönüşü yoxdur." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete trvale skrýt heslo pro obnovení na tomto zařízení?

    Tuto akci nelze vrátit." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du dein Wiederherstellungspasswort auf diesem Gerät dauerhaft ausblenden möchtest?

    Dies kann nicht mehr rückgängig gemacht werden." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to permanently hide your recovery password on this device?

    This cannot be undone." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Êtes-vous sûr de vouloir cacher définitivement votre mot de passe de récupération sur cet appareil ?

    Cette action est irréversible." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på at du har lyst til å permanent skjule gjenopprettingspassordet ditt?

    Dette kan ikke angres." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet u zeker dat u uw herstelwachtwoord permanent wilt verbergen op dit apparaat?

    Dit kan niet ongedaan gemaakt worden." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите навсегда скрыть ваш пароль восстановления на этом устройстве?

    Это действие не может быть отменено." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill dölja ditt återställningslösenord permanent på denna enhet?

    Detta kan inte ångras." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazda kurtarma şifrenizi kalıcı olarak gizlemek istediğinizden emin misiniz?

    Bu işlem geri alınamaz." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви впевнені, що хочете назавжди приховати пароль для відновлення на цьому пристрої?

    Цю дію неможливо скасувати." + } } } }, @@ -372720,11 +377290,53 @@ "value" : "Geri qaytarma paroluna bax" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit heslo pro obnovení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiederherstellungspasswort anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "View Recovery Password" } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis Gjenopprettingspassord" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekijk Herstelwachtwoord" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать пароль восстановления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa återställningslösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перегляд паролю для відновлення" + } } } }, @@ -372737,11 +377349,53 @@ "value" : "Geri qaytarma parolu görünməsi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Viditelnost hesla pro obnovení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sichtbarkeit des Wiederherstellungspassworts" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Recovery Password Visibility" } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gjenopprettingspassord Synlighet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zichtbaarheid herstelwachtwoord" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видимость пароля восстановления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synlighet för återställningslösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видимість пароля для відновлення" + } } } }, @@ -373891,11 +378545,23 @@ "value" : "Başda fərqli {platform_account} vasitəsilə {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protože jste se původně zaregistrovali do {app_pro} přes jiný {platform_account}, je třeba použít ten {platform_account}, abyste aktualizovali váš tarif." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Because you originally signed up for {app_pro} via a different {platform_account}, you'll need to use that {platform_account} to update your plan." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via een ander {platform_account}, moet je datzelfde {platform_account} gebruiken om je abonnement bij te werken." + } } } }, @@ -374267,6 +378933,24 @@ "value" : "{count}자 입력 가능" } }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tegn igjen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tegn igjen" + } + } + } + } + }, "nl" : { "variations" : { "plural" : { @@ -375446,11 +380130,47 @@ "value" : "{app_name} üçün hazırkı parolunuzu silin. Daxili olaraq saxlanılmış verilər, cihazınızda saxlanılan təsadüfi yaradılmış açarla təkrar şifrələnəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odstraňte své aktuální heslo pro {app_name}. Lokálně uložená data budou znovu zašifrována pomocí náhodně vygenerovaného klíče uloženého ve vašem zařízení." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entferne dein aktuelles Passwort für {app_name}. Lokal gespeicherte Daten werden mit einem zufällig generierten Schlüssel, der auf deinem Gerät gespeichert wird, erneut verschlüsselt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder je huidige wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met een willekeurig gegenereerde sleutel, opgeslagen op je apparaat." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалите текущий пароль для {app_name}. Локально сохранённые данные будут повторно зашифрованы с использованием случайно сгенерированного ключа, хранящегося на вашем устройстве." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta bort ditt nuvarande lösenord för {app_name}. Lokalt lagrad data kommer att krypteras om med en slumpmässigt genererad nyckel som lagras på din enhet." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видаліть свій поточний пароль для {app_name}. Локально збережені дані буде повторно зашифровано випадково згенерованим ключем, який зберігатиметься на вашому пристрої." + } } } }, @@ -375463,11 +380183,29 @@ "value" : "Yenilə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovit" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Renew" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlengen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновити" + } } } }, @@ -375959,11 +380697,29 @@ "value" : "Geri ödəmə tələb et" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Požádat o vrácení platby" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Request Refund" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetaling aanvragen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запит на повернення коштів" + } } } }, @@ -390673,6 +395429,18 @@ "value" : "연결 후보 전송 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی کاندیدەکانی پەیوەندی" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی کاندیدەکانی پەیوەندی" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -397030,11 +401798,29 @@ "value" : "{app_pro} Beta" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Beta" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Bèta" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} бета" + } } } }, @@ -398645,11 +403431,41 @@ "value" : "{app_name} üçün bir parol təyin edin. Daxili olaraq saxlanılmış verilər, bu parolla şifrələnəcək. {app_name} tətbiqini hər başlatdıqda, sizdən bu parolu daxil etməyiniz istənəcək." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavte heslo pro {app_name}. Lokálně uložená data budou šifrována tímto heslem. Při každém spuštění {app_name} budete vyzváni k zadání tohoto hesla." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel een wachtwoord in voor {app_name}. Lokaal opgeslagen gegevens worden versleuteld met dit wachtwoord. Je wordt gevraagd dit wachtwoord in te voeren telkens wanneer {app_name} wordt gestart." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Установите пароль для {app_name}. Локально сохранённые данные будут зашифрованы с использованием этого пароля. При каждом запуске {app_name} вам потребуется вводить этот пароль." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ställ in ett lösenord för {app_name}. Lokalt lagrade data kommer att krypteras med detta lösenord. Du kommer att bli ombedd att ange detta lösenord varje gång {app_name} startas." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Встановіть пароль для {app_name}. Дані, збережені локально, буде зашифровано цим паролем. Цей пароль запитуватиметься при кожному запуску {app_name}." + } } } }, @@ -398662,11 +403478,23 @@ "value" : "Ayar güncəllənə bilmir" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze aktualizovat volbu" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Cannot Update Setting" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неможливо оновити налаштування" + } } } }, @@ -399637,11 +404465,23 @@ "value" : "Açılış" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spuštění" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Startup" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автозапуск" + } } } }, @@ -403951,11 +408791,47 @@ "value" : "Yazı yoxlanışı" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrola pravopisu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechtschreibprüfung" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Spell Checker" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spellingcontrole" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверка орфографии" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stavningskontroll" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевірка орфографії" + } } } }, @@ -404447,11 +409323,53 @@ "value" : "Gücü" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Síla" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwortstärke" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Strength" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sterkte" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puternic" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надёжность" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Styrka" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надійність" + } } } }, @@ -404464,11 +409382,47 @@ "value" : "Problemlə üzləşmisiniz? Kömək məqalələrini oxuyun, ya da {app_name} Dəstək ilə bir sorğu açın." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máte problémy? Projděte si články nápovědy nebo kontaktujte podporu {app_name}." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probleme? Erkunde die Hilfeartikel oder öffne ein Ticket bei dem {app_name} Support." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Having issues? Explore help articles or open a ticket with {app_name} Support." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Problemen? Bekijk de hulpartikelen of open een ticket bij {app_name} Support." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возникли проблемы? Ознакомьтесь со статьями в справке или отправьте запрос в службу поддержки {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Har du problem? Utforska hjälpartiklar eller öppna en supportförfrågan hos {app_name}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виникли проблеми? Перегляньте довідкові статті або створіть запит до служби підтримки {app_name}." + } } } }, @@ -405478,7 +410432,7 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Toca para reintentar" + "value" : "Toque para reintentar" } }, "es-ES" : { @@ -407061,11 +412015,47 @@ "value" : "Tema önizləməsi" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Náhled motivu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Design-Vorschau" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Theme Preview" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thema voorbeeld" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпросмотр темы" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förhandsvisning av tema" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попередній перегляд теми" + } } } }, @@ -407078,11 +412068,29 @@ "value" : "Qayıt" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpět" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Return" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terug" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зворотно" + } } } }, @@ -407357,11 +412365,47 @@ "value" : "Tərcümə et" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Překlad" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Übersetzen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Translate" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vertalen" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевод" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Översätt" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переклад" + } } } }, @@ -407374,11 +412418,41 @@ "value" : "Sini" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lišta" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Tray" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Systeemvak" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трей" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivitetsfält" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Область сповіщень" + } } } }, @@ -410504,12 +415578,24 @@ "value" : "Actualizează informațiile comunității" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить информацию о сообществе" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Uppdatera communityinformation" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити інформацію про спільноту" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -410617,12 +415703,24 @@ "value" : "Numele și descrierea comunității sunt vizibile pentru toți membrii comunității" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название и описание Community видны всем участникам Community" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Communitynamn och beskrivning är synliga för alla communitymedlemmar" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назву та опис спільноти бачать усі учасники" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -410730,12 +415828,24 @@ "value" : "Te rugăm să introduci o descriere a comunității mai scurtă" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите более короткое описание сообщества" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Vänligen ange en kortare communitybeskrivning" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть коротший опис спільноти" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -410843,12 +415953,24 @@ "value" : "Te rugăm să introduci un nume al comunității mai scurt" } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, введите более короткое название сообщества" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Vänligen ange ett kortare communitynamn" } }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть коротшу назву спільноти" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", @@ -414025,6 +419147,12 @@ "value" : "Versîyoneke nû ({version}) ya {app_name} berdest e." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En ny versjon ({version}) av {app_name} er tilgjengelig." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -414102,11 +419230,29 @@ "value" : "Planı güncəllə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizovat tarif" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Update Plan" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement bijwerken" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити тарифний план" + } } } }, @@ -414119,11 +419265,29 @@ "value" : "Planınızı güncəlləməyin iki yolu var:" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dva způsoby, jak aktualizovat váš tarif:" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Two ways to update your plan:" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twee manieren om je abonnement bij te werken:" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Два шляхи поновлення твоєї підписки:" + } } } }, @@ -414136,11 +419300,65 @@ "value" : "Profil məlumatlarını güncəllə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravit informace profilu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilinformationen aktualisieren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Update Profile Information" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información de perfil" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información de perfil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profielinformatie bijwerken" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizarea informațiilor de profil" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить информацию профиля" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera profilinformation" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити інформацію облікового запису" + } } } }, @@ -414153,11 +419371,53 @@ "value" : "Ekran adınız və ekran şəkliniz bütün danışıqlarda görünür." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše zobrazované jméno a profilová fotka jsou viditelné ve všech konverzacích." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Anzeigename und Profilbild sind in allen Unterhaltungen sichtbar." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your display name and display picture are visible in all conversations." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je weergavenaam en profielfoto zijn zichtbaar in alle gesprekken." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și poza de profil sunt vizibile în toate conversațiile tale." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваше отображаемое имя и фотография профиля видны во всех беседах." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt visningsnamn och din visningsbild är synliga i alla konversationer." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваше відображуване ім’я та зображення профілю видимі у всіх розмовах." + } } } }, @@ -414649,11 +419909,47 @@ "value" : "Güncəlləmələr" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizace" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualisierungen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Updates" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updates" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdateringar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновлення" + } } } }, @@ -415630,11 +420926,47 @@ "value" : "Güncəllənir..." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizuji..." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wird aktualisiert..." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Updating..." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijwerken..." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizare..." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdaterar..." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновлення..." + } } } }, @@ -418179,11 +423511,29 @@ "value" : "Keçidlər, brauzerinizdə açılacaq." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odkazy se otevřou ve vašem prohlížeči." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Links will open in your browser." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links worden in uw browser geopend." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За ланкою перейде твоє оглядало мережців за промовчання." + } } } }, @@ -418675,11 +424025,23 @@ "value" : "{platform_store} veb saytı vasitəsilə" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přes webové stránky {platform_store}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Via the {platform_store} website" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via de {platform_store} website" + } } } }, @@ -418692,11 +424054,23 @@ "value" : "Qeydiyyatdan keçərkən istifadə etdiyiniz {platform_account} hesabı ilə {platform_store} veb saytı üzərindən planınızı dəyişdirin." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změňte svůj tarif pomocí {platform_account}, se kterým jste se zaregistrovali, prostřednictvím webu {platform_store}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig je abonnement met het {platform_account} waarmee je je hebt aangemeld, via de {platform_store} website." + } } } }, @@ -423342,11 +428716,53 @@ "value" : "Geri qaytarma parolunuz" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše heslo pro obnovení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Wiederherstellungspasswort" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your Recovery Password" } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt Gjenopprettingspassord" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je herstelwachtwoord" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш Пароль Восстановления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt återställningslösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль для відновлення" + } } } }, @@ -423359,11 +428775,47 @@ "value" : "Böyütmə amili" } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Měřítko přiblížení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vergrößerungsfaktor" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Zoom Factor" } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoomfactor" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Масштабирование приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoomfaktor" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Масштаб" + } } } }, @@ -423376,11 +428828,47 @@ "value" : "Mətnin və vizual elementlərin ölçüsünü ayarla." } }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravte velikost textu a vizuálních prvků." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passe die Größe von Text und visuellen Elementen an." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Adjust the size of text and visual elements." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de grootte van tekst en visuele elementen aan." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настройте размер текста и визуальных элементов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Justera storleken på text och visuella element." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування розміру тексту та візуальних елементів." + } } } } From ea8dac909c04295d83fc6efe5bcc7aa41b4cb988 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 10:56:03 +1000 Subject: [PATCH 200/244] Fixed a bug where profile data wouldn't be updated for new contacts --- SessionMessagingKit/Utilities/Profile+Updating.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKit/Utilities/Profile+Updating.swift b/SessionMessagingKit/Utilities/Profile+Updating.swift index c4b4b9772d..dc5ceb9f60 100644 --- a/SessionMessagingKit/Utilities/Profile+Updating.swift +++ b/SessionMessagingKit/Utilities/Profile+Updating.swift @@ -141,7 +141,10 @@ public extension Profile { let profile: Profile = Profile.fetchOrCreate(db, id: publicKey) var profileChanges: [ConfigColumnAssignment] = [] - guard profileUpdateTimestamp > profile.profileLastUpdated.defaulting(to: 0) else { return } + guard + profile.profileLastUpdated == nil || + profileUpdateTimestamp > profile.profileLastUpdated.defaulting(to: 0) + else { return } // Name switch (displayNameUpdate, isCurrentUser) { @@ -214,7 +217,7 @@ public extension Profile { default: break } - // Persist any changes + /// Persist any changes if !profileChanges.isEmpty { profileChanges.append(Profile.Columns.profileLastUpdated.set(to: profileUpdateTimestamp)) @@ -228,7 +231,8 @@ public extension Profile { using: dependencies ) - + /// We don't automatically update the current users profile data when changed in the database so need to manually + /// trigger the update if isCurrentUser, let updatedProfile = try? Profile.fetchOne(db, id: publicKey) { try dependencies.mutate(cache: .libSession) { cache in try cache.performAndPushChange(db, for: .userProfile, sessionId: dependencies[cache: .general].sessionId) { _ in From d2ba543253b275d7c8525134415ae4aabb2f15f9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 10:58:02 +1000 Subject: [PATCH 201/244] Fixed the broken unit tests --- .../LibSession/LibSessionUtilSpec.swift | 20 +++++++++---- .../_TestUtilities/MockLibSessionCache.swift | 4 +-- SessionTests/Onboarding/OnboardingSpec.swift | 28 ++++++++++++++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift index 56ab349c08..84c3252952 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift @@ -882,12 +882,20 @@ fileprivate extension LibSessionUtilSpec { expect(pushData5.pointee.seqno).to(equal(3)) expect(pushData6.pointee.seqno).to(equal(3)) - // They should have resolved the conflict to the same thing: - expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) - expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) - // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized - // message just happens to have a higher hash -- and thus gets priority -- for this particular - // test). + // They should have resolved the conflict to the same thing - since the configs set + // a timestamp to the current time when modifying the profile (and we don't have a + // mechanism via the C API to set it directly, we can do this directly in the C++ but + // not here) we don't actually know whether "Nibbler" or "Raz" will win here so instead + // the best we can do is insure they match each other, and that they match one of the options + let confNamePtr: UnsafePointer? = user_profile_get_name(conf) + let conf2NamePtr: UnsafePointer? = user_profile_get_name(conf2) + try require(confNamePtr).toNot(beNil()) + try require(conf2NamePtr).toNot(beNil()) + let confName: String = String(cString: confNamePtr!) + let conf2Name: String = String(cString: conf2NamePtr!) + expect(Set(["Nibbler", "Raz"])).to(contain(confName)) + expect(Set(["Nibbler", "Raz"])).to(contain(conf2Name)) + expect(confName).to(equal(conf2Name)) // Since only one of them set a profile pic there should be no conflict there: let pic3: user_profile_pic = user_profile_get_pic(conf) diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index d6d04cce23..24f5fa056a 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -184,8 +184,8 @@ class MockLibSessionCache: Mock, LibSessionCacheType { mockNoReturn(generics: [T.self], args: [key, value]) } - func updateProfile(displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?) throws { - try mockThrowingNoReturn(args: [displayName, displayPictureUrl, displayPictureEncryptionKey]) + func updateProfile(displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?, isReuploadProfilePicture: Bool) throws { + try mockThrowingNoReturn(args: [displayName, displayPictureUrl, displayPictureEncryptionKey, isReuploadProfilePicture]) } func canPerformChange( diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 377b4bc6ff..d8409d60fc 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -666,16 +666,36 @@ class OnboardingSpec: AsyncSpec { let result: [ConfigDump]? = mockStorage.read { db in try ConfigDump.fetchAll(db) } - let expectedData: Data? = Data(base64Encoded: "ZDE6IWkxZTE6JDEwNDpkMTojaTFlMTomZDE6K2ktMWUxOm4xNjpUZXN0Q29tcGxldGVOYW1lZTE6PGxsaTBlMzI66hc7V77KivGMNRmnu/acPnoF0cBJ+pVYNB2Ou0iwyWVkZWVlMTo9ZDE6KzA6MTpuMDplZTE6KGxlMTopbGUxOipkZTE6K2RlZQ==") + try require(result).to(haveCount(1)) - expect(result).to(equal([ + /// Since the `UserProfile` data is not deterministic then the best we can do is compare the `ConfigDump` + /// without it's data to ensure everything else is correct, then check that the dump data contains expected values + let resultData: Data = result![0].data + let resultWithoutData: ConfigDump = ConfigDump( + variant: result![0].variant, + sessionId: result![0].sessionId.hexString, + data: Data(), + timestampMs: result![0].timestampMs + ) + var resultDataString: String = "" + + for i in (0.. Date: Mon, 15 Sep 2025 09:04:54 +0800 Subject: [PATCH 202/244] Clean up update icon handlers --- Session/Settings/AppIconViewModel.swift | 29 +++++++------------------ 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 5a8386fab5..5ebefe1e55 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -137,12 +137,6 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl lazy var observation: TargetObservation = ObservationBuilderOld .subject(selectedOptionsSubject) .mapWithPrevious { [weak self, dependencies] previous, current -> [SectionModel] in - - if let currentIcon = current { - // Save latest app icon disguise selected - dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] = currentIcon - } - return [ SectionModel( model: .appIcon, @@ -158,9 +152,11 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl oldValue: (previous != nil) ), onTap: { [weak self] in + let lastSelected: String? = dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] + switch current { case .some: self?.updateAppIcon(nil) - case .none: self?.restorePreviousIcon(previous) // Previous is String?? + case .none: self?.updateAppIcon(lastSelected.map { AppIcon(name: $0) } ?? .weather) } } ) @@ -194,20 +190,11 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl } selectedOptionsSubject.send(icon?.rawValue) - } - - private func restorePreviousIcon(_ identifier: String??) { - var previousIcon: AppIcon? { - if let previousIcon = identifier { - // Set previous app icon - return AppIcon(name: previousIcon) - } else if let previousIcon = dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] { - // Handles app close instance to restore previously selected - return AppIcon(name: previousIcon) - } - return .weather - } - updateAppIcon(previousIcon) + // Only store custom icons + if let currentIconName = icon?.rawValue { + // Save latest app icon disguise selected + dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] = currentIconName + } } } From c0366a8f523b0576d790f06159965aabd7be075e Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 15 Sep 2025 11:06:39 +0800 Subject: [PATCH 203/244] Added tap outside input view to dismiss keyboard --- .../ConversationVC+Interaction.swift | 23 +++++++++++++++---- Session/Conversations/ConversationVC.swift | 12 ++++++++++ .../Conversations/Input View/InputView.swift | 4 ++-- .../Message Cells/CallMessageCell.swift | 2 ++ .../Message Cells/InfoMessageCell.swift | 2 ++ .../Message Cells/MessageCell.swift | 3 +++ .../Message Cells/VisibleMessageCell.swift | 2 ++ 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 1330cba707..1e24c0950d 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -33,6 +33,11 @@ extension ConversationVC: openSettingsFromTitleView() } + // Handle taps outside of tableview cell to dismiss keyboard + @MainActor @objc func handleTableViewTap() { + _ = self.snInputView.resignFirstResponder() + } + @MainActor func openSettingsFromTitleView() { // If we shouldn't be able to access settings then disable the title view shortcuts guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } @@ -1055,6 +1060,11 @@ extension ConversationVC: } // MARK: MessageCellDelegate + + func willHandleItemCellTapped() { + // Dismiss keyboard when cell is tapped + _ = snInputView.resignFirstResponder() + } func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the unblock modal if needed @@ -2250,10 +2260,15 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - if isShowingSearchUI { willManuallyCancelSearchUI() } - - _ = snInputView.becomeFirstResponder() - completion?() + // Add delay before doing any ui updates + // Delay added to give time for long press actions to dismiss + let delay = completion == nil ? 0 : ContextMenuVC.dismissDuration + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } + _ = self?.snInputView.becomeFirstResponder() + completion?() + } } func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b842c0f713..f45cc2870d 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -382,6 +382,15 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return result }() + + // Handle taps outside of tableview cell + private lazy var tableViewTapGesture: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer() + result.addTarget(self, action: #selector(handleTableViewTap)) + result.cancelsTouchesInView = false + + return result + }() // MARK: - Settings @@ -533,6 +542,9 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa object: nil ) } + + // Gesture + tableView.addGestureRecognizer(tableViewTapGesture) self.viewModel.navigatableState.setupBindings(viewController: self, disposables: &self.viewModel.disposables) diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 37ece1f334..b75531a7e0 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -66,7 +66,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M let result: UISwipeGestureRecognizer = UISwipeGestureRecognizer() result.direction = .down result.addTarget(self, action: #selector(didSwipeDown)) - result.isEnabled = false + result.cancelsTouchesInView = false return result }() @@ -464,7 +464,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self.accessibilityIdentifier = updatedInputState.accessibility?.identifier self.accessibilityLabel = updatedInputState.accessibility?.label tapGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes == .none) - swipeGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes != .none) + inputState = updatedInputState disabledInputLabel.text = (updatedInputState.message ?? "") disabledInputLabel.accessibilityIdentifier = updatedInputState.messageAccessibility?.identifier diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 305ea3a29c..2fa994f359 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -217,6 +217,8 @@ final class CallMessageCell: MessageCell { } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + delegate?.willHandleItemCellTapped() + guard let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 9c626a90c5..8869b6e574 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -183,6 +183,8 @@ final class InfoMessageCell: MessageCell { @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } + delegate?.willHandleItemCellTapped() + if cellViewModel.variant == .infoDisappearingMessagesUpdate && cellViewModel.canDoFollowingSetting() { delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 39730a9d6c..8d42c776ac 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -108,6 +108,9 @@ protocol MessageCellDelegate: ReactionDelegate { func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) func handleReadMoreButtonTapped(_ cell: UITableViewCell, for cellViewModel: MessageViewModel) + + // Handle taps events outside of `handleItemTapped` contents + func willHandleItemCellTapped() } extension MessageCellDelegate { diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 21a9517955..d2fb437430 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -997,6 +997,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } + delegate?.willHandleItemCellTapped() + let location = gestureRecognizer.location(in: self) if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile { From 5e384eb7fb1129ac34183530837917c156f21614 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 14:22:39 +1000 Subject: [PATCH 204/244] Moved and renamed remaining API definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Cleaned up API structures to be more consistent • Moved OpenGroupAPI into SessionNetworkingKit • Moved PushNotificationAPI into SessionNetworkingKit • Renamed to OpenGroupAPI to SOGSAPI --- Session.xcodeproj/project.pbxproj | 648 +++++--- .../Closed Groups/EditGroupViewModel.swift | 2 +- .../ConversationVC+Interaction.swift | 20 +- .../New Conversation/NewMessageScreen.swift | 2 +- .../MessageInfoScreen.swift | 2 +- Session/Meta/AppDelegate.swift | 2 +- Session/Notifications/SyncPushTokensJob.swift | 4 +- Session/Open Groups/JoinOpenGroupVC.swift | 7 +- .../Open Groups/OpenGroupSuggestionGrid.swift | 5 +- .../Settings/DeveloperSettingsViewModel.swift | 16 +- Session/Settings/NukeDataModal.swift | 6 +- .../SessionNetworkScreen+ViewModel.swift | 4 +- .../Crypto/Crypto+SessionMessagingKit.swift | 34 +- .../_023_SplitSnodeReceivedMessageInfo.swift | 4 +- .../_024_ResetUserConfigLastHashes.swift | 2 +- .../_036_GroupsRebuildChanges.swift | 25 +- .../Database/Models/Attachment.swift | 12 +- .../Database/Models/ClosedGroup.swift | 41 +- .../Database/Models/ConfigDump.swift | 22 +- .../Database/Models/OpenGroup.swift | 3 +- .../Jobs/AttachmentDownloadJob.swift | 2 +- .../Jobs/ConfigMessageReceiveJob.swift | 4 +- .../Jobs/ConfigurationSyncJob.swift | 6 +- .../Jobs/DisplayPictureDownloadJob.swift | 6 +- .../Jobs/ExpirationUpdateJob.swift | 2 +- .../Jobs/GarbageCollectionJob.swift | 2 +- .../Jobs/GetExpirationJob.swift | 2 +- .../Jobs/GroupLeavingJob.swift | 2 +- SessionMessagingKit/Jobs/MessageSendJob.swift | 2 +- ...ProcessPendingGroupMemberRemovalsJob.swift | 8 +- .../RetrieveDefaultOpenGroupRoomsJob.swift | 28 +- .../LibSession+GroupInfo.swift | 2 +- .../LibSession/Types/Config.swift | 2 +- .../Messages/Message+Destination.swift | 4 +- .../Messages/Message+Origin.swift | 2 +- SessionMessagingKit/Messages/Message.swift | 16 +- .../Open Groups/Crypto/Crypto+OpenGroup.swift | 87 - .../Open Groups/OpenGroupAPI.swift | 1396 ----------------- .../Open Groups/OpenGroupManager.swift | 60 +- .../Open Groups/Types/Capabilities.swift | 17 - .../Open Groups/Types/PendingChange.swift | 24 +- .../AttachmentUploader.swift | 28 +- .../MessageReceiver+Groups.swift | 6 +- .../MessageReceiver+UnsendRequests.swift | 2 +- .../MessageSender+Groups.swift | 23 +- .../Sending & Receiving/MessageSender.swift | 16 +- .../Models/LegacyGroupOnlyRequest.swift | 12 - .../Models/LegacyGroupRequest.swift | 10 - .../Models/LegacyNotifyRequest.swift | 15 - .../Models/LegacyPushServerResponse.swift | 10 - .../Models/LegacyUnsubscribeRequest.swift | 14 - .../PushNotificationAPI+SMK.swift | 127 ++ .../Notifications/PushNotificationAPI.swift | 320 ---- .../Pollers/CommunityPoller.swift | 88 +- .../Pollers/CurrentUserPoller.swift | 2 +- .../Pollers/GroupPoller.swift | 4 +- .../Pollers/PollerType.swift | 2 +- .../Pollers/SwarmPoller.swift | 16 +- .../MessageViewModel+DeletionActions.swift | 12 +- .../Authentication+SessionMessagingKit.swift | 45 +- .../Utilities/ExtensionHelper.swift | 4 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 2 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 102 +- .../LibSession/LibSessionGroupInfoSpec.swift | 2 +- ...PISpec.swift => CryptoOpenGroupSpec.swift} | 159 +- ...ilitiesSpec.swift => CapabilitySpec.swift} | 28 +- .../Open Groups/OpenGroupManagerSpec.swift | 223 +-- .../Open Groups/Types/SOGSErrorSpec.swift | 24 - .../MessageReceiverGroupsSpec.swift | 49 +- .../MessageSenderGroupsSpec.swift | 53 +- .../_TestUtilities/MockOGMCache.swift | 2 +- .../_TestUtilities/MockPoller.swift | 2 +- .../_TestUtilities/MockSwarmPoller.swift | 2 +- .../Crypto/Crypto+SessionNetworkingKit.swift | 83 +- .../Models/SnodeReceivedMessageInfo.swift | 136 -- .../FileServer/AppVersionResponse.swift | 97 -- .../FileServer/Crypto/Crypto+FileServer.swift | 89 ++ .../FileServer/FileServer.swift | 40 +- .../FileServer/FileServerAPI.swift | 50 + .../FileServer/FileServerEndpoint.swift | 23 + .../Models/AppVersionResponse.swift | 162 +- .../LibSession/LibSession+Networking.swift | 10 +- .../Models/FileUploadResponse.swift | 2 + .../Crypto/Crypto+PushNotification.swift | 41 + .../Models/AuthenticatedRequest.swift | 4 +- .../Models/NotificationMetadata.swift | 22 +- .../Models/SubscribeRequest.swift | 11 +- .../Models/SubscribeResponse.swift | 16 +- .../Models/UnsubscribeRequest.swift | 3 +- .../Models/UnsubscribeResponse.swift | 16 +- .../PushNotification/PushNotification.swift | 29 + .../PushNotificationAPI.swift | 194 +++ .../PushNotificationEndpoint.swift | 4 +- .../Types/ProcessResult.swift | 2 +- .../Types/Request+PushNotificationAPI.swift | 9 +- .../PushNotification}/Types/Service.swift | 6 +- .../PushNotification}/Types/ServiceInfo.swift | 2 +- .../SOGS/Crypto/Crypto+SOGS.swift | 153 ++ .../SOGS/Models/CapabilitiesResponse.swift | 8 +- .../SOGS/Models/DeleteInboxResponse.swift | 9 + .../SOGS/Models/DirectMessage.swift | 34 + .../SOGS/Models/PinnedMessage.swift | 22 + .../SOGS/Models/ReactionResponse.swift | 44 + SessionNetworkingKit/SOGS/Models/Room.swift | 191 +++ .../SOGS/Models/RoomPollInfo.swift | 143 ++ .../SOGS/Models/SOGSMessage.swift | 11 +- .../Models/SendDirectMessageRequest.swift | 2 +- .../Models/SendDirectMessageResponse.swift | 4 +- .../SOGS/Models/SendSOGSMessageRequest.swift | 78 + .../SOGS/Models/UpdateMessageRequest.swift | 35 + .../SOGS/Models/UserBanRequest.swift | 2 +- .../SOGS/Models/UserModeratorRequest.swift | 2 +- .../SOGS/Models/UserUnbanRequest.swift | 2 +- SessionNetworkingKit/SOGS/SOGS.swift | 15 + SessionNetworkingKit/SOGS/SOGSAPI.swift | 229 ++- SessionNetworkingKit/SOGS/SOGSEndpoint.swift | 7 +- SessionNetworkingKit/SOGS/SOGSError.swift | 2 +- .../SOGS/Types/HTTPHeader+SOGS.swift | 1 - .../SOGS/Types/HTTPQueryParam+SOGS.swift | 1 - .../SOGS/Types/Personalization.swift | 14 + .../SOGS/Types/Request+SOGS.swift | 6 +- .../SOGS/Types/UpdateTypes.swift | 9 + .../SessionNetwork/SessionNetworkAPI.swift | 10 +- .../Database/SnodeReceivedMessageInfo.swift | 4 +- .../Models/AppVersionResponse.swift | 97 -- .../Models/DeleteAllBeforeRequest.swift | 10 +- .../Models/DeleteAllMessagesRequest.swift | 10 +- .../Models/DeleteMessagesRequest.swift | 6 +- .../Models/FileUploadResponse.swift | 29 - .../Models/GetExpiriesRequest.swift | 6 +- .../Models/GetMessagesRequest.swift | 10 +- .../Models/GetNetworkTimestampResponse.swift | 4 +- .../Models/LegacyGetMessagesRequest.swift | 6 +- .../Models/LegacySendMessageRequest.swift | 6 +- .../Models/ONSResolveRequest.swift | 4 +- .../Models/ONSResolveResponse.swift | 4 +- .../Models/OxenDaemonRPCRequest.swift | 32 +- .../Models/RevokeSubaccountRequest.swift | 6 +- .../Models/SendMessageRequest.swift | 10 +- .../SnodeAuthenticatedRequestBody.swift | 2 +- .../Models/SnodeBatchRequest.swift | 4 +- .../StorageServer/Models/SnodeMessage.swift | 62 - .../Models/SnodeReceivedMessage.swift | 68 - .../StorageServer/Models/SnodeRequest.swift | 4 +- .../Models/UnrevokeSubaccountRequest.swift | 6 +- .../Models/UpdateExpiryAllRequest.swift | 10 +- .../Models/UpdateExpiryRequest.swift | 6 +- .../StorageServer/SnodeAPI.swift | 799 +++++++++- .../StorageServer/SnodeAPIEndpoint.swift | 2 +- .../StorageServer/SnodeAPIError.swift | 2 +- .../StorageServer/SnodeAPINamespace.swift | 2 +- .../Types/Request+SnodeAPI.swift | 6 +- .../Types/SnodeReceivedMessage.swift | 4 +- SessionNetworkingKit/Types/Network.swift | 138 +- .../SOGS/Crypto/Authentication+SOGS.swift | 50 + .../SOGS/Crypto/CryptoSOGSAPISpec.swift | 180 +++ .../SOGS/Models/CapabilitiesResponse.swift | 38 + .../SOGS}/Models/RoomPollInfoSpec.swift | 10 +- .../SOGS}/Models/RoomSpec.swift | 6 +- .../SOGS}/Models/SOGSMessageSpec.swift | 29 +- .../Models/SendDirectMessageRequestSpec.swift | 4 +- .../Models/SendSOGSMessageRequestSpec.swift | 14 +- .../Models/UpdateMessageRequestSpec.swift | 6 +- .../SOGS/SOGSAPISpec.swift | 1211 +++++++------- .../SOGS}/Types/PersonalizationSpec.swift | 6 +- .../SOGS}/Types/SOGSEndpointSpec.swift | 60 +- .../SOGS/Types/SOGSErrorSpec.swift | 24 + .../CommonSSKMockExtensions.swift | 91 ++ .../_TestUtilities/MockNetwork.swift | 2 +- .../NotificationResolution.swift | 13 +- .../NotificationServiceExtension.swift | 14 +- SessionShareExtension/ThreadPickerVC.swift | 2 +- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 173 files changed, 4406 insertions(+), 4739 deletions(-) delete mode 100644 SessionMessagingKit/Open Groups/OpenGroupAPI.swift delete mode 100644 SessionMessagingKit/Open Groups/Types/Capabilities.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift delete mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift rename SessionMessagingKitTests/Open Groups/Crypto/{CryptoOpenGroupAPISpec.swift => CryptoOpenGroupSpec.swift} (61%) rename SessionMessagingKitTests/Open Groups/Models/{CapabilitiesSpec.swift => CapabilitySpec.swift} (73%) delete mode 100644 SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift delete mode 100644 SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift delete mode 100644 SessionNetworkingKit/FileServer/AppVersionResponse.swift create mode 100644 SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/AuthenticatedRequest.swift (97%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/NotificationMetadata.swift (81%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/SubscribeRequest.swift (96%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/SubscribeResponse.swift (85%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/UnsubscribeRequest.swift (98%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Models/UnsubscribeResponse.swift (85%) create mode 100644 SessionNetworkingKit/PushNotification/PushNotification.swift create mode 100644 SessionNetworkingKit/PushNotification/PushNotificationAPI.swift rename SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift => SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift (80%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Types/ProcessResult.swift (84%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Types/Request+PushNotificationAPI.swift (68%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Types/Service.swift (84%) rename {SessionMessagingKit/Sending & Receiving/Notifications => SessionNetworkingKit/PushNotification}/Types/ServiceInfo.swift (91%) create mode 100644 SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift create mode 100644 SessionNetworkingKit/SOGS/Models/DirectMessage.swift create mode 100644 SessionNetworkingKit/SOGS/Models/PinnedMessage.swift create mode 100644 SessionNetworkingKit/SOGS/Models/ReactionResponse.swift create mode 100644 SessionNetworkingKit/SOGS/Models/Room.swift create mode 100644 SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift create mode 100644 SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift create mode 100644 SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift create mode 100644 SessionNetworkingKit/SOGS/Types/Personalization.swift create mode 100644 SessionNetworkingKit/SOGS/Types/UpdateTypes.swift delete mode 100644 SessionNetworkingKit/StorageServer/Models/AppVersionResponse.swift delete mode 100644 SessionNetworkingKit/StorageServer/Models/FileUploadResponse.swift delete mode 100644 SessionNetworkingKit/StorageServer/Models/SnodeMessage.swift delete mode 100644 SessionNetworkingKit/StorageServer/Models/SnodeReceivedMessage.swift create mode 100644 SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift create mode 100644 SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift create mode 100644 SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Models/RoomPollInfoSpec.swift (92%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Models/RoomSpec.swift (93%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Models/SOGSMessageSpec.swift (92%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Models/SendDirectMessageRequestSpec.swift (86%) rename SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift => SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift (82%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Models/UpdateMessageRequestSpec.swift (87%) rename SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift => SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift (74%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Types/PersonalizationSpec.swift (78%) rename {SessionMessagingKitTests/Open Groups => SessionNetworkingKitTests/SOGS}/Types/SOGSEndpointSpec.swift (56%) create mode 100644 SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 6302fff458..582038180a 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -106,9 +106,7 @@ 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; - 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */; }; - 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; @@ -177,10 +175,9 @@ 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; }; - 947D7FD42D509FC900E8E413 /* SessionNetworkAPI+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */; }; 947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */; }; - 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */; }; - 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */; }; + 947D7FD72D509FC900E8E413 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* HTTPClient.swift */; }; + 947D7FD82D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */; }; 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */; }; 947D7FE72D51837200E8E413 /* ArrowCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */; }; 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; @@ -249,7 +246,6 @@ B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3F255A580C00E217F9 /* String+SSK.swift */; }; B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */; }; - B886B4A72398B23E00211ABE /* (null) in Sources */ = {isa = PBXBuildFile; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; @@ -318,7 +314,6 @@ C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; - C352A2FF25574B6300338F3E /* (null) in Sources */ = {isa = PBXBuildFile; }; C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */; }; @@ -423,8 +418,6 @@ FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; FD0150542CA24471005B08A1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150532CA24471005B08A1 /* Nimble */; }; FD0150582CA27DF3005B08A1 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */; }; - FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; - FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */; }; FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */; }; @@ -580,7 +573,6 @@ FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */; }; FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF306255B6DBE007E1867 /* OWSWindowManager.m */; }; FD245C632850664600B966DD /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD245C612850664300B966DD /* Configuration.swift */; }; - FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* OpenGroupAPI.swift */; }; FD245C682850666300B966DD /* Message+Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A30825574D8400338F3E /* Message+Destination.swift */; }; FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */; }; FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; }; @@ -621,8 +613,6 @@ FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */; }; FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */; }; FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */; }; - FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */; }; - FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */; }; FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; }; FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; }; FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; }; @@ -670,7 +660,6 @@ FD42ECD22E3071DE002D03EA /* ThemeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD12E3071DC002D03EA /* ThemeText.swift */; }; FD42ECD42E32FF2E002D03EA /* StringUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD32E32FF2A002D03EA /* StringUtilitiesSpec.swift */; }; FD42ECD62E3308B5002D03EA /* ObservableKey+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD52E3308AC002D03EA /* ObservableKey+SessionUtilitiesKit.swift */; }; - FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD432433299C6985008A0213 /* PendingReadReceipt.swift */; }; FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */; }; FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */; }; @@ -691,7 +680,6 @@ FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; FD4BB22B2D63F20700D0DC3D /* MigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */; }; - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */ = {isa = PBXBuildFile; }; FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; @@ -740,11 +728,74 @@ FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; + FD6B928C2E779DCC004463B5 /* FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928B2E779DC8004463B5 /* FileServer.swift */; }; + FD6B928E2E779E99004463B5 /* FileServerEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */; }; + FD6B92902E779EDD004463B5 /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */; }; + FD6B92922E779FC8004463B5 /* SessionNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92912E779FC6004463B5 /* SessionNetwork.swift */; }; + FD6B92942E77A003004463B5 /* SessionNetworkEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */; }; + FD6B92972E77A047004463B5 /* Price.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92962E77A042004463B5 /* Price.swift */; }; + FD6B92992E77A06E004463B5 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92982E77A06C004463B5 /* Token.swift */; }; + FD6B929B2E77A084004463B5 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B929A2E77A083004463B5 /* NetworkInfo.swift */; }; + FD6B929D2E77A096004463B5 /* Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B929C2E77A095004463B5 /* Info.swift */; }; + FD6B92A32E77A18B004463B5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92A22E77A189004463B5 /* SnodeAPI.swift */; }; + FD6B92AB2E77A920004463B5 /* SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92A92E77A8F8004463B5 /* SOGS.swift */; }; + FD6B92AC2E77A993004463B5 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; + FD6B92AD2E77A9F1004463B5 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.swift */; }; + FD6B92AE2E77A9F7004463B5 /* SOGSAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* SOGSAPI.swift */; }; + FD6B92AF2E77AA03004463B5 /* HTTPQueryParam+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */; }; + FD6B92B02E77AA03004463B5 /* Request+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+SOGS.swift */; }; + FD6B92B12E77AA03004463B5 /* HTTPHeader+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */; }; + FD6B92B22E77AA03004463B5 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; + FD6B92B32E77AA03004463B5 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; + FD6B92B42E77AA11004463B5 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; + FD6B92B52E77AA11004463B5 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; + FD6B92B62E77AA11004463B5 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; + FD6B92B72E77AA11004463B5 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; + FD6B92B82E77AA11004463B5 /* SendSOGSMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */; }; + FD6B92B92E77AA11004463B5 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; + FD6B92BA2E77AA11004463B5 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; + FD6B92BB2E77AA11004463B5 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; + FD6B92BC2E77AA11004463B5 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; + FD6B92BD2E77AA11004463B5 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; + FD6B92BE2E77AA11004463B5 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; + FD6B92BF2E77AA11004463B5 /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FD6B92C02E77AA11004463B5 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; + FD6B92C12E77AA11004463B5 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */; }; + FD6B92C22E77AA11004463B5 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; + FD6B92C62E77AD0F004463B5 /* Crypto+FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */; }; + FD6B92C82E77AD39004463B5 /* Crypto+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */; }; + FD6B92CD2E77B22D004463B5 /* SOGSMessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */; }; + FD6B92CE2E77B234004463B5 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; + FD6B92CF2E77B234004463B5 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; + FD6B92D02E77B23B004463B5 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */; }; + FD6B92D12E77B253004463B5 /* SendSOGSMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */; }; + FD6B92D22E77B270004463B5 /* SendDirectMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */; }; + FD6B92D32E77B270004463B5 /* UpdateMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */; }; + FD6B92D42E77B2C7004463B5 /* SOGSAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */; }; + FD6B92D62E77B55D004463B5 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; + FD6B92D72E77B55D004463B5 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; + FD6B92D82E77B55D004463B5 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; + FD6B92DB2E77B597004463B5 /* CryptoSOGSAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */; }; + FD6B92DE2E77BDE2004463B5 /* Authentication+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */; }; + FD6B92E12E77C1E1004463B5 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E02E77C1DC004463B5 /* PushNotification.swift */; }; + FD6B92E22E77C21D004463B5 /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */; }; + FD6B92E62E77C5A2004463B5 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; + FD6B92E72E77C5A2004463B5 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; + FD6B92E82E77C5B7004463B5 /* PushNotificationEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */; }; + FD6B92E92E77C5D1004463B5 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; + FD6B92EA2E77C5D1004463B5 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; + FD6B92EB2E77C5D1004463B5 /* AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */; }; + FD6B92EF2E77C5D1004463B5 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; + FD6B92F02E77C5D1004463B5 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; + FD6B92F22E77C5D1004463B5 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; + FD6B92F42E77C61A004463B5 /* ServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */; }; + FD6B92F72E77C6D7004463B5 /* Crypto+PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */; }; + FD6B92F82E77C725004463B5 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; - FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */; }; FD6F5B602E657A33009A8D01 /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; @@ -790,7 +841,7 @@ FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */; }; FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */; }; - FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */; }; + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */; }; FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD74433F2D07A25C00862443 /* PushRegistrationManager.swift */; }; FD7443422D07A27E00862443 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7443412D07A27E00862443 /* SyncPushTokensJob.swift */; }; FD74434A2D07CA9F00862443 /* Codable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7443492D07CA9F00862443 /* Codable+Utilities.swift */; }; @@ -824,8 +875,7 @@ FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */; }; - FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; }; - FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; + FD83B9C727CF3F10005E1583 /* CapabilitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */; }; FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; }; FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; }; @@ -941,56 +991,20 @@ FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; - FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; - FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; - FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; - FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */; }; - FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */; }; - FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; - FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; - FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; }; FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; - FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; - FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; - FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; }; - FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */; }; - FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */; }; - FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */; }; - FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; - FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; - FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; }; - FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; - FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; - FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; }; - FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; - FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; - FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; - FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; - FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; }; FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; - FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; }; - FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */; }; - FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; - FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; - FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; - FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; - FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; - FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; }; FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; - FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; - FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */; }; FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */; }; FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */; }; @@ -1011,7 +1025,6 @@ FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; - FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; @@ -1030,7 +1043,7 @@ FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; - FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */; }; + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */; }; FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */; }; FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */; }; @@ -1079,12 +1092,6 @@ FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */; }; FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755212C9BC1BA002A2623 /* LibSessionError.swift */; }; FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */; }; - FDEF57212C3CF03A00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57222C3CF03D00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57232C3CF04300131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57242C3CF04700131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57252C3CF04C00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57262C3CF05F00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */; }; FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; @@ -1106,11 +1113,8 @@ FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */; }; FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */; }; FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */; }; - FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; - FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */; }; FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; }; FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */; }; - FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */; }; FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */; }; FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */; }; FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */; }; @@ -1146,7 +1150,6 @@ FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; }; FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; }; FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; }; - FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */; }; @@ -1551,9 +1554,8 @@ 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = ""; }; 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkAPI.swift; sourceTree = ""; }; - 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Database.swift"; sourceTree = ""; }; - 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Models.swift"; sourceTree = ""; }; - 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Network.swift"; sourceTree = ""; }; + 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyValueStore+SessionNetwork.swift"; sourceTree = ""; }; + 947D7FD22D509FC900E8E413 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 947D7FD92D5180F200E8E413 /* SessionNetworkScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkScreen.swift; sourceTree = ""; }; 947D7FDA2D5180F200E8E413 /* SessionNetworkScreen+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+Models.swift"; sourceTree = ""; }; 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+ViewModel.swift"; sourceTree = ""; }; @@ -1622,7 +1624,7 @@ B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = ""; }; B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; - B88FA7B726045D100049422F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = ""; }; + B88FA7B726045D100049422F /* SOGSAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSAPI.swift; sourceTree = ""; }; B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSuggestionGrid.swift; sourceTree = ""; }; B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; @@ -1801,7 +1803,7 @@ FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = ""; }; FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = ""; }; - FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; + FD02CC132C3677E6009AB976 /* Request+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+SOGS.swift"; sourceTree = ""; }; FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = ""; }; FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipPayloadKey.swift; sourceTree = ""; }; FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _043_RenameAttachments.swift; sourceTree = ""; }; @@ -2047,9 +2049,27 @@ FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_SNK_SetupStandardJobs.swift; sourceTree = ""; }; + FD6B928B2E779DC8004463B5 /* FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServer.swift; sourceTree = ""; }; + FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServerEndpoint.swift; sourceTree = ""; }; + FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = ""; }; + FD6B92912E779FC6004463B5 /* SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetwork.swift; sourceTree = ""; }; + FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkEndpoint.swift; sourceTree = ""; }; + FD6B92962E77A042004463B5 /* Price.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Price.swift; sourceTree = ""; }; + FD6B92982E77A06C004463B5 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + FD6B929A2E77A083004463B5 /* NetworkInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfo.swift; sourceTree = ""; }; + FD6B929C2E77A095004463B5 /* Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = ""; }; + FD6B92A22E77A189004463B5 /* SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; + FD6B92A92E77A8F8004463B5 /* SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGS.swift; sourceTree = ""; }; + FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+FileServer.swift"; sourceTree = ""; }; + FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SOGS.swift"; sourceTree = ""; }; + FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; + FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSOGSAPISpec.swift; sourceTree = ""; }; + FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Authentication+SOGS.swift"; sourceTree = ""; }; + FD6B92E02E77C1DC004463B5 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = ""; }; + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PushNotificationAPI+SMK.swift"; sourceTree = ""; }; + FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+PushNotification.swift"; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; - FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; @@ -2090,7 +2110,7 @@ FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowLevel+Utilities.swift"; sourceTree = ""; }; FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSMKSpec.swift; sourceTree = ""; }; - FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOpenGroupAPISpec.swift; sourceTree = ""; }; + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOpenGroupSpec.swift; sourceTree = ""; }; FD74433F2D07A25C00862443 /* PushRegistrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = ""; }; FD7443412D07A27E00862443 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = ""; }; FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Utilities.swift"; sourceTree = ""; }; @@ -2120,7 +2140,7 @@ FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = ""; }; FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSpec.swift; sourceTree = ""; }; - FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = ""; }; + FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitySpec.swift; sourceTree = ""; }; FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryWithDependencies.swift; sourceTree = ""; }; @@ -2222,18 +2242,16 @@ FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; - FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = ""; }; - FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = ""; }; + FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationEndpoint.swift; sourceTree = ""; }; FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; - FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; FDC1BD652CFD6C4E002CDC71 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryArchiver.swift; sourceTree = ""; }; FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; - FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; + FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSOGSMessageRequestSpec.swift; sourceTree = ""; }; FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequestSpec.swift; sourceTree = ""; }; FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequestSpec.swift; sourceTree = ""; }; FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessageSpec.swift; sourceTree = ""; }; @@ -2242,20 +2260,19 @@ FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = ""; }; FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerSpec.swift; sourceTree = ""; }; FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; - FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = ""; }; + FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; - FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* AppVersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionResponse.swift; sourceTree = ""; }; FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = ""; }; FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessage.swift; sourceTree = ""; }; FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfo.swift; sourceTree = ""; }; - FDC4386627B4E10E00C60D73 /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = ""; }; + FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; - FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; + FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSOGSMessageRequest.swift; sourceTree = ""; }; FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionMessagingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPISpec.swift; sourceTree = ""; }; + FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSAPISpec.swift; sourceTree = ""; }; FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = ""; }; FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = ""; }; FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = ""; }; @@ -2266,7 +2283,6 @@ FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationAction.swift; sourceTree = ""; }; FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; - FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesRequest.swift; sourceTree = ""; }; FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesResponse.swift; sourceTree = ""; }; @@ -2295,7 +2311,7 @@ FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunityPoller.swift; sourceTree = ""; }; - FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroupAPI.swift"; sourceTree = ""; }; + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroup.swift"; sourceTree = ""; }; FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmPoller.swift; sourceTree = ""; }; FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReceiverGroupsSpec.swift; sourceTree = ""; }; FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSenderGroupsSpec.swift; sourceTree = ""; }; @@ -2379,10 +2395,9 @@ FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_RemoveLegacyYDB.swift; sourceTree = ""; }; FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionMessage.swift; sourceTree = ""; }; FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LibSession.swift"; sourceTree = ""; }; - FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; - FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPQueryParam+OpenGroup.swift"; sourceTree = ""; }; + FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+SOGS.swift"; sourceTree = ""; }; + FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPQueryParam+SOGS.swift"; sourceTree = ""; }; FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPINamespace.swift; sourceTree = ""; }; - FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = ""; }; FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMessagesRequest.swift; sourceTree = ""; }; FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeResponse.swift; sourceTree = ""; }; @@ -2850,16 +2865,16 @@ path = SwiftUI; sourceTree = ""; }; - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */ = { + 947D7FD32D509FC900E8E413 /* SessionNetwork */ = { isa = PBXGroup; children = ( - 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */, + FD6B92952E77A036004463B5 /* Models */, + FD6B929E2E77A0E8004463B5 /* Types */, + FD6B92912E779FC6004463B5 /* SessionNetwork.swift */, 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */, - 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */, - 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */, - 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */, + FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */, ); - path = SessionNetworkAPI; + path = SessionNetwork; sourceTree = ""; }; 947D7FDC2D5180F200E8E413 /* SessionNetworkScreen */ = { @@ -3565,9 +3580,8 @@ isa = PBXGroup; children = ( FDC13D4E2A16EE41007267C7 /* Types */, - FDC4382D27B383A600C60D73 /* Models */, FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */, - C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */, ); path = Notifications; sourceTree = ""; @@ -3623,9 +3637,7 @@ isa = PBXGroup; children = ( FD23CE202A661CE80000B97C /* Crypto */, - FDC4381827B34EAD00C60D73 /* Models */, - FDC4380727B31D3A00C60D73 /* Types */, - B88FA7B726045D100049422F /* OpenGroupAPI.swift */, + FDC4381827B34EAD00C60D73 /* Types */, C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */, ); path = "Open Groups"; @@ -3673,11 +3685,13 @@ children = ( C3C2A5B0255385C700C340D1 /* Meta */, FDE754E22C9BAFF4002A2623 /* Crypto */, - FD17D79D27F40CAA00122BE0 /* Database */, + FD6B928A2E779DB6004463B5 /* FileServer */, FD7F74682BAB8A5D006DDFD8 /* LibSession */, - FDF8489929405C5A007DCAE5 /* Models */, - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, - FD2272842C33E28D004D8A6C /* SnodeAPI */, + FD6B92DF2E77C1CB004463B5 /* PushNotification */, + 947D7FD32D509FC900E8E413 /* SessionNetwork */, + FD6B92892E779D8D004463B5 /* SOGS */, + FD2272842C33E28D004D8A6C /* StorageServer */, + FD6B92A52E77A3BD004463B5 /* Models */, FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, ); @@ -4063,19 +4077,11 @@ sourceTree = ""; }; FD17D79D27F40CAA00122BE0 /* Database */ = { - isa = PBXGroup; - children = ( - FD17D7A827F41BE300122BE0 /* Models */, - ); - path = Database; - sourceTree = ""; - }; - FD17D7A827F41BE300122BE0 /* Models */ = { isa = PBXGroup; children = ( FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */, ); - path = Models; + path = Database; sourceTree = ""; }; FD17D7B427F51E6700122BE0 /* Types */ = { @@ -4123,17 +4129,18 @@ path = Database; sourceTree = ""; }; - FD2272842C33E28D004D8A6C /* SnodeAPI */ = { + FD2272842C33E28D004D8A6C /* StorageServer */ = { isa = PBXGroup; children = ( - FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, + FD17D79D27F40CAA00122BE0 /* Database */, + FDF8489929405C5A007DCAE5 /* Models */, + FD6B92A12E77A153004463B5 /* Types */, + FD6B92A22E77A189004463B5 /* SnodeAPI.swift */, FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */, FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, - FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, - FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, ); - path = SnodeAPI; + path = StorageServer; sourceTree = ""; }; FD2272C52C34E9D1004D8A6C /* Types */ = { @@ -4175,7 +4182,7 @@ FD23CE202A661CE80000B97C /* Crypto */ = { isa = PBXGroup; children = ( - FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */, + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */, ); path = Crypto; sourceTree = ""; @@ -4335,6 +4342,206 @@ path = Models; sourceTree = ""; }; + FD6B92892E779D8D004463B5 /* SOGS */ = { + isa = PBXGroup; + children = ( + FD6B92C32E77ACF2004463B5 /* Crypto */, + FD6B92A82E77A8B2004463B5 /* Models */, + FD6B92A72E77A875004463B5 /* Types */, + FD6B92A92E77A8F8004463B5 /* SOGS.swift */, + B88FA7B726045D100049422F /* SOGSAPI.swift */, + FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, + FDC4380827B31D4E00C60D73 /* SOGSError.swift */, + ); + path = SOGS; + sourceTree = ""; + }; + FD6B928A2E779DB6004463B5 /* FileServer */ = { + isa = PBXGroup; + children = ( + FD6B92C42E77AD01004463B5 /* Crypto */, + FD6B92A42E77A37A004463B5 /* Models */, + FD6B928B2E779DC8004463B5 /* FileServer.swift */, + FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */, + FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */, + ); + path = FileServer; + sourceTree = ""; + }; + FD6B92952E77A036004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FD6B929C2E77A095004463B5 /* Info.swift */, + FD6B929A2E77A083004463B5 /* NetworkInfo.swift */, + FD6B92962E77A042004463B5 /* Price.swift */, + FD6B92982E77A06C004463B5 /* Token.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B929E2E77A0E8004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */, + 947D7FD22D509FC900E8E413 /* HTTPClient.swift */, + 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A12E77A153004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, + FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, + FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, + FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A42E77A37A004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4383727B3863200C60D73 /* AppVersionResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92A52E77A3BD004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92A72E77A875004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */, + FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */, + FDC4381627B32EC700C60D73 /* Personalization.swift */, + FD02CC132C3677E6009AB976 /* Request+SOGS.swift */, + 7B81682228A4C1210069F315 /* UpdateTypes.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A82E77A8B2004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */, + FDC4385C27B4C18900C60D73 /* Room.swift */, + FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, + FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, + FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */, + FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, + FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */, + FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, + FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, + FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */, + FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */, + FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, + FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, + FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + 7B81682928B6F1420069F315 /* ReactionResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92C32E77ACF2004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92C42E77AD01004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92C92E77B1A7004463B5 /* SOGS */ = { + isa = PBXGroup; + children = ( + FD6B92D92E77B58B004463B5 /* Crypto */, + FD6B92CA2E77B1AE004463B5 /* Models */, + FD6B92D52E77B54B004463B5 /* Types */, + FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */, + ); + path = SOGS; + sourceTree = ""; + }; + FD6B92CA2E77B1AE004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */, + FDC2908627D7047F005DAE71 /* RoomSpec.swift */, + FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, + FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, + FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */, + FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, + FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92D52E77B54B004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, + FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, + FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92D92E77B58B004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */, + FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92DF2E77C1CB004463B5 /* PushNotification */ = { + isa = PBXGroup; + children = ( + FD6B92F52E77C6AF004463B5 /* Crypto */, + FDC4382D27B383A600C60D73 /* Models */, + FD6B92E52E77C33B004463B5 /* Types */, + FD6B92E02E77C1DC004463B5 /* PushNotification.swift */, + C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, + FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */, + ); + path = PushNotification; + sourceTree = ""; + }; + FD6B92E52E77C33B004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, + FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */, + FDC13D482A16EC20007267C7 /* Service.swift */, + FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92F52E77C6AF004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */, + ); + path = Crypto; + sourceTree = ""; + }; FD7115F528C8150600B47552 /* Combine */ = { isa = PBXGroup; children = ( @@ -4459,7 +4666,7 @@ FD72BDA52BE369B600CF6CF6 /* Crypto */ = { isa = PBXGroup; children = ( - FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */, + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */, ); path = Crypto; sourceTree = ""; @@ -4586,14 +4793,8 @@ FD83B9C127CF33EE005E1583 /* Models */ = { isa = PBXGroup; children = ( - FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */, + FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */, FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, - FDC2908627D7047F005DAE71 /* RoomSpec.swift */, - FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, - FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, - FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */, - FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, - FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, ); path = Models; sourceTree = ""; @@ -4728,6 +4929,7 @@ children = ( FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */, FD3765DD2AD8F02300DC1489 /* _TestUtilities */, + FD6B92C92E77B1A7004463B5 /* SOGS */, FDAA16792AC28E2200DDBF77 /* Models */, FD2272C52C34E9D1004D8A6C /* Types */, ); @@ -4741,11 +4943,6 @@ FD981BD62DC9A61600564172 /* NotificationCategory.swift */, FDB11A4B2DCC527900BEF49F /* NotificationContent.swift */, FD981BD82DC9A69000564172 /* NotificationUserInfoKey.swift */, - FDC13D482A16EC20007267C7 /* Service.swift */, - FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */, - FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, - FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */, - FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */, ); path = Types; sourceTree = ""; @@ -4769,51 +4966,12 @@ path = LibSession; sourceTree = ""; }; - FDC2909227D710A9005DAE71 /* Types */ = { + FDC4381827B34EAD00C60D73 /* Types */ = { isa = PBXGroup; children = ( - FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, - FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, - FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, - ); - path = Types; - sourceTree = ""; - }; - FDC4380727B31D3A00C60D73 /* Types */ = { - isa = PBXGroup; - children = ( - FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */, - FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */, - FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */, - FDC4381627B32EC700C60D73 /* Personalization.swift */, - FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */, - FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, - 7B81682228A4C1210069F315 /* UpdateTypes.swift */, - ); - path = Types; - sourceTree = ""; - }; - FDC4381827B34EAD00C60D73 /* Models */ = { - isa = PBXGroup; - children = ( - FDC4386627B4E10E00C60D73 /* Capabilities.swift */, - FDC4385C27B4C18900C60D73 /* Room.swift */, - FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, - FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, - FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, - FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, - FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */, - FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, - FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, - FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */, - FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */, - FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, - FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, - FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, - 7B81682928B6F1420069F315 /* ReactionResponse.swift */, 7B81682B28B72F480069F315 /* PendingChange.swift */, ); - path = Models; + path = Types; sourceTree = ""; }; FDC4382D27B383A600C60D73 /* Models */ = { @@ -4824,11 +4982,6 @@ FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */, - FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */, - FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */, - FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */, - FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */, - FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */, FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */, ); path = Models; @@ -4857,8 +5010,6 @@ children = ( FD72BDA52BE369B600CF6CF6 /* Crypto */, FD83B9C127CF33EE005E1583 /* Models */, - FDC2909227D710A9005DAE71 /* Types */, - FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */, FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */, ); path = "Open Groups"; @@ -5064,14 +5215,10 @@ FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */, FDF848BA29405C5A007DCAE5 /* RevokeSubaccountRequest.swift */, FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */, - FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, - FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */, FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */, FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */, FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */, - FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */, - FDC4383727B3863200C60D73 /* AppVersionResponse.swift */, ); path = Models; sourceTree = ""; @@ -6187,17 +6334,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD6B92E12E77C1E1004463B5 /* PushNotification.swift in Sources */, FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */, FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */, + FD6B928E2E779E99004463B5 /* FileServerEndpoint.swift in Sources */, FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */, FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, + FD6B92F82E77C725004463B5 /* ProcessResult.swift in Sources */, + FD6B92902E779EDD004463B5 /* FileServerAPI.swift in Sources */, FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, + FD6B92E82E77C5B7004463B5 /* PushNotificationEndpoint.swift in Sources */, + FD6B92AC2E77A993004463B5 /* SOGSEndpoint.swift in Sources */, + FD6B92922E779FC8004463B5 /* SessionNetwork.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */, FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, @@ -6205,9 +6359,17 @@ FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, + FD6B92992E77A06E004463B5 /* Token.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */, + FD6B92AF2E77AA03004463B5 /* HTTPQueryParam+SOGS.swift in Sources */, + FD6B92B02E77AA03004463B5 /* Request+SOGS.swift in Sources */, + FD6B92B12E77AA03004463B5 /* HTTPHeader+SOGS.swift in Sources */, + FD6B92F72E77C6D7004463B5 /* Crypto+PushNotification.swift in Sources */, + FD6B92B22E77AA03004463B5 /* UpdateTypes.swift in Sources */, + FD6B92B32E77AA03004463B5 /* Personalization.swift in Sources */, + FD6B929B2E77A084004463B5 /* NetworkInfo.swift in Sources */, FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */, FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, @@ -6219,35 +6381,69 @@ FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */, FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */, FD2272B02C33E337004D8A6C /* NetworkError.swift in Sources */, + FD6B92AB2E77A920004463B5 /* SOGS.swift in Sources */, FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */, - 947D7FD42D509FC900E8E413 /* SessionNetworkAPI+Models.swift in Sources */, + FD6B92E92E77C5D1004463B5 /* SubscribeResponse.swift in Sources */, + FD6B92EA2E77C5D1004463B5 /* NotificationMetadata.swift in Sources */, + FD6B92EB2E77C5D1004463B5 /* AuthenticatedRequest.swift in Sources */, + FD6B92EF2E77C5D1004463B5 /* UnsubscribeRequest.swift in Sources */, + FD6B92F02E77C5D1004463B5 /* SubscribeRequest.swift in Sources */, + FD6B92F22E77C5D1004463B5 /* UnsubscribeResponse.swift in Sources */, 947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */, - 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */, - 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */, + 947D7FD72D509FC900E8E413 /* HTTPClient.swift in Sources */, + FD6B92B42E77AA11004463B5 /* PinnedMessage.swift in Sources */, + FD6B92B52E77AA11004463B5 /* SendDirectMessageResponse.swift in Sources */, + FD6B92B62E77AA11004463B5 /* UserUnbanRequest.swift in Sources */, + FD6B92B72E77AA11004463B5 /* UserModeratorRequest.swift in Sources */, + FD6B92B82E77AA11004463B5 /* SendSOGSMessageRequest.swift in Sources */, + FD6B92B92E77AA11004463B5 /* Room.swift in Sources */, + FD6B92BA2E77AA11004463B5 /* RoomPollInfo.swift in Sources */, + FD6B92BB2E77AA11004463B5 /* UpdateMessageRequest.swift in Sources */, + FD6B92BC2E77AA11004463B5 /* ReactionResponse.swift in Sources */, + FD6B92BD2E77AA11004463B5 /* UserBanRequest.swift in Sources */, + FD6B92BE2E77AA11004463B5 /* DirectMessage.swift in Sources */, + FD6B92BF2E77AA11004463B5 /* DeleteInboxResponse.swift in Sources */, + FD6B92C02E77AA11004463B5 /* SendDirectMessageRequest.swift in Sources */, + FD6B92C12E77AA11004463B5 /* CapabilitiesResponse.swift in Sources */, + FD6B92C22E77AA11004463B5 /* SOGSMessage.swift in Sources */, + 947D7FD82D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift in Sources */, FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */, + FD6B92F42E77C61A004463B5 /* ServiceInfo.swift in Sources */, FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */, + FD6B92A32E77A18B004463B5 /* SnodeAPI.swift in Sources */, FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, + FD6B92E22E77C21D004463B5 /* PushNotificationAPI.swift in Sources */, FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, + FD6B928C2E779DCC004463B5 /* FileServer.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, + FD6B92C82E77AD39004463B5 /* Crypto+SOGS.swift in Sources */, + FD6B92942E77A003004463B5 /* SessionNetworkEndpoint.swift in Sources */, FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */, FD2272BB2C33E337004D8A6C /* HTTPMethod.swift in Sources */, + FD6B92AE2E77A9F7004463B5 /* SOGSAPI.swift in Sources */, + FD6B92972E77A047004463B5 /* Price.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */, + FD6B92AD2E77A9F1004463B5 /* SOGSError.swift in Sources */, FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */, FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, - FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, + FD6B92C62E77AD0F004463B5 /* Crypto+FileServer.swift in Sources */, FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, + FD6B92E62E77C5A2004463B5 /* Service.swift in Sources */, + FD6B92E72E77C5A2004463B5 /* Request+PushNotificationAPI.swift in Sources */, + FD6B92DE2E77BDE2004463B5 /* Authentication+SOGS.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, + FD6B929D2E77A096004463B5 /* Info.swift in Sources */, FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, @@ -6310,7 +6506,6 @@ FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */, - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */, FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */, @@ -6418,7 +6613,6 @@ FD981BD72DC9A61A00564172 /* NotificationCategory.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */, - FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */, @@ -6442,11 +6636,9 @@ FD22727E2C32911C004D8A6C /* GarbageCollectionJob.swift in Sources */, FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */, FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, - FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */, FDE5219E2E0D0B9B00061B8E /* AsyncAccessible.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */, - 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */, FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */, FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */, @@ -6460,49 +6652,36 @@ FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */, FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */, FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */, - FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */, FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */, FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */, - FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, - FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */, 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, - FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */, FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */, FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */, - 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, - FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, - FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, FD22727C2C32911C004D8A6C /* GroupPromoteMemberJob.swift in Sources */, FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */, - FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, - FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */, FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */, - FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */, - FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */, FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */, - FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */, + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */, FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */, - FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, - FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, @@ -6523,7 +6702,6 @@ FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */, 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */, FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */, - FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */, FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */, @@ -6532,7 +6710,6 @@ C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, - FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */, FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, @@ -6541,28 +6718,19 @@ FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */, FD2272732C32911C004D8A6C /* ConfigurationSyncJob.swift in Sources */, FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */, - FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */, 94B6BAFA2E38454F00E718BB /* SessionProState.swift in Sources */, - FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */, FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */, - FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */, FD2272752C32911C004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, FD2272712C32911C004D8A6C /* MessageReceiveJob.swift in Sources */, FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */, - FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */, FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */, - FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, - FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */, - FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */, FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */, - FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */, FDB11A5F2DD5B77800BEF49F /* Message+Origin.swift in Sources */, FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, - FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */, 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, @@ -6578,34 +6746,26 @@ FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */, FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */, - FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */, FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, - FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, - FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, FD981BC42DC304E600564172 /* MessageDeduplication.swift in Sources */, FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */, FD09796E27FA6D0000936362 /* Contact.swift in Sources */, - FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */, FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, - FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */, FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */, - FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */, FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */, - FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */, FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */, FD981BD92DC9A69600564172 /* NotificationUserInfoKey.swift in Sources */, - C352A2FF25574B6300338F3E /* (null) in Sources */, FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */, FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, @@ -6620,7 +6780,6 @@ FD2273012C352D8E004D8A6C /* LibSession+Shared.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, FD245C682850666300B966DD /* Message+Destination.swift in Sources */, - FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, FD981BC62DC3310B00564172 /* ExtensionHelper.swift in Sources */, @@ -6639,7 +6798,6 @@ FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, FD09798127FCFEE800936362 /* SessionThread.swift in Sources */, - FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */, FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */, FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */, FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, @@ -6648,9 +6806,9 @@ FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */, - FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */, FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */, FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SMK.swift in Sources */, FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FDB11A522DCC6B0000BEF49F /* OpenGroupUrlInfo.swift in Sources */, @@ -6658,9 +6816,6 @@ FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, - FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, - FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, - FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6692,7 +6847,6 @@ B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */, - FDEF57222C3CF03D00131302 /* (null) in Sources */, 7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */, FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */, FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */, @@ -6722,7 +6876,6 @@ B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, - FDEF57262C3CF05F00131302 /* (null) in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */, 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, @@ -6766,7 +6919,6 @@ 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, - FDEF57232C3CF04300131302 /* (null) in Sources */, FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */, FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, @@ -6800,7 +6952,6 @@ 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */, FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */, - B886B4A72398B23E00211ABE /* (null) in Sources */, 94CD96322E1B88C20097754D /* ExpandingAttachmentsButton.swift in Sources */, 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, @@ -6840,13 +6991,11 @@ 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, - FDEF57242C3CF04700131302 /* (null) in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, - FDEF57212C3CF03A00131302 /* (null) in Sources */, 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, @@ -6886,7 +7035,6 @@ 940943402C7ED62300D9D2E0 /* StartupError.swift in Sources */, FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */, 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */, - FDEF57252C3CF04C00131302 /* (null) in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, @@ -6995,6 +7143,8 @@ FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */, FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */, + FD6B92CD2E77B22D004463B5 /* SOGSMessageSpec.swift in Sources */, + FD6B92DB2E77B597004463B5 /* CryptoSOGSAPISpec.swift in Sources */, FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */, FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */, FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */, @@ -7003,11 +7153,19 @@ FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */, FDB11A582DD17D0600BEF49F /* MockLogger.swift in Sources */, FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */, + FD6B92D22E77B270004463B5 /* SendDirectMessageRequestSpec.swift in Sources */, + FD6B92D32E77B270004463B5 /* UpdateMessageRequestSpec.swift in Sources */, FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */, FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */, + FD6B92D02E77B23B004463B5 /* CapabilitiesResponse.swift in Sources */, + FD6B92D42E77B2C7004463B5 /* SOGSAPISpec.swift in Sources */, FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */, FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */, + FD6B92D12E77B253004463B5 /* SendSOGSMessageRequestSpec.swift in Sources */, + FD6B92D62E77B55D004463B5 /* SOGSEndpointSpec.swift in Sources */, + FD6B92D72E77B55D004463B5 /* SOGSErrorSpec.swift in Sources */, + FD6B92D82E77B55D004463B5 /* PersonalizationSpec.swift in Sources */, FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */, FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */, FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */, @@ -7016,6 +7174,8 @@ FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */, FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */, FD0150492CA243CB005B08A1 /* Mock.swift in Sources */, + FD6B92CE2E77B234004463B5 /* RoomPollInfoSpec.swift in Sources */, + FD6B92CF2E77B234004463B5 /* RoomSpec.swift in Sources */, FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */, FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, @@ -7027,21 +7187,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */, - FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift in Sources */, FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */, - FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, - FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, - FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */, FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */, FD981BC92DC4641100564172 /* ExtensionHelperSpec.swift in Sources */, FD981BCB2DC4A21C00564172 /* MessageDeduplicationSpec.swift in Sources */, - FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, + FD83B9C727CF3F10005E1583 /* CapabilitySpec.swift in Sources */, FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */, FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */, - FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FDB11A562DD17C3300BEF49F /* MockLogger.swift in Sources */, FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */, @@ -7065,26 +7220,21 @@ FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */, - FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */, FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */, - FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */, FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, - FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */, FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */, - FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */, FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */, FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */, FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, - FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FD01503B2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD3F2EE72DE6CC4100FD6849 /* NotificationsManagerSpec.swift in Sources */, FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */, diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 8309d70282..b7fe384e4d 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -534,7 +534,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.some(let inviteByIdValue), _): // This could be an ONS name let viewController = ModalActivityIndicatorViewController() { modalActivityIndicator in - SnodeAPI + Network.SnodeAPI .getSessionID(for: inviteByIdValue, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .receive(on: DispatchQueue.main, using: dependencies) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 814a8591e1..70558f64e7 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1746,7 +1746,7 @@ extension ConversationVC: let openGroupServerMessageId: Int64 = cellViewModel.openGroupServerMessageId else { return } - let pendingChange: OpenGroupAPI.PendingChange = viewModel.dependencies[singleton: .openGroupManager] + let pendingChange: OpenGroupManager.PendingChange = viewModel.dependencies[singleton: .openGroupManager] .addPendingReaction( emoji: emoji, id: openGroupServerMessageId, @@ -1756,7 +1756,7 @@ extension ConversationVC: ) Result { - try OpenGroupAPI.preparedReactionDeleteAll( + try Network.SOGS.preparedReactionDeleteAll( emoji: emoji, id: openGroupServerMessageId, roomToken: roomToken, @@ -1831,14 +1831,14 @@ extension ConversationVC: typealias OpenGroupInfo = ( pendingReaction: Reaction?, - pendingChange: OpenGroupAPI.PendingChange, + pendingChange: OpenGroupManager.PendingChange, preparedRequest: Network.PreparedRequest ) /// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup /// cache from blocking either the main thread or the database write thread Deferred { [dependencies = viewModel.dependencies] in - Future { resolver in + Future { resolver in guard threadVariant == .community, let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId, @@ -1859,7 +1859,7 @@ extension ConversationVC: } } .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: viewModel.dependencies) - .flatMapStorageWritePublisher(using: viewModel.dependencies) { [weak self, dependencies = viewModel.dependencies] db, pendingChange -> (OpenGroupAPI.PendingChange?, Reaction?, Message.Destination, AuthenticationMethod) in + .flatMapStorageWritePublisher(using: viewModel.dependencies) { [weak self, dependencies = viewModel.dependencies] db, pendingChange -> (OpenGroupManager.PendingChange?, Reaction?, Message.Destination, AuthenticationMethod) in // Update the thread to be visible (if it isn't already) if self?.viewModel.threadData.threadShouldBeVisible == false { try SessionThread.updateVisibility( @@ -1937,12 +1937,12 @@ extension ConversationVC: let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId, let openGroupServer: String = cellViewModel.threadOpenGroupServer, let openGroupRoom: String = openGroupRoom, - let pendingChange: OpenGroupAPI.PendingChange = pendingChange + let pendingChange: OpenGroupManager.PendingChange = pendingChange else { throw MessageSenderError.invalidMessage } let preparedRequest: Network.PreparedRequest = try { guard !remove else { - return try OpenGroupAPI + return try Network.SOGS .preparedReactionDelete( emoji: emoji, id: serverMessageId, @@ -1953,7 +1953,7 @@ extension ConversationVC: .map { _, response in response.seqNo } } - return try OpenGroupAPI + return try Network.SOGS .preparedReactionAdd( emoji: emoji, id: serverMessageId, @@ -2579,7 +2579,7 @@ extension ConversationVC: } .publisher .tryFlatMap { (roomToken: String, authMethod: AuthenticationMethod) in - try OpenGroupAPI.preparedUserBan( + try Network.SOGS.preparedUserBan( sessionId: cellViewModel.authorId, from: [roomToken], authMethod: authMethod, @@ -2657,7 +2657,7 @@ extension ConversationVC: } .publisher .tryFlatMap { (roomToken: String, authMethod: AuthenticationMethod) in - try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( + try Network.SOGS.preparedUserBanAndDeleteAllMessages( sessionId: cellViewModel.authorId, roomToken: roomToken, authMethod: authMethod, diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index 725fa06ed8..4eb0dbfa9f 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -87,7 +87,7 @@ struct NewMessageScreen: View { // This could be an ONS name ModalActivityIndicatorViewController .present(fromViewController: self.host.controller?.navigationController!, canCancel: false) { modalActivityIndicator in - SnodeAPI + Network.SnodeAPI .getSessionID(for: accountIdOrONS, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index f4c447dd46..9456f8dc42 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -183,7 +183,7 @@ struct MessageInfoScreen: View { spacing: Values.mediumSpacing ) { InfoBlock(title: "attachmentsFileId".localized()) { - Text(attachment.downloadUrl.map { Attachment.fileId(for: $0) } ?? "") + Text(attachment.downloadUrl.map { Network.FileServer.fileId(for: $0) } ?? "") .font(.system(size: Values.mediumFontSize)) .foregroundColor(themeColor: .textPrimary) } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index cb0c65534e..e70361c687 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -704,7 +704,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self?.startPollersIfNeeded() - SessionNetworkAPI.client.initialize(using: dependencies) + Network.SessionNetwork.client.initialize(using: dependencies) if dependencies[singleton: .appContext].isMainApp { DispatchQueue.main.async { diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 7cf9efb5a4..5d32a61072 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -82,7 +82,7 @@ public enum SyncPushTokensJob: JobExecutor { // Unregister from our server if let existingToken: String = lastRecordedPushToken { Log.info(.syncPushTokensJob, "Unregister using last recorded push token: \(redact(existingToken))") - return PushNotificationAPI + return Network.PushNotification .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) .map { _ in () } .eraseToAnyPublisher() @@ -177,7 +177,7 @@ public enum SyncPushTokensJob: JobExecutor { } Log.info(.syncPushTokensJob, "Sending push token to PN server") - return PushNotificationAPI + return Network.PushNotification .subscribeAll( token: Data(hex: pushToken), isForcedUpdate: true, diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 5b3f4e4177..0c5fa3c2cc 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -6,6 +6,7 @@ import AVFoundation import GRDB import SessionUIKit import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -485,11 +486,11 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O ) } - func join(_ room: OpenGroupAPI.Room) { + func join(_ room: Network.SOGS.Room) { joinOpenGroupVC?.joinOpenGroup( roomToken: room.token, - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, shouldOpenCommunity: true, onError: nil ) diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 28bbe31ee6..ddc4c72ba9 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Combine import NVActivityIndicatorView +import SessionNetworkingKit import SessionMessagingKit import SessionUIKit import SessionUtilitiesKit @@ -354,7 +355,7 @@ extension OpenGroupSuggestionGrid { snContentView.pin(to: self) } - fileprivate func update(with room: OpenGroupAPI.Room, openGroup: OpenGroup, using dependencies: Dependencies) { + fileprivate func update(with room: Network.SOGS.Room, openGroup: OpenGroup, using dependencies: Dependencies) { label.text = room.name let maybePath: String? = openGroup.displayPictureOriginalUrl @@ -380,7 +381,7 @@ extension OpenGroupSuggestionGrid { // MARK: - Delegate protocol OpenGroupSuggestionGridDelegate { - func join(_ room: OpenGroupAPI.Room) + func join(_ room: Network.SOGS.Room) } // MARK: - LastRowCenteredLayout diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 263ae04ace..78ea7b150c 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -239,7 +239,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let serviceNetwork: ServiceNetwork let forceOffline: Bool - let pushNotificationService: PushNotificationAPI.Service + let pushNotificationService: Network.PushNotification.Service let debugDisappearingMessageDurations: Bool @@ -594,9 +594,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onTap: { [weak self, dependencies] in self?.transitionToScreen( SessionTableViewController( - viewModel: SessionListViewModel( + viewModel: SessionListViewModel( title: "Push Notification Service", - options: PushNotificationAPI.Service.allCases, + options: Network.PushNotification.Service.allCases, behaviour: .autoDismiss( initialSelection: current.pushNotificationService, onOptionSelected: self?.updatePushNotificationService @@ -1181,7 +1181,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } - private func updatePushNotificationService(to updatedService: PushNotificationAPI.Service?) { + private func updatePushNotificationService(to updatedService: Network.PushNotification.Service?) { guard dependencies[defaults: .standard, key: .isUsingFullAPNs], updatedService != dependencies[feature: .pushNotificationService] @@ -1270,7 +1270,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service /// layer and we don't want these to be cancelled) if let existingToken: String = dependencies[singleton: .storage].read({ db in db[.lastRecordedPushToken] }) { - PushNotificationAPI + Network.PushNotification .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) .sinkUntilComplete() } @@ -2104,9 +2104,9 @@ final class PollLimitInputView: UIView, UITextFieldDelegate, SessionCell.Accesso extension ServiceNetwork: @retroactive ContentIdentifiable {} extension ServiceNetwork: @retroactive ContentEquatable {} extension ServiceNetwork: Listable {} -extension PushNotificationAPI.Service: @retroactive ContentIdentifiable {} -extension PushNotificationAPI.Service: @retroactive ContentEquatable {} -extension PushNotificationAPI.Service: Listable {} +extension Network.PushNotification.Service: @retroactive ContentIdentifiable {} +extension Network.PushNotification.Service: @retroactive ContentEquatable {} +extension Network.PushNotification.Service: Listable {} extension Log.Level: @retroactive ContentIdentifiable {} extension Log.Level: @retroactive ContentEquatable {} extension Log.Level: Listable {} diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index d4162bb02c..ffc90bcd3d 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -201,7 +201,7 @@ final class NukeDataModal: Modal { try communityAuth.compactMap { authMethod in switch authMethod.info { case .community(let server, _, _, _, _): - return try OpenGroupAPI.preparedClearInbox( + return try Network.SOGS.preparedClearInbox( requestAndPathBuildTimeout: Network.defaultTimeout, authMethod: authMethod, using: dependencies @@ -218,7 +218,7 @@ final class NukeDataModal: Modal { .eraseToAnyPublisher() } .tryFlatMap { authMethod, clearedServers in - try SnodeAPI + try Network.SnodeAPI .preparedDeleteAllMessages( namespace: .all, requestAndPathBuildTimeout: Network.defaultTimeout, @@ -296,7 +296,7 @@ final class NukeDataModal: Modal { UIApplication.shared.unregisterForRemoteNotifications() if let deviceToken: String = maybeDeviceToken, dependencies[singleton: .storage].isValid { - PushNotificationAPI + Network.PushNotification .unsubscribeAll(token: Data(hex: deviceToken), using: dependencies) .sinkUntilComplete() } diff --git a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift index d7795ea74f..dfefd70e69 100644 --- a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift +++ b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift @@ -78,8 +78,8 @@ extension SessionNetworkScreenContent { self.isRefreshing.toggle() self.lastRefreshWasSuccessful = false - SessionNetworkAPI.client.getInfo(using: dependencies) - .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) + Network.SessionNetwork.client.getInfo(using: dependencies) + .subscribe(on: Network.SessionNetwork.workQueue, using: dependencies) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { _ in }, diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index 8621e3a2fa..ff92a72277 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -164,38 +164,6 @@ public extension Crypto.Generator { } } - static func plaintextWithPushNotificationPayload( - payload: Data, - encKey: Data - ) -> Crypto.Generator { - return Crypto.Generator( - id: "plaintextWithPushNotificationPayload", - args: [payload, encKey] - ) { - var cPayload: [UInt8] = Array(payload) - var cEncKey: [UInt8] = Array(encKey) - var maybePlaintext: UnsafeMutablePointer? = nil - var plaintextLen: Int = 0 - - guard - cEncKey.count == 32, - session_decrypt_push_notification( - &cPayload, - cPayload.count, - &cEncKey, - &maybePlaintext, - &plaintextLen - ), - plaintextLen > 0, - let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) - else { throw MessageReceiverError.decryptionFailed } - - free(UnsafeMutableRawPointer(mutating: maybePlaintext)) - - return plaintext - } - } - static func plaintextWithMultiEncrypt( ciphertext: Data, senderSessionId: SessionId, @@ -231,7 +199,7 @@ public extension Crypto.Generator { static func messageServerHash( swarmPubkey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, data: Data ) -> Crypto.Generator { return Crypto.Generator( diff --git a/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift index e77ead364d..91746c8cef 100644 --- a/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift +++ b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift @@ -91,10 +91,10 @@ enum _023_SplitSnodeReceivedMessageInfo: Migration { let targetNamespace: Int = { guard swarmPublicKeySplitComponents.count == 2 else { - return SnodeAPI.Namespace.default.rawValue + return Network.SnodeAPI.Namespace.default.rawValue } - return (Int(swarmPublicKeySplitComponents[1]) ?? SnodeAPI.Namespace.default.rawValue) + return (Int(swarmPublicKeySplitComponents[1]) ?? Network.SnodeAPI.Namespace.default.rawValue) }() let wasDeletedOrInvalid: Bool? = info["wasDeletedOrInvalid"] diff --git a/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift index 2fad3edb4e..60052a5eda 100644 --- a/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift +++ b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift @@ -15,7 +15,7 @@ enum _024_ResetUserConfigLastHashes: Migration { static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws { try db.execute(literal: """ DELETE FROM snodeReceivedMessageInfo - WHERE namespace IN (\(SnodeAPI.Namespace.configContacts.rawValue), \(SnodeAPI.Namespace.configUserProfile.rawValue), \(SnodeAPI.Namespace.configUserGroups.rawValue), \(SnodeAPI.Namespace.configConvoInfoVolatile.rawValue)) + WHERE namespace IN (\(Network.SnodeAPI.Namespace.configContacts.rawValue), \(Network.SnodeAPI.Namespace.configUserProfile.rawValue), \(Network.SnodeAPI.Namespace.configUserGroups.rawValue), \(Network.SnodeAPI.Namespace.configConvoInfoVolatile.rawValue)) """) MigrationExecution.updateProgress(1) diff --git a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index 87081585e3..55cb03346f 100644 --- a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -144,19 +144,24 @@ enum _036_GroupsRebuildChanges: Migration { /// If the group isn't in the invited state then make sure to subscribe for PNs once the migrations are done if !group.invited, let token: String = dependencies[defaults: .standard, key: .deviceToken] { - db.afterCommit { - dependencies[singleton: .storage] - .readPublisher { db in - try PushNotificationAPI.preparedSubscribe( - db, + let maybeAuthMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: group.groupSessionId, + using: dependencies + ) + + if let authMethod: AuthenticationMethod = maybeAuthMethod { + db.afterCommit { + try? Network.PushNotification + .preparedSubscribe( token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.groupSessionId)], + swarms: [(SessionId(.group, hex: group.groupSessionId), authMethod)], using: dependencies ) - } - .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + } } } } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 9419f7b1d9..072306a4d3 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -489,7 +489,7 @@ extension Attachment { /// **Note:** We need to continue to send this because it seems that the Desktop client _does_ in fact still use this /// id for downloading attachments. Desktop will be updated to remove it's use but in order to fix attachments for old /// versions we set this value again - let legacyId: UInt64 = (Attachment.fileId(for: self.downloadUrl).map { UInt64($0) } ?? 0) + let legacyId: UInt64 = (Network.FileServer.fileId(for: self.downloadUrl).map { UInt64($0) } ?? 0) let builder = SNProtoAttachmentPointer.builder(id: legacyId) builder.setContentType(contentType) @@ -689,14 +689,4 @@ extension Attachment { return true } - - public static func fileId(for downloadUrl: String?) -> String? { - return downloadUrl - .map { urlString -> String? in - urlString - .split(separator: "/") // stringlint:ignore - .last - .map { String($0) } - } - } } diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index f4a9b5cb6a..da2655eb59 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -226,16 +226,22 @@ public extension ClosedGroup { /// Subscribe for group push notifications if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI - .preparedSubscribe( - db, - token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.id)], - using: dependencies - ) - .send(using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + let maybeAuthMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: group.id, + using: dependencies + ) + + if let authMethod: AuthenticationMethod = maybeAuthMethod { + try? Network.PushNotification + .preparedSubscribe( + token: Data(hex: token), + swarms: [(SessionId(.group, hex: group.id), authMethod)], + using: dependencies + ) + .send(using: dependencies) + .sinkUntilComplete() + } } } @@ -305,13 +311,20 @@ public extension ClosedGroup { /// Bulk unsubscripe from updated groups being removed if dataToRemove.contains(.pushNotifications) && threadVariants.contains(where: { $0.variant == .group }) { if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI + try? Network.PushNotification .preparedUnsubscribe( - db, token: Data(hex: token), - sessionIds: threadVariants + swarms: threadVariants .filter { $0.variant == .group } - .map { SessionId(.group, hex: $0.id) }, + .compactMap { info in + let authMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: info.id, + using: dependencies + ) + + return authMethod.map { (SessionId(.group, hex: info.id), $0) } + }, using: dependencies ) .send(using: dependencies) diff --git a/SessionMessagingKit/Database/Models/ConfigDump.swift b/SessionMessagingKit/Database/Models/ConfigDump.swift index 0b429793ad..eed9528aa9 100644 --- a/SessionMessagingKit/Database/Models/ConfigDump.swift +++ b/SessionMessagingKit/Database/Models/ConfigDump.swift @@ -86,7 +86,7 @@ public extension ConfigDump.Variant { .groupInfo, .groupMembers, .groupKeys ] - init(namespace: SnodeAPI.Namespace) { + init(namespace: Network.SnodeAPI.Namespace) { switch namespace { case .configUserProfile: self = .userProfile case .configContacts: self = .contacts @@ -104,19 +104,19 @@ public extension ConfigDump.Variant { /// Config messages should last for 30 days rather than the standard 14 var ttl: UInt64 { 30 * 24 * 60 * 60 * 1000 } - var namespace: SnodeAPI.Namespace { + var namespace: Network.SnodeAPI.Namespace { switch self { - case .userProfile: return SnodeAPI.Namespace.configUserProfile - case .contacts: return SnodeAPI.Namespace.configContacts - case .convoInfoVolatile: return SnodeAPI.Namespace.configConvoInfoVolatile - case .userGroups: return SnodeAPI.Namespace.configUserGroups - case .local: return SnodeAPI.Namespace.configLocal + case .userProfile: return Network.SnodeAPI.Namespace.configUserProfile + case .contacts: return Network.SnodeAPI.Namespace.configContacts + case .convoInfoVolatile: return Network.SnodeAPI.Namespace.configConvoInfoVolatile + case .userGroups: return Network.SnodeAPI.Namespace.configUserGroups + case .local: return Network.SnodeAPI.Namespace.configLocal - case .groupInfo: return SnodeAPI.Namespace.configGroupInfo - case .groupMembers: return SnodeAPI.Namespace.configGroupMembers - case .groupKeys: return SnodeAPI.Namespace.configGroupKeys + case .groupInfo: return Network.SnodeAPI.Namespace.configGroupInfo + case .groupMembers: return Network.SnodeAPI.Namespace.configGroupMembers + case .groupKeys: return Network.SnodeAPI.Namespace.configGroupKeys - case .invalid: return SnodeAPI.Namespace.unknown + case .invalid: return Network.SnodeAPI.Namespace.unknown } } diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index f6ac676fea..977887c1a1 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -4,6 +4,7 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit public struct OpenGroup: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { @@ -40,7 +41,7 @@ public struct OpenGroup: Codable, Equatable, Hashable, Identifiable, FetchableRe self.rawValue = rawValue } - public init(roomInfo: OpenGroupAPI.RoomPollInfo) { + public init(roomInfo: Network.SOGS.RoomPollInfo) { var permissions: Permissions = [] if roomInfo.read { permissions.insert(.read) } diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index b52dbe95d5..210d629967 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -112,7 +112,7 @@ public enum AttachmentDownloadJob: JobExecutor { switch maybeRoomToken { case .some(let roomToken): - return try OpenGroupAPI + return try Network.SOGS .preparedDownload( url: info.downloadUrl, roomToken: roomToken, diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index f0ba83de12..53cd2a73a5 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -92,13 +92,13 @@ extension ConfigMessageReceiveJob { case data } - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace public let serverHash: String public let serverTimestampMs: Int64 public let data: Data public init( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index 136b736815..f1266f72a1 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -96,14 +96,14 @@ public enum ConfigurationSyncJob: JobExecutor { try Authentication.with(db, swarmPublicKey: swarmPublicKey, using: dependencies) } .tryFlatMap { authMethod -> AnyPublisher<(ResponseInfoType, Network.BatchResponse), Error> in - try SnodeAPI.preparedSequence( + try Network.SnodeAPI.preparedSequence( requests: [] .appending(contentsOf: additionalTransientData?.beforeSequenceRequests) .appending( contentsOf: try pendingPushes.pushData .flatMap { pushData -> [ErasedPreparedRequest] in try pushData.data.map { data -> ErasedPreparedRequest in - try SnodeAPI + try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: swarmPublicKey, @@ -121,7 +121,7 @@ public enum ConfigurationSyncJob: JobExecutor { .appending(try { guard !pendingPushes.obsoleteHashes.isEmpty else { return nil } - return try SnodeAPI.preparedDeleteMessages( + return try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(pendingPushes.obsoleteHashes), requireSuccessfulDeletion: false, authMethod: authMethod, diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index c5257be82b..dac761d872 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -39,7 +39,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { switch details.target { case .profile(_, let url, _), .group(_, let url, _): guard - let fileId: String = Attachment.fileId(for: url), + let fileId: String = Network.FileServer.fileId(for: url), let downloadUrl: URL = URL(string: Network.FileServer.downloadUrlString(for: url, fileId: fileId)) else { throw NetworkError.invalidURL } @@ -54,7 +54,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { .fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) else { throw JobRunnerError.missingRequiredDetails } - return try OpenGroupAPI.preparedDownload( + return try Network.SOGS.preparedDownload( fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), @@ -235,7 +235,7 @@ extension DisplayPictureDownloadJob { case .profile(_, let url, let encryptionKey), .group(_, let url, let encryptionKey): return ( !url.isEmpty && - Attachment.fileId(for: url) != nil && + Network.FileServer.fileId(for: url) != nil && encryptionKey.count == DisplayPictureManager.aes256KeyByteLength ) diff --git a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift index 03e602be04..ef3cdb9d7a 100644 --- a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift @@ -26,7 +26,7 @@ public enum ExpirationUpdateJob: JobExecutor { dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI + try Network.SnodeAPI .preparedUpdateExpiry( serverHashes: details.serverHashes, updatedExpiryMs: details.expirationTimestampMs, diff --git a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift index 907d245582..aacaa48e5c 100644 --- a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift @@ -182,7 +182,7 @@ public enum GarbageCollectionJob: JobExecutor { LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) WHERE ( \(thread[.id]) IS NULL AND - \(SQL("\(openGroup[.server]) != \(OpenGroupAPI.defaultServer.lowercased())")) + \(SQL("\(openGroup[.server]) != \(Network.SOGS.defaultServer.lowercased())")) ) ) """) diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index 477268c35f..00be1730b7 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -39,7 +39,7 @@ public enum GetExpirationJob: JobExecutor { dependencies[singleton: .storage] .readPublisher { db -> Network.PreparedRequest in - try SnodeAPI.preparedGetExpiries( + try Network.SnodeAPI.preparedGetExpiries( of: expirationInfo.map { $0.key }, authMethod: try Authentication.with( db, diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 92448ad51b..208072da67 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -92,7 +92,7 @@ public enum GroupLeavingJob: JobExecutor { .tryFlatMap { requestType -> AnyPublisher in switch requestType { case .sendLeaveMessage(let authMethod, let disappearingConfig): - return try SnodeAPI + return try Network.SnodeAPI .preparedBatch( requests: [ /// Don't expire the `GroupUpdateMemberLeftMessage` as that's not a UI-based diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 49948e7bac..bbb2b1c7fe 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -352,7 +352,7 @@ public extension MessageSendJob { .compactMap { info in guard let attachment: Attachment = attachments[info.attachmentId], - let fileId: String = Attachment.fileId(for: info.downloadUrl) + let fileId: String = Network.FileServer.fileId(for: info.downloadUrl) else { return nil } return (attachment, fileId) diff --git a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift index 096d6729ff..0253242bc7 100644 --- a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift +++ b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift @@ -97,7 +97,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { .tryMap { _ -> Network.PreparedRequest in /// Revoke the members authData from the group so the server rejects API calls from the ex-members (fire-and-forget /// this request, we don't want it to be blocking) - let preparedRevokeSubaccounts: Network.PreparedRequest = try SnodeAPI.preparedRevokeSubaccounts( + let preparedRevokeSubaccounts: Network.PreparedRequest = try Network.SnodeAPI.preparedRevokeSubaccounts( subaccountsToRevoke: try dependencies.mutate(cache: .libSession) { cache in try Array(pendingRemovals.keys).map { memberId in try dependencies[singleton: .crypto].tryGenerate( @@ -131,7 +131,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { domain: .kickedMessage ) ) - let preparedGroupDeleteMessage: Network.PreparedRequest = try SnodeAPI + let preparedGroupDeleteMessage: Network.PreparedRequest = try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: groupSessionId.hexString, @@ -179,7 +179,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { }() /// Combine the two requests to be sent at the same time - return try SnodeAPI.preparedSequence( + return try Network.SnodeAPI.preparedSequence( requests: [preparedRevokeSubaccounts, preparedGroupDeleteMessage, preparedMemberContentRemovalMessage] .compactMap { $0 }, requireAllBatchResponses: true, @@ -262,7 +262,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { ) /// Delete the messages from the swarm so users won't download them again - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index 06d2b8e3b9..1dac5e6dd7 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -40,17 +40,17 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .isEmpty else { return deferred(job) } - // The OpenGroupAPI won't make any API calls if there is no entry for an OpenGroup + // The Network.SOGS won't make any API calls if there is no entry for an OpenGroup // in the database so we need to create a dummy one to retrieve the default room data - let defaultGroupId: String = OpenGroup.idFor(roomToken: "", server: OpenGroupAPI.defaultServer) + let defaultGroupId: String = OpenGroup.idFor(roomToken: "", server: Network.SOGS.defaultServer) dependencies[singleton: .storage].write { db in guard try OpenGroup.exists(db, id: defaultGroupId) == false else { return } try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "", userCount: 0, @@ -64,13 +64,13 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .readPublisher { [dependencies] db -> AuthenticationMethod in try Authentication.with( db, - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, activeOnly: false, /// The record for the default rooms is inactive using: dependencies ) } - .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, Network.SOGS.CapabilitiesAndRoomsResponse), Error> in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: authMethod, using: dependencies ).send(using: dependencies) @@ -96,11 +96,11 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { OpenGroupManager.handleCapabilities( db, capabilities: response.capabilities.data, - on: OpenGroupAPI.defaultServer + on: Network.SOGS.defaultServer ) let existingImageIds: [String: String] = try OpenGroup - .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer) + .filter(OpenGroup.Columns.server == Network.SOGS.defaultServer) .filter(OpenGroup.Columns.imageId != nil) .fetchAll(db) .reduce(into: [:]) { result, next in result[next.id] = next.imageId } @@ -112,9 +112,9 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { return ( room, try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: room.token, - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: room.name, roomDescription: room.roomDescription, @@ -131,7 +131,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { db, id: OpenGroup.idFor( roomToken: room.token, - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer ) ) .map { (room, $0) } @@ -140,7 +140,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { /// Schedule the room image download (if it doesn't match out current one) result.forEach { room, openGroup in - let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer) + let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: Network.SOGS.defaultServer) guard let imageId: String = room.imageId, @@ -157,7 +157,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { target: .community( imageId: imageId, roomToken: room.token, - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer ), timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index 5abc9e32d3..82cdc5060c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -293,7 +293,7 @@ internal extension LibSessionCacheType { swarmPublicKey: groupSessionId.hexString, using: dependencies )).map { authMethod in - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(messageHashesToDelete), requireSuccessfulDeletion: false, diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift index 7d503d57f0..7eb71d798f 100644 --- a/SessionMessagingKit/LibSession/Types/Config.swift +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -324,7 +324,7 @@ public extension LibSession { .sorted() if successfulMergeTimestamps.count != messages.count { - Log.warn(.libSession, "Unable to merge \(SnodeAPI.Namespace.configGroupKeys) messages (\(successfulMergeTimestamps.count)/\(messages.count))") + Log.warn(.libSession, "Unable to merge \(Network.SnodeAPI.Namespace.configGroupKeys) messages (\(successfulMergeTimestamps.count)/\(messages.count))") } return successfulMergeTimestamps.last diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index e6e7a98934..a61a43ec3d 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -39,7 +39,7 @@ public extension Message { } } - public var defaultNamespace: SnodeAPI.Namespace? { + public var defaultNamespace: Network.SnodeAPI.Namespace? { switch self { case .contact, .syncMessage: return .`default` case .closedGroup(let groupId) where (try? SessionId.Prefix(from: groupId)) == .group: @@ -61,7 +61,7 @@ public extension Message { if prefix == .blinded15 || prefix == .blinded25 { guard let lookup: BlindedIdLookup = try? BlindedIdLookup.fetchOne(db, id: threadId) else { - throw OpenGroupAPIError.blindedLookupMissingCommunityInfo + throw SOGSError.blindedLookupMissingCommunityInfo } return .openGroupInbox( diff --git a/SessionMessagingKit/Messages/Message+Origin.swift b/SessionMessagingKit/Messages/Message+Origin.swift index e64ebd18bb..1c0d5f330a 100644 --- a/SessionMessagingKit/Messages/Message+Origin.swift +++ b/SessionMessagingKit/Messages/Message+Origin.swift @@ -8,7 +8,7 @@ public extension Message { enum Origin: Codable, Hashable { case swarm( publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, serverExpirationTimestamp: TimeInterval diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 1f775614c2..2583d84d7c 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -83,7 +83,7 @@ public class Message: Codable { case (false, .some(let sigTimestampMs), .some): let delta: TimeInterval = (TimeInterval(max(sigTimestampMs, sentTimestampMs) - min(sigTimestampMs, sentTimestampMs)) / 1000) - return delta < OpenGroupAPI.validTimestampVarianceThreshold + return delta < Network.SOGS.validTimestampVarianceThreshold // FIXME: We want to remove support for this case in a future release case (_, .none, _): return true @@ -174,7 +174,7 @@ public enum ProcessedMessage { ) case config( publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data, @@ -190,7 +190,7 @@ public enum ProcessedMessage { } } - var namespace: SnodeAPI.Namespace { + var namespace: Network.SnodeAPI.Namespace { switch self { case .standard(_, let threadVariant, _, _, _): switch threadVariant { @@ -432,12 +432,12 @@ public extension Message { static func processRawReceivedReactions( _ db: ObservingDatabase, openGroupId: String, - message: OpenGroupAPI.Message, - associatedPendingChanges: [OpenGroupAPI.PendingChange], + message: Network.SOGS.Message, + associatedPendingChanges: [OpenGroupManager.PendingChange], using dependencies: Dependencies ) -> [Reaction] { guard - let reactions: [String: OpenGroupAPI.Message.Reaction] = message.reactions, + let reactions: [String: Network.SOGS.Message.Reaction] = message.reactions, let openGroupCapabilityInfo: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: openGroupId) else { return [] } @@ -483,7 +483,7 @@ public extension Message { let pendingChangeSelfReaction: Bool? = { // Find the newest 'PendingChange' entry with a matching emoji, if one exists, and // set the "self reaction" value based on it's action - let maybePendingChange: OpenGroupAPI.PendingChange? = associatedPendingChanges + let maybePendingChange: OpenGroupManager.PendingChange? = associatedPendingChanges .sorted(by: { lhs, rhs -> Bool in (lhs.seqNo ?? Int64.max) >= (rhs.seqNo ?? Int64.max) }) .first { pendingChange in if case .reaction(_, let emoji, _) = pendingChange.metadata { @@ -495,7 +495,7 @@ public extension Message { // If there is no pending change for this reaction then return nil guard - let pendingChange: OpenGroupAPI.PendingChange = maybePendingChange, + let pendingChange: OpenGroupManager.PendingChange = maybePendingChange, case .reaction(_, _, let action) = pendingChange.metadata else { return nil } diff --git a/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift b/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift index 9f476ce00e..2ff8f76fc6 100644 --- a/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift @@ -3,96 +3,9 @@ // stringlint:disable import Foundation -import CryptoKit import SessionUtil import SessionUtilitiesKit -public extension Crypto.Generator { - /// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` - static func blinded15KeyPair( - serverPublicKey: String, - ed25519SecretKey: [UInt8] - ) -> Crypto.Generator { - return Crypto.Generator( - id: "blinded15KeyPair", - args: [serverPublicKey, ed25519SecretKey] - ) { - var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) - var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) - var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) - var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 32) - - guard - cEd25519SecretKey.count == 64, - cServerPublicKey.count == 32, - session_blind15_key_pair( - &cEd25519SecretKey, - &cServerPublicKey, - &cBlindedPubkey, - &cBlindedSeckey - ) - else { throw CryptoError.keyGenerationFailed } - - return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) - } - } - - /// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` - static func blinded25KeyPair( - serverPublicKey: String, - ed25519SecretKey: [UInt8] - ) -> Crypto.Generator { - return Crypto.Generator( - id: "blinded25KeyPair", - args: [serverPublicKey, ed25519SecretKey] - ) { - var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) - var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) - var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) - var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 32) - - guard - cEd25519SecretKey.count == 64, - cServerPublicKey.count == 32, - session_blind25_key_pair( - &cEd25519SecretKey, - &cServerPublicKey, - &cBlindedPubkey, - &cBlindedSeckey - ) - else { throw CryptoError.keyGenerationFailed } - - return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) - } - } -} - -public extension Crypto.Verification { - /// This method should be used to check if a users standard sessionId matches a blinded one - static func sessionId( - _ standardSessionId: String, - matchesBlindedId blindedSessionId: String, - serverPublicKey: String - ) -> Crypto.Verification { - return Crypto.Verification( - id: "sessionId", - args: [standardSessionId, blindedSessionId, serverPublicKey] - ) { - guard - var cStandardSessionId: [CChar] = standardSessionId.cString(using: .utf8), - var cBlindedSessionId: [CChar] = blindedSessionId.cString(using: .utf8), - var cServerPublicKey: [CChar] = serverPublicKey.cString(using: .utf8) - else { return false } - - return session_id_matches_blinded_id( - &cStandardSessionId, - &cBlindedSessionId, - &cServerPublicKey - ) - } - } -} - // MARK: - Messages public extension Crypto.Generator { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift deleted file mode 100644 index efe47a4c8c..0000000000 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ /dev/null @@ -1,1396 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SessionNetworkingKit -import SessionUtilitiesKit - -public enum OpenGroupAPI { - public struct RoomInfo: Codable { - let roomToken: String - let infoUpdates: Int64 - let sequenceNumber: Int64 - } - - // MARK: - Settings - - public static let legacyDefaultServerIP = "116.203.70.33" - public static let defaultServer = "https://open.getsession.org" - public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - public static let validTimestampVarianceThreshold: TimeInterval = (6 * 60 * 60) - - public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue - - // MARK: - Batching & Polling - - /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open - /// Group, currently this will retrieve: - /// - Capabilities for the server - /// - For each room: - /// - Poll Info - /// - Messages (includes additions and deletions) - /// - Inbox for the server - /// - Outbox for the server - public static func preparedPoll( - roomInfo: [RoomInfo], - lastInboxMessageId: Int64, - lastOutboxMessageId: Int64, - hasPerformedInitialPoll: Bool, - timeSinceLastPoll: TimeInterval, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest> { - guard case .community(_, _, _, let supportsBlinding, _) = authMethod.info else { - throw NetworkError.invalidPreparedRequest - } - - let preparedRequests: [any ErasedPreparedRequest] = [ - try preparedCapabilities( - authMethod: authMethod, - using: dependencies - ) - ].appending( - // Per-room requests - contentsOf: try roomInfo - .flatMap { roomInfo -> [any ErasedPreparedRequest] in - let shouldRetrieveRecentMessages: Bool = ( - roomInfo.sequenceNumber == 0 || ( - // If it's the first poll for this launch and it's been longer than - // 'maxInactivityPeriod' then just retrieve recent messages instead - // of trying to get all messages since the last one retrieved - !hasPerformedInitialPoll && - timeSinceLastPoll > CommunityPoller.maxInactivityPeriod - ) - ) - - return [ - try preparedRoomPollInfo( - lastUpdated: roomInfo.infoUpdates, - roomToken: roomInfo.roomToken, - authMethod: authMethod, - using: dependencies - ), - (shouldRetrieveRecentMessages ? - try preparedRecentMessages( - roomToken: roomInfo.roomToken, - authMethod: authMethod, - using: dependencies - ) : - try preparedMessagesSince( - seqNo: roomInfo.sequenceNumber, - roomToken: roomInfo.roomToken, - authMethod: authMethod, - using: dependencies - ) - ) - ] - } - ) - .appending( - contentsOf: ( - // The 'inbox' and 'outbox' only work with blinded keys so don't bother polling them if not blinded - !supportsBlinding ? [] : - [ - // Inbox (only check the inbox if the user want's community message requests) - (!dependencies.mutate(cache: .libSession) { $0.get(.checkForCommunityMessageRequests) } ? nil : - (lastInboxMessageId == 0 ? - try preparedInbox(authMethod: authMethod, using: dependencies) : - try preparedInboxSince( - id: lastInboxMessageId, - authMethod: authMethod, - using: dependencies - ) - ) - ), - - // Outbox - (lastOutboxMessageId == 0 ? - try preparedOutbox(authMethod: authMethod, using: dependencies) : - try preparedOutboxSince( - id: lastOutboxMessageId, - authMethod: authMethod, - using: dependencies - ) - ), - ].compactMap { $0 } - ) - ) - - return try OpenGroupAPI - .preparedBatch( - requests: preparedRequests, - authMethod: authMethod, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one - /// - /// Requests are performed independently, that is, if one fails the others will still be attempted - there is no guarantee on the order in which - /// requests will be carried out (for sequential, related requests invoke via `/sequence` instead) - /// - /// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided - /// with the request body. - public static func preparedBatch( - requests: [any ErasedPreparedRequest], - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest> { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.batch, - body: Network.BatchRequest(requests: requests), - authMethod: authMethod - ), - responseType: Network.BatchResponseMap.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests - /// if the previous request returned a non-`2xx` response - /// - /// For example, this can be used to ban and delete all of a user's messages by sequencing the ban followed by the `delete_all`: if the - /// ban fails (e.g. because permission is denied) then the `delete_all` will not occur. The batch body and response are identical to the - /// `/batch` endpoint; requests that are not carried out because of an earlier failure will have a response code of `412` (Precondition Failed)." - /// - /// Like `/batch`, responses are returned in the same order as requests, but unlike `/batch` there may be fewer elements in the response - /// list (if requests were stopped because of a non-2xx response) - In such a case, the final, non-2xx response is still included as the final - /// response value - private static func preparedSequence( - requests: [any ErasedPreparedRequest], - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest> { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.sequence, - body: Network.BatchRequest(requests: requests), - authMethod: authMethod - ), - responseType: Network.BatchResponseMap.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Capabilities - - /// Return the list of server features/capabilities - /// - /// Optionally takes a `required` parameter containing a comma-separated list of capabilites; if any are not satisfied a 412 (Precondition Failed) - /// response will be returned with missing requested capabilities in the `missing` key - /// - /// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch` - /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` - public static func preparedCapabilities( - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - endpoint: .capabilities, - authMethod: authMethod - ), - responseType: Capabilities.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Room - - /// Returns a list of available rooms on the server - /// - /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included - public static func preparedRooms( - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[Room]> { - return try Network.PreparedRequest( - request: Request( - endpoint: .rooms, - authMethod: authMethod - ), - responseType: [Room].self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Returns the details of a single room - public static func preparedRoom( - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - endpoint: .room(roomToken), - authMethod: authMethod - ), - responseType: Room.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Polls a room for metadata updates - /// - /// The endpoint polls room metadata for this room, always including the instantaneous room details (such as the user's permission and current - /// number of active users), and including the full room metadata if the room's info_updated counter has changed from the provided value - public static func preparedRoomPollInfo( - lastUpdated: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomPollInfo(roomToken, lastUpdated), - authMethod: authMethod - ), - responseType: RoomPollInfo.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - public typealias CapabilitiesAndRoomResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), - room: (info: ResponseInfoType, data: Room) - ) - - /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `room` requests, refer to those - /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRoom( - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try OpenGroupAPI - .preparedSequence( - requests: [ - // Get the latest capabilities for the server (in case it's a new server or the - // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies), - preparedRoom(roomToken: roomToken, authMethod: authMethod, using: dependencies) - ], - authMethod: authMethod, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) - let maybeRoomResponse: Any? = response.data - .first(where: { key, _ in - switch key { - case .room: return true - default: return false - } - }) - .map { _, value in value } - let maybeRoom: Network.BatchSubResponse? = (maybeRoomResponse as? Network.BatchSubResponse) - - guard - let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, - let roomInfo: ResponseInfoType = maybeRoom, - let room: Room = maybeRoom?.body - else { throw NetworkError.parsingFailed } - - return ( - capabilities: (info: capabilitiesInfo, data: capabilities), - room: (info: roomInfo, data: room) - ) - } - } - - public typealias CapabilitiesAndRoomsResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), - rooms: (info: ResponseInfoType, data: [Room]) - ) - - /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `rooms` requests, refer to those - /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRooms( - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try OpenGroupAPI - .preparedSequence( - requests: [ - // Get the latest capabilities for the server (in case it's a new server or the - // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies), - preparedRooms(authMethod: authMethod, using: dependencies) - ], - authMethod: authMethod, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) - let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data - .first(where: { key, _ in - switch key { - case .rooms: return true - default: return false - } - }) - .map { _, value in value as? Network.BatchSubResponse<[Room]> } - - guard - let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, - let roomsInfo: ResponseInfoType = maybeRooms, - let roomsResponse: Network.BatchSubResponse<[Room]> = maybeRooms, - !roomsResponse.failedToParseBody - else { throw NetworkError.parsingFailed } - - // We might want to remove all default rooms for some reason so support that case - return ( - capabilities: (info: capabilitiesInfo, data: capabilities), - rooms: (info: roomsInfo, data: (roomsResponse.body ?? [])) - ) - } - } - - // MARK: - Messages - - /// Posts a new message to a room - public static func preparedSend( - plaintext: Data, - roomToken: String, - whisperTo: String?, - whisperMods: Bool, - fileIds: [String]?, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let signResult: (publicKey: String, signature: [UInt8]) = try sign( - messageBytes: plaintext.bytes, - authMethod: authMethod, - fallbackSigningType: .standard, - using: dependencies - ) - - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.roomMessage(roomToken), - body: SendMessageRequest( - data: plaintext, - signature: Data(signResult.signature), - whisperTo: whisperTo, - whisperMods: whisperMods, - fileIds: fileIds - ), - authMethod: authMethod - ), - responseType: Message.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Returns a single message by ID - public static func preparedMessage( - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomMessageIndividual(roomToken, id: id), - authMethod: authMethod - ), - responseType: Message.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Edits a message, replacing its existing content with new content and a new signature - /// - /// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room - public static func preparedMessageUpdate( - id: Int64, - plaintext: Data, - fileIds: [Int64]?, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let signResult: (publicKey: String, signature: [UInt8]) = try sign( - messageBytes: plaintext.bytes, - authMethod: authMethod, - fallbackSigningType: .standard, - using: dependencies - ) - - return try Network.PreparedRequest( - request: Request( - method: .put, - endpoint: Endpoint.roomMessageIndividual(roomToken, id: id), - body: UpdateMessageRequest( - data: plaintext, - signature: Data(signResult.signature), - fileIds: fileIds - ), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Remove a message by its message id - public static func preparedMessageDelete( - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .delete, - endpoint: .roomMessageIndividual(roomToken, id: id), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Retrieves recent messages posted to this room - /// - /// Returns the most recent limit messages (100 if no limit is given). This only returns extant messages, and always returns the latest - /// versions: that is, deleted message indicators and pre-editing versions of messages are not returned. Messages are returned in order - /// from most recent to least recent - public static func preparedRecentMessages( - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[Failable]> { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomMessagesRecent(roomToken), - queryParameters: [ - .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5", - .limit: "\(dependencies[feature: .communityPollLimit])" - ], - authMethod: authMethod - ), - responseType: [Failable].self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Retrieves messages from the room preceding a given id. - /// - /// This endpoint is intended to be used with .../recent to allow a client to retrieve the most recent messages and then walk backwards - /// through batches of ever-older messages. As with .../recent, messages are returned in order from most recent to least recent. - /// - /// As with .../recent, this endpoint does not include deleted messages and always returns the current version, for edited messages. - public static func preparedMessagesBefore( - messageId: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[Failable]> { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomMessagesBefore(roomToken, id: messageId), - queryParameters: [ - .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5", - .limit: "\(dependencies[feature: .communityPollLimit])" - ], - authMethod: authMethod - ), - responseType: [Failable].self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Retrieves message updates from a room. This is the main message polling endpoint in SOGS. - /// - /// This endpoint retrieves new, edited, and deleted messages or message reactions posted to this room since the given message - /// sequence counter. Returns limit messages at a time (100 if no limit is given). Returned messages include any new messages, updates - /// to existing messages (i.e. edits), and message deletions made to the room since the given update id. Messages are returned in "update" - /// order, that is, in the order in which the change was applied to the room, from oldest the newest. - public static func preparedMessagesSince( - seqNo: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[Failable]> { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), - queryParameters: [ - .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5", - .limit: "\(dependencies[feature: .communityPollLimit])" - ], - authMethod: authMethod - ), - responseType: [Failable].self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server - /// - /// - Parameters: - /// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted - /// - /// - roomToken: The room token from which the messages should be deleted - /// - /// The invoking user **must** be a moderator of the given room or an admin if trying to delete the messages - /// of another admin. - /// - /// - server: The server to delete messages from - /// - /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedMessagesDeleteAll( - sessionId: String, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .delete, - endpoint: Endpoint.roomDeleteMessages(roomToken, sessionId: sessionId), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Reactions - - /// Returns the list of all reactors who have added a particular reaction to a particular message. - public static func preparedReactors( - emoji: String, - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - /// URL(String:) won't convert raw emojis, so need to do a little encoding here. - /// The raw emoji will come back when calling url.path - guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji - } - - return try Network.PreparedRequest( - request: Request( - method: .get, - endpoint: .reactors(roomToken, id: id, emoji: encodedEmoji), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Adds a reaction to the given message in this room. The user must have read access in the room. - /// - /// Reactions are short strings of 1-12 unicode codepoints, typically emoji (or character sequences to produce an emoji variant, - /// such as 👨🏿‍🦰, which is composed of 4 unicode "characters" but usually renders as a single emoji "Man: Dark Skin Tone, Red Hair"). - public static func preparedReactionAdd( - emoji: String, - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - /// URL(String:) won't convert raw emojis, so need to do a little encoding here. - /// The raw emoji will come back when calling url.path - guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji - } - - return try Network.PreparedRequest( - request: Request( - method: .put, - endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji), - authMethod: authMethod - ), - responseType: ReactionAddResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Removes a reaction from a post this room. The user must have read access in the room. This only removes the user's own reaction - /// but does not affect the reactions of other users. - public static func preparedReactionDelete( - emoji: String, - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - /// URL(String:) won't convert raw emojis, so need to do a little encoding here. - /// The raw emoji will come back when calling url.path - guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji - } - - return try Network.PreparedRequest( - request: Request( - method: .delete, - endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji), - authMethod: authMethod - ), - responseType: ReactionRemoveResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Removes all reactions of all users from a post in this room. The calling must have moderator permissions in the room. This endpoint - /// can either remove a single reaction (e.g. remove all 🍆 reactions) by specifying it after the message id (following a /), or remove all - /// reactions from the post by not including the / suffix of the URL. - public static func preparedReactionDeleteAll( - emoji: String, - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - /// URL(String:) won't convert raw emojis, so need to do a little encoding here. - /// The raw emoji will come back when calling url.path - guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji - } - - return try Network.PreparedRequest( - request: Request( - method: .delete, - endpoint: .reactionDelete(roomToken, id: id, emoji: encodedEmoji), - authMethod: authMethod - ), - responseType: ReactionRemoveAllResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Pinning - - /// Adds a pinned message to this room - /// - /// **Note:** Existing pinned messages are not removed: the new message is added to the pinned message list (If you want to remove existing - /// pins then build a sequence request that first calls .../unpin/all) - /// - /// The user must have admin (not just moderator) permissions in the room in order to pin messages - /// - /// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned - /// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the - /// order in which pinned messages should be displayed - public static func preparedPinMessage( - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: .roomPinMessage(roomToken, id: id), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Remove a message from this room's pinned message list - /// - /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinMessage( - id: Int64, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: .roomUnpinMessage(roomToken, id: id), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Removes _all_ pinned messages from this room - /// - /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinAll( - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: .roomUnpinAll(roomToken), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Files - - public static func preparedUpload( - data: Data, - roomToken: String, - fileName: String? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - guard case .community(let server, let publicKey, _, _, _) = authMethod.info else { - throw NetworkError.invalidPreparedRequest - } - - return try Network.PreparedRequest( - request: Request( - endpoint: Endpoint.roomFile(roomToken), - destination: .serverUpload( - server: server, - x25519PublicKey: publicKey, - fileName: fileName - ), - body: data - ), - responseType: FileUploadResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: Network.fileUploadTimeout, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - public static func downloadUrlString( - for fileId: String, - server: String, - roomToken: String - ) -> String { - return "\(server)/\(Endpoint.roomFileIndividual(roomToken, fileId).path)" - } - - public static func preparedDownload( - url: URL, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - guard let fileId: String = Attachment.fileId(for: url.absoluteString) else { throw NetworkError.invalidURL } - - return try preparedDownload(fileId: fileId, roomToken: roomToken, authMethod: authMethod, using: dependencies) - } - - public static func preparedDownload( - fileId: String, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - endpoint: .roomFileIndividual(roomToken, fileId), - authMethod: authMethod - ), - responseType: Data.self, - additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: Network.fileDownloadTimeout, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Inbox/Outbox (Message Requests) - - /// Retrieves all of the user's current DMs (up to limit) - /// - /// **Note:** `inbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInbox( - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[DirectMessage]?> { - return try Network.PreparedRequest( - request: Request( - endpoint: .inbox, - authMethod: authMethod - ), - responseType: [DirectMessage]?.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages - /// - /// **Note:** `inboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInboxSince( - id: Int64, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[DirectMessage]?> { - return try Network.PreparedRequest( - request: Request( - endpoint: .inboxSince(id: id), - authMethod: authMethod - ), - responseType: [DirectMessage]?.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Remove all message requests from inbox, this methrod will return the number of messages deleted - public static func preparedClearInbox( - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .delete, - endpoint: .inbox, - authMethod: authMethod - ), - responseType: DeleteInboxResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Delivers a direct message to a user via their blinded Session ID - /// - /// The body of this request is a JSON object containing a message key with a value of the encrypted-then-base64-encoded message to deliver - public static func preparedSend( - ciphertext: Data, - toInboxFor blindedSessionId: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.inboxFor(sessionId: blindedSessionId), - body: SendDirectMessageRequest( - message: ciphertext - ), - authMethod: authMethod - ), - responseType: SendDirectMessageResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Retrieves all of the user's sent DMs (up to limit) - /// - /// **Note:** `outbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutbox( - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[DirectMessage]?> { - return try Network.PreparedRequest( - request: Request( - endpoint: .outbox, - authMethod: authMethod - ), - responseType: [DirectMessage]?.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages - /// - /// **Note:** `outboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutboxSince( - id: Int64, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[DirectMessage]?> { - return try Network.PreparedRequest( - request: Request( - endpoint: .outboxSince(id: id), - authMethod: authMethod - ), - responseType: [DirectMessage]?.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Users - - /// Applies a ban of a user from specific rooms, or from the server globally - /// - /// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a - /// `globalModerator` (or `globalAdmin`) if using the global parameter - /// - /// **Note:** The user's messages are not deleted by this request - In order to ban and delete all messages use the `/sequence` endpoint to - /// bundle a `/user/.../ban` with a `/user/.../deleteMessages` request - /// - /// - Parameters: - /// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted - /// - /// - timeout: Value specifying a time limit on the ban, in seconds - /// - /// The applied ban will expire and be removed after the given interval - If omitted (or `null`) then the ban is permanent - /// - /// If this endpoint is called multiple times then the timeout of the last call takes effect (eg. a permanent ban can be replaced - /// with a time-limited ban by calling the endpoint again with a timeout value, and vice versa) - /// - /// - roomTokens: List of one or more room tokens from which the user should be banned from - /// - /// The invoking user **must** be a moderator of all of the given rooms. - /// - /// This may be set to the single-element list `["*"]` to ban the user from all rooms in which the current user has moderator - /// permissions (the call will succeed if the calling user is a moderator in at least one channel) - /// - /// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter (the invoking user must be a - /// global moderator in order to add a global ban) - /// - /// - server: The server to delete messages from - /// - /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserBan( - sessionId: String, - for timeout: TimeInterval? = nil, - from roomTokens: [String]? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.userBan(sessionId), - body: UserBanRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil), - timeout: timeout - ), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Removes a user ban from specific rooms, or from the server globally - /// - /// The invoking user must have `moderator` (or `admin`) permission in all given rooms when specifying rooms, and must be a global server `moderator` - /// (or `admin`) if using the `global` parameter - /// - /// **Note:** Room and global bans are independent: if a user is banned globally and has a room-specific ban then removing the global ban does not remove - /// the room specific ban, and removing the room-specific ban does not remove the global ban (to fully unban a user globally and from all rooms, submit a - /// `/sequence` request with a global unban followed by a "rooms": ["*"] unban) - /// - /// - Parameters: - /// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted - /// - /// - roomTokens: List of one or more room tokens from which the user should be unbanned from - /// - /// The invoking user **must** be a moderator of all of the given rooms. - /// - /// This may be set to the single-element list `["*"]` to unban the user from all rooms in which the current user has moderator - /// permissions (the call will succeed if the calling user is a moderator in at least one channel) - /// - /// **Note:** You can ban from all rooms on a server by providing a `nil` value for this parameter - /// - /// - server: The server to delete messages from - /// - /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserUnban( - sessionId: String, - from roomTokens: [String]?, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.userUnban(sessionId), - body: UserUnbanRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil) - ), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// Appoints or removes a moderator or admin - /// - /// This endpoint is used to appoint or remove moderator/admin permissions either for specific rooms or for server-wide global moderator permissions - /// - /// Admins/moderators of rooms can only be appointed or removed by a user who has admin permissions in the room (including global admins) - /// - /// Global admins/moderators may only be appointed by a global admin - /// - /// The admin/moderator paramters interact as follows: - /// - **admin=true, moderator omitted:** This adds admin permissions, which automatically also implies moderator permissions - /// - **admin=true, moderator=true:** Exactly the same as above - /// - **admin=false, moderator=true:** Removes any existing admin permissions from the rooms (or globally), if present, and adds - /// moderator permissions to the rooms/globally (if not already present) - /// - **admin=false, moderator omitted:** This removes admin permissions but leaves moderator permissions, if present (this - /// effectively "downgrades" an admin to a moderator). Unlike the above this does **not** add moderator permissions to matching rooms - /// if not already present - /// - **moderator=true, admin omitted:** Adds moderator permissions to the given rooms (or globally), if not already present. If - /// the user already has admin permissions this does nothing (that is, admin permission is *not* removed, unlike the above) - /// - **moderator=false, admin omitted:** This removes moderator **and** admin permissions from all given rooms (or globally) - /// - **moderator=false, admin=false:** Exactly the same as above - /// - **moderator=false, admin=true:** This combination is **not permitted** (because admin permissions imply moderator - /// permissions) and will result in Bad Request error if given - /// - /// - Parameters: - /// - sessionId: The sessionId (either standard or blinded) of the user to modify the permissions of - /// - /// - moderator: Value indicating that this user should have moderator permissions added (true), removed (false), or left alone (null) - /// - /// - admin: Value indicating that this user should have admin permissions added (true), removed (false), or left alone (null) - /// - /// Granting admin permission automatically includes granting moderator permission (and thus it is an error to use admin=true with - /// moderator=false) - /// - /// - visible: Value indicating whether the moderator/admin should be made publicly visible as a moderator/admin of the room(s) - /// (if true) or hidden (false) - /// - /// Hidden moderators/admins still have all the same permissions as visible moderators/admins, but are visible only to other - /// moderators/admins; regular users in the room will not know their moderator status - /// - /// - roomTokens: List of one or more room tokens to which the permission changes should be applied - /// - /// The invoking user **must** be an admin of all of the given rooms. - /// - /// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin - /// permissions (the call will succeed if the calling user is an admin in at least one channel) - /// - /// **Note:** You can specify a change to global permisisons by providing a `nil` value for this parameter - /// - /// - server: The server to perform the permission changes on - /// - /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserModeratorUpdate( - sessionId: String, - moderator: Bool? = nil, - admin: Bool? = nil, - visible: Bool, - for roomTokens: [String]?, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { - throw NetworkError.invalidPreparedRequest - } - - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.userModerator(sessionId), - body: UserModeratorRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil), - moderator: moderator, - admin: admin, - visible: visible - ), - authMethod: authMethod - ), - responseType: NoResponse.self, - additionalSignatureData: AdditionalSigningData(authMethod), - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - /// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those - /// methods for the documented behaviour of each method - public static func preparedUserBanAndDeleteAllMessages( - sessionId: String, - roomToken: String, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest> { - return try OpenGroupAPI - .preparedSequence( - requests: [ - preparedUserBan( - sessionId: sessionId, - from: [roomToken], - authMethod: authMethod, - using: dependencies - ), - preparedMessagesDeleteAll( - sessionId: sessionId, - roomToken: roomToken, - authMethod: authMethod, - using: dependencies - ) - ], - authMethod: authMethod, - using: dependencies - ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) - } - - // MARK: - Authentication - - fileprivate static func signatureHeaders( - url: URL, - method: HTTPMethod, - body: Data?, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> [HTTPHeader: String] { - let path: String = url.path - .appending(url.query.map { value in "?\(value)" }) - let method: String = method.rawValue - let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970)) - - guard - case .community(_, let publicKey, _, _, _) = authMethod.info, - !publicKey.isEmpty, - let nonce: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(16)), - let timestampBytes: [UInt8] = "\(timestamp)".data(using: .ascii).map({ Array($0) }) - else { throw OpenGroupAPIError.signingFailed } - - /// Get a hash of any body content - let bodyHash: [UInt8]? = { - guard let body: Data = body else { return nil } - - return dependencies[singleton: .crypto].generate(.hash(message: body.bytes, length: 64)) - }() - - /// Generate the signature message - /// "ServerPubkey || Nonce || Timestamp || Method || Path || Blake2b Hash(Body) - /// `ServerPubkey` - /// `Nonce` - /// `Timestamp` is the bytes of an ascii decimal string - /// `Method` - /// `Path` - /// `Body` is a Blake2b hash of the data (if there is a body) - let messageBytes: [UInt8] = Data(hex: publicKey).bytes - .appending(contentsOf: nonce) - .appending(contentsOf: timestampBytes) - .appending(contentsOf: method.bytes) - .appending(contentsOf: path.bytes) - .appending(contentsOf: bodyHash ?? []) - - /// Sign the above message - let signResult: (publicKey: String, signature: [UInt8]) = try sign( - messageBytes: messageBytes, - authMethod: authMethod, - fallbackSigningType: .unblinded, - using: dependencies - ) - - return [ - HTTPHeader.sogsPubKey: signResult.publicKey, - HTTPHeader.sogsTimestamp: "\(timestamp)", - HTTPHeader.sogsNonce: Data(nonce).base64EncodedString(), - HTTPHeader.sogsSignature: signResult.signature.toBase64() - ] - } - - /// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities) - private static func sign( - messageBytes: [UInt8], - authMethod: AuthenticationMethod, - fallbackSigningType signingType: SessionId.Prefix, - using dependencies: Dependencies - ) throws -> (publicKey: String, signature: [UInt8]) { - guard - !dependencies[cache: .general].ed25519SecretKey.isEmpty, - !dependencies[cache: .general].ed25519Seed.isEmpty, - case .community(_, let publicKey, let hasCapabilities, let supportsBlinding, let forceBlinded) = authMethod.info - else { throw OpenGroupAPIError.signingFailed } - - // If we have no capabilities or if the server supports blinded keys then sign using the blinded key - if forceBlinded || !hasCapabilities || supportsBlinding { - guard - let blinded15KeyPair: KeyPair = dependencies[singleton: .crypto].generate( - .blinded15KeyPair( - serverPublicKey: publicKey, - ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey - ) - ), - let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( - .signatureBlind15( - message: messageBytes, - serverPublicKey: publicKey, - ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey - ) - ) - else { throw OpenGroupAPIError.signingFailed } - - return ( - publicKey: SessionId(.blinded15, publicKey: blinded15KeyPair.publicKey).hexString, - signature: signatureResult - ) - } - - // Otherwise sign using the fallback type - switch signingType { - case .unblinded: - guard - let signature: Authentication.Signature = dependencies[singleton: .crypto].generate( - .signature( - message: messageBytes, - ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey - ) - ), - let ed25519KeyPair: KeyPair = dependencies[singleton: .crypto].generate( - .ed25519KeyPair(seed: dependencies[cache: .general].ed25519Seed) - ), - case .standard(let signatureResult) = signature - else { throw OpenGroupAPIError.signingFailed } - - return ( - publicKey: SessionId(.unblinded, publicKey: ed25519KeyPair.publicKey).hexString, - signature: signatureResult - ) - - // Default to using the 'standard' key - default: - guard - let ed25519KeyPair: KeyPair = dependencies[singleton: .crypto].generate( - .ed25519KeyPair(seed: dependencies[cache: .general].ed25519Seed) - ), - let x25519PublicKey: [UInt8] = dependencies[singleton: .crypto].generate( - .x25519(ed25519Pubkey: ed25519KeyPair.publicKey) - ), - let x25519SecretKey: [UInt8] = dependencies[singleton: .crypto].generate( - .x25519(ed25519Seckey: ed25519KeyPair.secretKey) - ), - let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( - .signatureXed25519(data: messageBytes, curve25519PrivateKey: x25519SecretKey) - ) - else { throw OpenGroupAPIError.signingFailed } - - return ( - publicKey: SessionId(.standard, publicKey: x25519PublicKey).hexString, - signature: signatureResult - ) - } - } - - /// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities) - private static func signRequest( - preparedRequest: Network.PreparedRequest, - using dependencies: Dependencies - ) throws -> Network.Destination { - guard let signingData: AdditionalSigningData = preparedRequest.additionalSignatureData as? AdditionalSigningData else { - throw OpenGroupAPIError.signingFailed - } - - return try preparedRequest.destination - .signed(data: signingData, body: preparedRequest.body, using: dependencies) - } -} - -private extension OpenGroupAPI { - struct AdditionalSigningData { - let authMethod: AuthenticationMethod - - init(_ authMethod: AuthenticationMethod) { - self.authMethod = authMethod - } - } -} - -private extension Network.Destination { - func signed(data: OpenGroupAPI.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { - switch self { - case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: throw NetworkError.unauthorised - case .cached: return self - case .server(let info): return .server(info: try info.signed(data, body, using: dependencies)) - case .serverUpload(let info, let fileName): - return .serverUpload(info: try info.signed(data, body, using: dependencies), fileName: fileName) - - case .serverDownload(let info): - return .serverDownload(info: try info.signed(data, body, using: dependencies)) - } - } -} - -private extension Network.Destination.ServerInfo { - func signed(_ data: OpenGroupAPI.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { - return updated(with: try OpenGroupAPI.signatureHeaders( - url: url, - method: method, - body: body, - authMethod: data.authMethod, - using: dependencies - )) - } -} diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 6b22a8c476..e0b7269ffd 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -35,7 +35,7 @@ public extension Log.Category { // MARK: - OpenGroupManager public final class OpenGroupManager { - public typealias DefaultRoomInfo = (room: OpenGroupAPI.Room, openGroup: OpenGroup) + public typealias DefaultRoomInfo = (room: Network.SOGS.Room, openGroup: OpenGroup) private let dependencies: Dependencies @@ -79,8 +79,8 @@ public final class OpenGroupManager { .replacingOccurrences(of: serverPort, with: "") ) let options: Set = Set([ - OpenGroupAPI.legacyDefaultServerIP, - OpenGroupAPI.defaultServer + Network.SOGS.legacyDefaultServerIP, + Network.SOGS.defaultServer .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") ]) @@ -103,7 +103,7 @@ public final class OpenGroupManager { .lowercased() .replacingOccurrences(of: serverPort, with: "") ) - let defaultServerHost: String = OpenGroupAPI.defaultServer + let defaultServerHost: String = Network.SOGS.defaultServer .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") var serverOptions: Set = Set([ @@ -119,9 +119,9 @@ public final class OpenGroupManager { serverOptions.insert(defaultServerHost) serverOptions.insert("http://\(defaultServerHost)") serverOptions.insert("https://\(defaultServerHost)") - serverOptions.insert(OpenGroupAPI.legacyDefaultServerIP) - serverOptions.insert("http://\(OpenGroupAPI.legacyDefaultServerIP)") - serverOptions.insert("https://\(OpenGroupAPI.legacyDefaultServerIP)") + serverOptions.insert(Network.SOGS.legacyDefaultServerIP) + serverOptions.insert("http://\(Network.SOGS.legacyDefaultServerIP)") + serverOptions.insert("https://\(Network.SOGS.legacyDefaultServerIP)") } // First check if there is no poller for the specified server @@ -161,7 +161,7 @@ public final class OpenGroupManager { return server.lowercased() } - return OpenGroupAPI.defaultServer + return Network.SOGS.defaultServer }() let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: targetServer) @@ -223,11 +223,11 @@ public final class OpenGroupManager { return server.lowercased() } - return OpenGroupAPI.defaultServer + return Network.SOGS.defaultServer }() return Result { - try OpenGroupAPI + try Network.SOGS .preparedCapabilitiesAndRoom( roomToken: roomToken, authMethod: Authentication.community( @@ -243,7 +243,7 @@ public final class OpenGroupManager { } .publisher .flatMap { [dependencies] in $0.send(using: dependencies) } - .flatMapStorageWritePublisher(using: dependencies) { [dependencies] (db: ObservingDatabase, response: (info: ResponseInfoType, value: OpenGroupAPI.CapabilitiesAndRoomResponse)) -> Void in + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] (db: ObservingDatabase, response: (info: ResponseInfoType, value: Network.SOGS.CapabilitiesAndRoomResponse)) -> Void in // Add the new open group to libSession try LibSession.add( db, @@ -263,7 +263,7 @@ public final class OpenGroupManager { // Then the room try OpenGroupManager.handlePollInfo( db, - pollInfo: OpenGroupAPI.RoomPollInfo(room: response.value.room.data), + pollInfo: Network.SOGS.RoomPollInfo(room: response.value.room.data), publicKey: publicKey, for: roomToken, on: targetServer, @@ -334,7 +334,7 @@ public final class OpenGroupManager { try MessageDeduplication.deleteIfNeeded(db, threadIds: [openGroupId], using: dependencies) // Remove the open group (no foreign key to the thread so it won't auto-delete) - if server?.lowercased() != OpenGroupAPI.defaultServer.lowercased() { + if server?.lowercased() != Network.SOGS.defaultServer.lowercased() { _ = try? OpenGroup .filter(id: openGroupId) .deleteAll(db) @@ -359,7 +359,7 @@ public final class OpenGroupManager { internal static func handleCapabilities( _ db: ObservingDatabase, - capabilities: OpenGroupAPI.Capabilities, + capabilities: Network.SOGS.CapabilitiesResponse, on server: String ) { // Remove old capabilities first @@ -371,7 +371,7 @@ public final class OpenGroupManager { capabilities.capabilities.forEach { capability in try? Capability( openGroupServer: server.lowercased(), - variant: capability, + variant: Capability.Variant(from: capability), isMissing: false ) .upsert(db) @@ -379,7 +379,7 @@ public final class OpenGroupManager { capabilities.missing?.forEach { capability in try? Capability( openGroupServer: server.lowercased(), - variant: capability, + variant: Capability.Variant(from: capability), isMissing: true ) .upsert(db) @@ -388,7 +388,7 @@ public final class OpenGroupManager { internal static func handlePollInfo( _ db: ObservingDatabase, - pollInfo: OpenGroupAPI.RoomPollInfo, + pollInfo: Network.SOGS.RoomPollInfo, publicKey maybePublicKey: String?, for roomToken: String, on server: String, @@ -431,7 +431,7 @@ public final class OpenGroupManager { .updateAllAndConfig(db, changes, using: dependencies) // Update the admin/moderator group members - if let roomDetails: OpenGroupAPI.Room = pollInfo.details { + if let roomDetails: Network.SOGS.Room = pollInfo.details { _ = try? GroupMember .filter(GroupMember.Columns.groupId == threadId) .deleteAll(db) @@ -531,7 +531,7 @@ public final class OpenGroupManager { internal static func handleMessages( _ db: ObservingDatabase, - messages: [OpenGroupAPI.Message], + messages: [Network.SOGS.Message], for roomToken: String, on server: String, using dependencies: Dependencies @@ -543,7 +543,7 @@ public final class OpenGroupManager { // Sorting the messages by server ID before importing them fixes an issue where messages // that quote older messages can't find those older messages - let sortedMessages: [OpenGroupAPI.Message] = messages + let sortedMessages: [Network.SOGS.Message] = messages .filter { $0.deleted != true } .sorted { lhs, rhs in lhs.id < rhs.id } var messageServerInfoToRemove: [(id: Int64, seqNo: Int64)] = messages @@ -684,7 +684,7 @@ public final class OpenGroupManager { internal static func handleDirectMessages( _ db: ObservingDatabase, - messages: [OpenGroupAPI.DirectMessage], + messages: [Network.SOGS.DirectMessage], fromOutbox: Bool, on server: String, using dependencies: Dependencies @@ -698,7 +698,7 @@ public final class OpenGroupManager { // Sorting the messages by server ID before importing them fixes an issue where messages // that quote older messages can't find those older messages - let sortedMessages: [OpenGroupAPI.DirectMessage] = messages + let sortedMessages: [Network.SOGS.DirectMessage] = messages .sorted { lhs, rhs in lhs.id < rhs.id } let latestMessageId: Int64 = sortedMessages[sortedMessages.count - 1].id var lookupCache: [String: BlindedIdLookup] = [:] // Only want this cache to exist for the current loop @@ -829,9 +829,9 @@ public final class OpenGroupManager { id: Int64, in roomToken: String, on server: String, - type: OpenGroupAPI.PendingChange.ReactAction - ) -> OpenGroupAPI.PendingChange { - let pendingChange = OpenGroupAPI.PendingChange( + type: OpenGroupManager.PendingChange.ReactAction + ) -> OpenGroupManager.PendingChange { + let pendingChange = OpenGroupManager.PendingChange( server: server, room: roomToken, changeType: .reaction, @@ -849,7 +849,7 @@ public final class OpenGroupManager { return pendingChange } - public func updatePendingChange(_ pendingChange: OpenGroupAPI.PendingChange, seqNo: Int64?) { + public func updatePendingChange(_ pendingChange: OpenGroupManager.PendingChange, seqNo: Int64?) { dependencies.mutate(cache: .openGroupManager) { if let index = $0.pendingChanges.firstIndex(of: pendingChange) { $0.pendingChanges[index].seqNo = seqNo @@ -857,7 +857,7 @@ public final class OpenGroupManager { } } - public func removePendingChange(_ pendingChange: OpenGroupAPI.PendingChange) { + public func removePendingChange(_ pendingChange: OpenGroupManager.PendingChange) { dependencies.mutate(cache: .openGroupManager) { if let index = $0.pendingChanges.firstIndex(of: pendingChange) { $0.pendingChanges.remove(at: index) @@ -989,7 +989,7 @@ public extension OpenGroupManager { private let dependencies: Dependencies private let defaultRoomsSubject: CurrentValueSubject<[DefaultRoomInfo], Error> = CurrentValueSubject([]) private var _lastSuccessfulCommunityPollTimestamp: TimeInterval? - public var pendingChanges: [OpenGroupAPI.PendingChange] = [] + public var pendingChanges: [OpenGroupManager.PendingChange] = [] public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error> { defaultRoomsSubject @@ -1044,13 +1044,13 @@ public extension OpenGroupManager { public protocol OGMImmutableCacheType: ImmutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get } - var pendingChanges: [OpenGroupAPI.PendingChange] { get } + var pendingChanges: [OpenGroupManager.PendingChange] { get } } public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get } - var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + var pendingChanges: [OpenGroupManager.PendingChange] { get set } func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) diff --git a/SessionMessagingKit/Open Groups/Types/Capabilities.swift b/SessionMessagingKit/Open Groups/Types/Capabilities.swift deleted file mode 100644 index c4cb7c5c77..0000000000 --- a/SessionMessagingKit/Open Groups/Types/Capabilities.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension Network.SOGS { - public struct Capabilities: Codable, Equatable { - public let capabilities: [Network.SOGS.Capability.Variant] - public let missing: [Network.SOGS.Capability.Variant]? - - // MARK: - Initialization - - public init(capabilities: [Network.SOGS.Capability.Variant], missing: [Network.SOGS.Capability.Variant]? = nil) { - self.capabilities = capabilities - self.missing = missing - } - } -} diff --git a/SessionMessagingKit/Open Groups/Types/PendingChange.swift b/SessionMessagingKit/Open Groups/Types/PendingChange.swift index dd5af98b5f..6ab6cd2145 100644 --- a/SessionMessagingKit/Open Groups/Types/PendingChange.swift +++ b/SessionMessagingKit/Open Groups/Types/PendingChange.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension OpenGroupManager { public struct PendingChange: Equatable { public enum ChangeType { case reaction @@ -24,23 +24,23 @@ extension OpenGroupAPI { var seqNo: Int64? let metadata: Metadata - public static func == (lhs: OpenGroupAPI.PendingChange, rhs: OpenGroupAPI.PendingChange) -> Bool { - guard lhs.server == rhs.server && - lhs.room == rhs.room && - lhs.changeType == rhs.changeType && - lhs.seqNo == rhs.seqNo - else { - return false - } + public static func == (lhs: OpenGroupManager.PendingChange, rhs: OpenGroupManager.PendingChange) -> Bool { + guard + lhs.server == rhs.server && + lhs.room == rhs.room && + lhs.changeType == rhs.changeType && + lhs.seqNo == rhs.seqNo + else { return false } switch lhs.changeType { case .reaction: if case .reaction(let lhsMessageId, let lhsEmoji, let lhsAction) = lhs.metadata, - case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata { + case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata + { return lhsMessageId == rhsMessageId && lhsEmoji == rhsEmoji && lhsAction == rhsAction - } else { - return false } + + return false } } } diff --git a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift index dec4a6495e..c543b0dd4b 100644 --- a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift +++ b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift @@ -11,7 +11,7 @@ import SessionUtilitiesKit public final class AttachmentUploader { private enum Destination { case fileServer - case community(LibSession.OpenGroupCapabilityInfo) + case community(roomToken: String, server: String) var shouldEncrypt: Bool { switch self { @@ -78,7 +78,9 @@ public final class AttachmentUploader { // Generate the correct upload info based on the state of the attachment let destination: AttachmentUploader.Destination = { switch authMethod { - case let auth as Authentication.community: return .community(auth.openGroupCapabilityInfo) + case let auth as Authentication.community: + return .community(roomToken: auth.roomToken, server: auth.server) + default: return .fileServer } }() @@ -86,14 +88,14 @@ public final class AttachmentUploader { let endpoint: (any EndpointType) = { switch destination { case .fileServer: return Network.FileServer.Endpoint.file - case .community(let info): return OpenGroupAPI.Endpoint.roomFile(info.roomToken) + case .community(let roomToken, _): return Network.SOGS.Endpoint.roomFile(roomToken) } }() // This can occur if an AttachmentUploadJob was explicitly created for a message // dependant on the attachment being uploaded (in this case the attachment has // already been uploaded so just succeed) - if attachment.state == .uploaded, let fileId: String = Attachment.fileId(for: attachment.downloadUrl) { + if attachment.state == .uploaded, let fileId: String = Network.FileServer.fileId(for: attachment.downloadUrl) { return ( attachment, try Network.PreparedRequest.cached( @@ -114,7 +116,7 @@ public final class AttachmentUploader { // Note: The most common cases for this will be for LinkPreviews or Quotes if attachment.state == .downloaded, - let fileId: String = Attachment.fileId(for: attachment.downloadUrl), + let fileId: String = Network.FileServer.fileId(for: attachment.downloadUrl), ( !destination.shouldEncrypt || ( attachment.encryptionKey != nil && @@ -174,13 +176,13 @@ public final class AttachmentUploader { digest ) - case .community(let info): + case .community(let roomToken, _): return ( attachment, - try OpenGroupAPI.preparedUpload( + try Network.SOGS.preparedUpload( data: finalData, - roomToken: info.roomToken, - authMethod: Authentication.community(info: info), + roomToken: roomToken, + authMethod: authMethod, using: dependencies ), encryptionKey, @@ -211,11 +213,11 @@ public final class AttachmentUploader { case (_, _, .fileServer): return Network.FileServer.downloadUrlString(for: response.id) - case (_, _, .community(let info)): - return OpenGroupAPI.downloadUrlString( + case (_, _, .community(let roomToken, let server)): + return Network.SOGS.downloadUrlString( for: response.id, - server: info.server, - roomToken: info.roomToken + server: server, + roomToken: roomToken ) } }(), diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index e915187bad..79ca54ca61 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -307,7 +307,7 @@ extension MessageReceiver { // devices that had the group before they were promoted try SnodeReceivedMessageInfo .filter(SnodeReceivedMessageInfo.Columns.swarmPublicKey == groupSessionId.hexString) - .filter(SnodeReceivedMessageInfo.Columns.namespace == SnodeAPI.Namespace.groupMessages.rawValue) + .filter(SnodeReceivedMessageInfo.Columns.namespace == Network.SnodeAPI.Namespace.groupMessages.rawValue) .updateAllAndConfig( db, SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: true), @@ -753,7 +753,7 @@ extension MessageReceiver { ) else { return } - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, @@ -921,7 +921,7 @@ extension MessageReceiver { db.afterCommit { dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: [serverHash], requireSuccessfulDeletion: false, authMethod: try Authentication.with( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index 3796c45882..c10460d2f4 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -70,7 +70,7 @@ extension MessageReceiver { case .contact: dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, authMethod: try Authentication.with( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 241ce6d030..8defb0e98d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -13,7 +13,7 @@ extension MessageSender { thread: SessionThread, group: ClosedGroup, members: [GroupMember], - preparedNotificationsSubscription: Network.PreparedRequest? + preparedNotificationsSubscription: Network.PreparedRequest? ) public static func createGroup( @@ -119,14 +119,19 @@ extension MessageSender { ) // Prepare the notification subscription - var preparedNotificationSubscription: Network.PreparedRequest? + var preparedNotificationSubscription: Network.PreparedRequest? if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - preparedNotificationSubscription = try? PushNotificationAPI + preparedNotificationSubscription = try? Network.PushNotification .preparedSubscribe( - db, token: Data(hex: token), - sessionIds: [createdInfo.groupSessionId], + swarms: [( + createdInfo.groupSessionId, + Authentication.groupAdmin( + groupSessionId: createdInfo.groupSessionId, + ed25519SecretKey: createdInfo.identityKeyPair.secretKey + ) + )], using: dependencies ) } @@ -593,7 +598,7 @@ extension MessageSender { using: dependencies ) - maybeSupplementalKeyRequest = try SnodeAPI.preparedSendMessage( + maybeSupplementalKeyRequest = try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: sessionId.hexString, data: supplementData, @@ -677,7 +682,7 @@ extension MessageSender { /// Unrevoke the newly added members just in case they had previously gotten their access to the group /// revoked (fire-and-forget this request, we don't want it to be blocking - if the invited user still can't access /// the group the admin can resend their invitation which will also attempt to unrevoke their subaccount) - let unrevokeRequest: Network.PreparedRequest = try SnodeAPI.preparedUnrevokeSubaccounts( + let unrevokeRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedUnrevokeSubaccounts( subaccountsToUnrevoke: memberJobData.map { _, _, _, subaccountToken in subaccountToken }, authMethod: Authentication.groupAdmin( groupSessionId: sessionId, @@ -856,7 +861,7 @@ extension MessageSender { using: dependencies ) - maybeSupplementalKeyRequest = try SnodeAPI.preparedSendMessage( + maybeSupplementalKeyRequest = try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: sessionId.hexString, data: supplementData, @@ -902,7 +907,7 @@ extension MessageSender { /// Unrevoke the member just in case they had previously gotten their access to the group revoked and the /// unrevoke request when initially added them failed (fire-and-forget this request, we don't want it to be blocking) - let unrevokeRequest: Network.PreparedRequest = try SnodeAPI + let unrevokeRequest: Network.PreparedRequest = try Network.SnodeAPI .preparedUnrevokeSubaccounts( subaccountsToUnrevoke: memberInfo.map { token, _ in token }, authMethod: Authentication.groupAdmin( diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 961a71a7f6..78ed522564 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -43,7 +43,7 @@ public final class MessageSender { public static func preparedSend( message: Message, to destination: Message.Destination, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, interactionId: Int64?, attachments: [(attachment: Attachment, fileId: String)]?, authMethod: AuthenticationMethod, @@ -138,7 +138,7 @@ public final class MessageSender { private static func preparedSendToSnodeDestination( message: Message, to destination: Message.Destination, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, interactionId: Int64?, attachments: [(attachment: Attachment, fileId: String)]?, messageSendTimestampMs: Int64, @@ -146,7 +146,9 @@ public final class MessageSender { onEvent: ((Event) -> Void)?, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - guard let namespace: SnodeAPI.Namespace = namespace else { throw MessageSenderError.invalidMessage } + guard let namespace: Network.SnodeAPI.Namespace = namespace else { + throw MessageSenderError.invalidMessage + } /// Set the sender/recipient info (needed to be valid) /// @@ -201,7 +203,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try SnodeAPI + return try Network.SnodeAPI .preparedSendMessage( message: snodeMessage, in: namespace, @@ -287,7 +289,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try OpenGroupAPI + return try Network.SOGS .preparedSend( plaintext: plaintext, roomToken: roomToken, @@ -352,7 +354,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try OpenGroupAPI + return try Network.SOGS .preparedSend( ciphertext: ciphertext, toInboxFor: recipientBlindedPublicKey, @@ -371,7 +373,7 @@ public final class MessageSender { // MARK: - Message Wrapping public static func encodeMessageForSending( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, destination: Message.Destination, message: Message, attachments: [(attachment: Attachment, fileId: String)]?, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift deleted file mode 100644 index 1a87dcf8e5..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyGroupOnlyRequest: Codable { - let token: String - let pubKey: String - let device: String - let legacyGroupPublicKeys: Set - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift deleted file mode 100644 index 962011dfd4..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyGroupRequest: Codable { - let pubKey: String - let closedGroupPublicKey: String - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift deleted file mode 100644 index 491fa77570..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyNotifyRequest: Codable { - enum CodingKeys: String, CodingKey { - case data - case sendTo = "send_to" - } - - let data: String - let sendTo: String - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift deleted file mode 100644 index bd412f24e7..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public extension PushNotificationAPI { - struct LegacyPushServerResponse: Codable { - let code: Int - let message: String? - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift deleted file mode 100644 index f29520550b..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionNetworkingKit - -extension PushNotificationAPI { - struct LegacyUnsubscribeRequest: Codable { - private let token: String - - init(token: String) { - self.token = token - } - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift new file mode 100644 index 0000000000..dda9ad50dd --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift @@ -0,0 +1,127 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import SessionNetworkingKit +import SessionUtilitiesKit + +public extension Network.PushNotification { + static func subscribeAll( + token: Data, + isForcedUpdate: Bool, + using dependencies: Dependencies + ) -> AnyPublisher { + let hexEncodedToken: String = token.toHexString() + let oldToken: String? = dependencies[defaults: .standard, key: .deviceToken] + let lastUploadTime: Double = dependencies[defaults: .standard, key: .lastDeviceTokenUpload] + let now: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { + Log.info(.pushNotificationAPI, "Device token hasn't changed or expired; no need to re-upload.") + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let userAuthMethod: AuthenticationMethod = try Authentication.with( + db, + swarmPublicKey: userSessionId.hexString, + using: dependencies + ) + + return try Network.PushNotification + .preparedSubscribe( + token: token, + swarms: [(userSessionId, userAuthMethod)] + .appending(contentsOf: try ClosedGroup + .select(.threadId) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .filter(ClosedGroup.Columns.shouldPoll) + .asRequest(of: String.self) + .fetchSet(db) + .map { threadId in + ( + SessionId(.group, hex: threadId), + try Authentication.with( + db, + swarmPublicKey: threadId, + using: dependencies + ) + ) + } + ), + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + guard response.subResponses.first?.success == true else { return } + + dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken + dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now + dependencies[defaults: .standard, key: .isUsingFullAPNs] = true + } + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _ in () } + .eraseToAnyPublisher() + } + + public static func unsubscribeAll( + token: Data, + using dependencies: Dependencies + ) -> AnyPublisher { + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let userAuthMethod: AuthenticationMethod = try Authentication.with( + db, + swarmPublicKey: userSessionId.hexString, + using: dependencies + ) + + return try Network.PushNotification + .preparedUnsubscribe( + token: token, + swarms: [(userSessionId, userAuthMethod)] + .appending(contentsOf: (try? ClosedGroup + .select(.threadId) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .asRequest(of: String.self) + .fetchSet(db)) + .defaulting(to: []) + .map { threadId in + ( + SessionId(.group, hex: threadId), + try Authentication.with( + db, + swarmPublicKey: threadId, + using: dependencies + ) + ) + }), + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + guard response.subResponses.first?.success == true else { return } + + dependencies[defaults: .standard, key: .deviceToken] = nil + } + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _ in () } + .eraseToAnyPublisher() + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift deleted file mode 100644 index 2b4201f5a1..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine -import GRDB -import SessionNetworkingKit -import SessionUtilitiesKit - -// MARK: - KeychainStorage - -public extension KeychainStorage.DataKey { static let pushNotificationEncryptionKey: Self = "PNEncryptionKeyKey" } - -// MARK: - Log.Category - -private extension Log.Category { - static let cat: Log.Category = .create("PushNotificationAPI", defaultLevel: .info) -} - -// MARK: - PushNotificationAPI - -public enum PushNotificationAPI { - internal static let encryptionKeyLength: Int = 32 - private static let maxRetryCount: Int = 4 - private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) - - public static let server: String = "https://push.getsession.org" - public static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" - - // MARK: - Batch Requests - - public static func subscribeAll( - token: Data, - isForcedUpdate: Bool, - using dependencies: Dependencies - ) -> AnyPublisher { - let hexEncodedToken: String = token.toHexString() - let oldToken: String? = dependencies[defaults: .standard, key: .deviceToken] - let lastUploadTime: Double = dependencies[defaults: .standard, key: .lastDeviceTokenUpload] - let now: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { - Log.info(.cat, "Device token hasn't changed or expired; no need to re-upload.") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedSubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: try ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .filter(ClosedGroup.Columns.shouldPoll) - .asRequest(of: String.self) - .fetchSet(db) - .map { SessionId(.group, hex: $0) } - ), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken - dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now - dependencies[defaults: .standard, key: .isUsingFullAPNs] = true - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() - } - - public static func unsubscribeAll( - token: Data, - using dependencies: Dependencies - ) -> AnyPublisher { - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedUnsubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: (try? ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .asRequest(of: String.self) - .fetchSet(db)) - .defaulting(to: []) - .map { SessionId(.group, hex: $0) }), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = nil - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() - } - - // MARK: - Prepared Requests - - public static func preparedSubscribe( - _ db: ObservingDatabase, - token: Data, - sessionIds: [SessionId], - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { - throw NetworkError.invalidPreparedRequest - } - - guard let notificationsEncryptionKey: Data = try? dependencies[singleton: .keychain].getOrGenerateEncryptionKey( - forKey: .pushNotificationEncryptionKey, - length: encryptionKeyLength, - cat: .cat, - legacyKey: "PNEncryptionKeyKey", - legacyService: "PNKeyChainService" - ) else { - Log.error(.cat, "Unable to retrieve PN encryption key.") - throw KeychainStorageError.keySpecInvalid - } - - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.subscribe, - body: SubscribeRequest( - subscriptions: sessionIds.map { sessionId -> SubscribeRequest.Subscription in - SubscribeRequest.Subscription( - namespaces: { - switch sessionId.prefix { - case .group: return [ - .groupMessages, - .configGroupKeys, - .configGroupInfo, - .configGroupMembers, - .revokedRetrievableGroupMessages - ] - default: return [ - .default, - .configUserProfile, - .configContacts, - .configConvoInfoVolatile, - .configUserGroups - ] - } - }(), - /// Note: Unfortunately we always need the message content because without the content - /// control messages can't be distinguished from visible messages which results in the - /// 'generic' notification being shown when receiving things like typing indicator updates - includeMessageData: true, - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - notificationsEncryptionKey: notificationsEncryptionKey, - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } - ) - ), - responseType: SubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't subscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't subscribe for push notifications due to error: \(error).") - } - } - ) - } - - public static func preparedUnsubscribe( - _ db: ObservingDatabase, - token: Data, - sessionIds: [SessionId], - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.unsubscribe, - body: UnsubscribeRequest( - subscriptions: sessionIds.map { sessionId -> UnsubscribeRequest.Subscription in - UnsubscribeRequest.Subscription( - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } - ) - ), - responseType: UnsubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't unsubscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't unsubscribe for push notifications due to error: \(error).") - } - } - ) - } - - // MARK: - Notification Handling - - public static func processNotification( - notificationContent: UNNotificationContent, - using dependencies: Dependencies - ) -> (data: Data?, metadata: NotificationMetadata, result: ProcessResult) { - // Make sure the notification is from the updated push server - guard notificationContent.userInfo["spns"] != nil else { - return (nil, .invalid, .legacyFailure) - } - - guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else { - return (nil, .invalid, .failureNoContent) - } - - // Decrypt and decode the payload - let notification: BencodeResponse - - do { - guard let encryptedData: Data = Data(base64Encoded: base64EncodedEncString) else { - throw CryptoError.invalidBase64EncodedData - } - - let notificationsEncryptionKey: Data = try dependencies[singleton: .keychain].getOrGenerateEncryptionKey( - forKey: .pushNotificationEncryptionKey, - length: encryptionKeyLength, - cat: .cat, - legacyKey: "PNEncryptionKeyKey", - legacyService: "PNKeyChainService" - ) - let decryptedData: Data = try dependencies[singleton: .crypto].tryGenerate( - .plaintextWithPushNotificationPayload( - payload: encryptedData, - encKey: notificationsEncryptionKey - ) - ) - notification = try BencodeDecoder(using: dependencies) - .decode(BencodeResponse.self, from: decryptedData) - } - catch { - Log.error(.cat, "Failed to decrypt or decode notification due to error: \(error)") - return (nil, .invalid, .failure) - } - - // If the metadata says that the message was too large then we should show the generic - // notification (this is a valid case) - guard !notification.info.dataTooLong else { return (nil, notification.info, .successTooLong) } - - // Check that the body we were given is valid and not empty - guard - let notificationData: Data = notification.data, - notification.info.dataLength == notificationData.count, - !notificationData.isEmpty - else { - Log.error(.cat, "Get notification data failed") - return (nil, notification.info, .failureNoContent) - } - - // Success, we have the notification content - return (notificationData, notification.info, .success) - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 39b2d0956f..40477f59f3 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -20,7 +20,7 @@ public extension Cache { // MARK: - CommunityPollerType public protocol CommunityPollerType { - typealias PollResponse = (info: ResponseInfoType, data: Network.BatchResponseMap) + typealias PollResponse = (info: ResponseInfoType, data: Network.BatchResponseMap) var isPolling: Bool { get } var receivedPollResponse: AnyPublisher { get } @@ -31,14 +31,13 @@ public protocol CommunityPollerType { // MARK: - CommunityPoller -private typealias Capabilities = OpenGroupAPI.Capabilities +private typealias Capabilities = Network.SOGS.CapabilitiesResponse public final class CommunityPoller: CommunityPollerType & PollerType { // MARK: - Settings private static let minPollInterval: TimeInterval = 3 private static let maxPollInterval: TimeInterval = (60 * 60) - internal static let maxInactivityPeriod: TimeInterval = (14 * 24 * 60 * 60) /// If there are hidden rooms that we poll and they fail too many times we want to prune them (as it likely means they no longer /// exist, and since they are already hidden it's unlikely that the user will notice that we stopped polling for them) @@ -75,7 +74,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject = .alwaysRandom, - namespaces: [SnodeAPI.Namespace] = [], + namespaces: [Network.SnodeAPI.Namespace] = [], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, @@ -217,13 +216,13 @@ public final class CommunityPoller: CommunityPollerType & PollerType { .subscribe(on: pollerQueue, using: dependencies) .receive(on: pollerQueue, using: dependencies) .tryMap { [dependencies] authMethod in - try OpenGroupAPI.preparedCapabilities( + try Network.SOGS.preparedCapabilities( authMethod: authMethod, using: dependencies ) } .flatMap { [dependencies] in $0.send(using: dependencies) } - .flatMapStorageWritePublisher(using: dependencies) { [pollerDestination] (db: ObservingDatabase, response: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities)) in + .flatMapStorageWritePublisher(using: dependencies) { [pollerDestination] (db: ObservingDatabase, response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse)) in OpenGroupManager.handleCapabilities( db, capabilities: response.data, @@ -260,7 +259,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) public func poll(forceSynchronousProcessing: Bool = false) -> AnyPublisher { typealias PollInfo = ( - roomInfo: [OpenGroupAPI.RoomInfo], + roomInfo: [Network.SOGS.PollRoomInfo], lastInboxMessageId: Int64, lastOutboxMessageId: Int64, authMethod: AuthenticationMethod @@ -276,15 +275,15 @@ public final class CommunityPoller: CommunityPollerType & PollerType { .readPublisher { [pollerDestination, dependencies] db -> PollInfo in /// **Note:** The `OpenGroup` type converts to lowercase in init let server: String = pollerDestination.target.lowercased() - let roomInfo: [OpenGroupAPI.RoomInfo] = try OpenGroup + let roomInfo: [Network.SOGS.PollRoomInfo] = try OpenGroup .select(.roomToken, .infoUpdates, .sequenceNumber) .filter(OpenGroup.Columns.server == server) .filter(OpenGroup.Columns.isActive == true) .filter(OpenGroup.Columns.roomToken != "") - .asRequest(of: OpenGroupAPI.RoomInfo.self) + .asRequest(of: Network.SOGS.PollRoomInfo.self) .fetchAll(db) - guard !roomInfo.isEmpty else { throw OpenGroupAPIError.invalidPoll } + guard !roomInfo.isEmpty else { throw SOGSError.invalidPoll } return ( roomInfo, @@ -303,12 +302,15 @@ public final class CommunityPoller: CommunityPollerType & PollerType { try Authentication.with(db, server: server, using: dependencies) ) } - .tryFlatMap { [pollCount, dependencies] pollInfo -> AnyPublisher<(ResponseInfoType, Network.BatchResponseMap), Error> in - try OpenGroupAPI + .tryFlatMap { [pollCount, dependencies] pollInfo -> AnyPublisher<(ResponseInfoType, Network.BatchResponseMap), Error> in + try Network.SOGS .preparedPoll( roomInfo: pollInfo.roomInfo, lastInboxMessageId: pollInfo.lastInboxMessageId, lastOutboxMessageId: pollInfo.lastOutboxMessageId, + checkForCommunityMessageRequests: dependencies.mutate(cache: .libSession) { + $0.get(.checkForCommunityMessageRequests) + }, hasPerformedInitialPoll: (pollCount > 0), timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp), authMethod: pollInfo.authMethod, @@ -340,16 +342,16 @@ public final class CommunityPoller: CommunityPollerType & PollerType { private func handlePollResponse( info: ResponseInfoType, - response: Network.BatchResponseMap, + response: Network.BatchResponseMap, failureCount: Int, using dependencies: Dependencies ) -> AnyPublisher { var rawMessageCount: Int = 0 - let validResponses: [OpenGroupAPI.Endpoint: Any] = response.data + let validResponses: [Network.SOGS.Endpoint: Any] = response.data .filter { endpoint, data in switch endpoint { case .capabilities: - guard (data as? Network.BatchSubResponse)?.body != nil else { + guard (data as? Network.BatchSubResponse)?.body != nil else { Log.error(.poller, "\(pollerName) failed due to invalid capability data.") return false } @@ -357,8 +359,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { return true case .roomPollInfo(let roomToken, _): - guard (data as? Network.BatchSubResponse)?.body != nil else { - switch (data as? Network.BatchSubResponse)?.code { + guard (data as? Network.BatchSubResponse)?.body != nil else { + switch (data as? Network.BatchSubResponse)?.code { case 404: Log.error(.poller, "\(pollerName) failed to retrieve info for unknown room '\(roomToken)'.") default: Log.error(.poller, "\(pollerName) failed due to invalid room info data.") } @@ -369,17 +371,17 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, - let responseBody: [Failable] = responseData.body + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, + let responseBody: [Failable] = responseData.body else { - switch (data as? Network.BatchSubResponse<[Failable]>)?.code { + switch (data as? Network.BatchSubResponse<[Failable]>)?.code { case 404: Log.error(.poller, "\(pollerName) failed to retrieve messages for unknown room '\(roomToken)'.") default: Log.error(.poller, "\(pollerName) failed due to invalid messages data.") } return false } - let successfulMessages: [OpenGroupAPI.Message] = responseBody.compactMap { $0.value } + let successfulMessages: [Network.SOGS.Message] = responseBody.compactMap { $0.value } rawMessageCount += successfulMessages.count if successfulMessages.count != responseBody.count { @@ -392,7 +394,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, + let responseData: Network.BatchSubResponse<[Network.SOGS.DirectMessage]?> = data as? Network.BatchSubResponse<[Network.SOGS.DirectMessage]?>, !responseData.failedToParseBody else { Log.error(.poller, "\(pollerName) failed due to invalid inbox/outbox data.") @@ -400,7 +402,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { } // Double optional because the server can return a `304` with an empty body - let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) + let messages: [Network.SOGS.DirectMessage] = ((responseData.body ?? []) ?? []) rawMessageCount += messages.count return !messages.isEmpty @@ -428,18 +430,18 @@ public final class CommunityPoller: CommunityPollerType & PollerType { } return dependencies[singleton: .storage] - .readPublisher { [pollerDestination] db -> (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) in + .readPublisher { [pollerDestination] db -> (capabilities: Network.SOGS.CapabilitiesResponse, groups: [OpenGroup]) in let allCapabilities: [Capability] = try Capability .filter(Capability.Columns.openGroupServer == pollerDestination.target) .fetchAll(db) - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( capabilities: allCapabilities .filter { !$0.isMissing } - .map { $0.variant }, + .map { $0.variant.rawValue }, missing: { - let missingCapabilities: [Capability.Variant] = allCapabilities + let missingCapabilities: [String] = allCapabilities .filter { $0.isMissing } - .map { $0.variant } + .map { $0.variant.rawValue } return (missingCapabilities.isEmpty ? nil : missingCapabilities) }() @@ -452,22 +454,22 @@ public final class CommunityPoller: CommunityPollerType & PollerType { return (capabilities, groups) } - .flatMap { [pollerDestination, dependencies] (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) -> AnyPublisher in - let changedResponses: [OpenGroupAPI.Endpoint: Any] = validResponses + .flatMap { [pollerDestination, dependencies] (capabilities: Network.SOGS.CapabilitiesResponse, groups: [OpenGroup]) -> AnyPublisher in + let changedResponses: [Network.SOGS.Endpoint: Any] = validResponses .filter { endpoint, data in switch endpoint { case .capabilities: guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.CapabilitiesResponse = responseData.body else { return false } return (responseBody != capabilities) case .roomPollInfo(let roomToken, _): guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.RoomPollInfo = responseData.body else { return false } guard let existingOpenGroup: OpenGroup = groups.first(where: { $0.roomToken == roomToken }) else { return true @@ -507,8 +509,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { switch endpoint { case .capabilities: guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.CapabilitiesResponse = responseData.body else { return } OpenGroupManager.handleCapabilities( @@ -519,8 +521,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomPollInfo(let roomToken, _): guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.RoomPollInfo = responseData.body else { return } try OpenGroupManager.handlePollInfo( @@ -534,8 +536,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, - let responseBody: [Failable] = responseData.body + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, + let responseBody: [Failable] = responseData.body else { return } interactionInfo.append( @@ -550,12 +552,12 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, + let responseData: Network.BatchSubResponse<[Network.SOGS.DirectMessage]?> = data as? Network.BatchSubResponse<[Network.SOGS.DirectMessage]?>, !responseData.failedToParseBody else { return } // Double optional because the server can return a `304` with an empty body - let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) + let messages: [Network.SOGS.DirectMessage] = ((responseData.body ?? []) ?? []) let fromOutbox: Bool = { switch endpoint { case .outbox, .outboxSince: return true @@ -734,4 +736,4 @@ public extension CommunityPollerCacheType { // MARK: - Conformance -extension OpenGroupAPI.RoomInfo: FetchableRecord {} +extension Network.SOGS.PollRoomInfo: @retroactive FetchableRecord {} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 804ed95182..57c63b8be8 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -33,7 +33,7 @@ public extension Singleton { // MARK: - CurrentUserPoller public final class CurrentUserPoller: SwarmPoller { - public static let namespaces: [SnodeAPI.Namespace] = [ + public static let namespaces: [Network.SnodeAPI.Namespace] = [ .default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups ] private let pollInterval: TimeInterval = 1.5 diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 1ec82b4124..7e816d4a4f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -23,7 +23,7 @@ public final class GroupPoller: SwarmPoller { private let minPollInterval: Double = 3 private let maxPollInterval: Double = 30 - public static func namespaces(swarmPublicKey: String) -> [SnodeAPI.Namespace] { + public static func namespaces(swarmPublicKey: String) -> [Network.SnodeAPI.Namespace] { guard (try? SessionId.Prefix(from: swarmPublicKey)) == .group else { return [.legacyClosedGroup] } @@ -62,7 +62,7 @@ public final class GroupPoller: SwarmPoller { .flatMap { [receivedPollResponse] _ in receivedPollResponse } .first() .map { $0.filter { $0.isConfigMessage } } - .filter { !$0.contains(where: { $0.namespace == SnodeAPI.Namespace.configGroupKeys }) } + .filter { !$0.contains(where: { $0.namespace == Network.SnodeAPI.Namespace.configGroupKeys }) } .sinkUntilComplete( receiveValue: { [pollerDestination, pollerName, dependencies] configMessages in Log.error(.poller, "\(pollerName) received no config messages in it's first poll, flagging as expired.") diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 91ef8cf1b9..c3e16e5fc5 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -64,7 +64,7 @@ public protocol PollerType: AnyObject { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index 2f3ca1906a..cfb327b06e 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -41,7 +41,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { public var lastPollStart: TimeInterval = 0 public var cancellable: AnyCancellable? - private let namespaces: [SnodeAPI.Namespace] + private let namespaces: [Network.SnodeAPI.Namespace] private let customAuthMethod: AuthenticationMethod? private let shouldStoreMessages: Bool private let receivedPollResponseSubject: PassthroughSubject = PassthroughSubject() @@ -53,7 +53,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int = 0, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, @@ -108,8 +108,8 @@ public class SwarmPoller: SwarmPollerType & PollerType { /// Fetch the messages return dependencies[singleton: .network] .getSwarm(for: pollerDestination.target) - .tryFlatMapWithRandomSnode(drainBehaviour: _pollerDrainBehaviour, using: dependencies) { [pollerDestination, customAuthMethod, namespaces, dependencies] snode -> AnyPublisher<(LibSession.Snode, Network.PreparedRequest), Error> in - dependencies[singleton: .storage].readPublisher { db -> (LibSession.Snode, Network.PreparedRequest) in + .tryFlatMapWithRandomSnode(drainBehaviour: _pollerDrainBehaviour, using: dependencies) { [pollerDestination, customAuthMethod, namespaces, dependencies] snode -> AnyPublisher<(LibSession.Snode, Network.PreparedRequest), Error> in + dependencies[singleton: .storage].readPublisher { db -> (LibSession.Snode, Network.PreparedRequest) in let authMethod: AuthenticationMethod = try (customAuthMethod ?? Authentication.with( db, swarmPublicKey: pollerDestination.target, @@ -118,7 +118,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { return ( snode, - try SnodeAPI.preparedPoll( + try Network.SnodeAPI.preparedPoll( db, namespaces: namespaces, refreshingConfigHashes: activeHashes, @@ -134,10 +134,10 @@ public class SwarmPoller: SwarmPollerType & PollerType { .map { _, response in (snode, response) } } .flatMapStorageWritePublisher(using: dependencies, updates: { [pollerDestination, shouldStoreMessages, forceSynchronousProcessing, dependencies] db, info -> ([Job], [Job], PollResult) in - let (snode, namespacedResults): (LibSession.Snode, SnodeAPI.PollResponse) = info + let (snode, namespacedResults): (LibSession.Snode, Network.SnodeAPI.PollResponse) = info /// Get all of the messages and sort them by their required `processingOrder` - typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + typealias MessageData = (namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) let sortedMessages: [MessageData] = namespacedResults .compactMap { namespace, result -> MessageData? in (result.data?.messages).map { (namespace, $0, result.data?.lastHash) } @@ -233,7 +233,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { shouldStoreMessages: Bool, ignoreDedupeFiles: Bool, forceSynchronousProcessing: Bool, - sortedMessages: [(namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?)], + sortedMessages: [(namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?)], using dependencies: Dependencies ) -> ([Job], [Job], PollResult) { /// No need to do anything if there are no messages diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index c67a3437eb..b56a2a423a 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -415,7 +415,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { unsendRequestChunk in .preparedRequest( - try SnodeAPI.preparedBatch( + try Network.SnodeAPI.preparedBatch( requests: unsendRequestChunk, requireAllBatchResponses: false, swarmPublicKey: threadData.threadId, @@ -426,7 +426,7 @@ public extension MessageViewModel.DeletionBehaviours { ) .appending(serverHashes.isEmpty ? nil : .preparedRequest( - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(serverHashes), requireSuccessfulDeletion: false, authMethod: try Authentication.with( @@ -496,7 +496,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { unsendRequestChunk in .preparedRequest( - try SnodeAPI.preparedBatch( + try Network.SnodeAPI.preparedBatch( requests: unsendRequestChunk, requireAllBatchResponses: false, swarmPublicKey: threadData.threadId, @@ -616,7 +616,7 @@ public extension MessageViewModel.DeletionBehaviours { ) ) .appending(serverHashes.isEmpty ? nil : - .preparedRequest(try SnodeAPI + .preparedRequest(try Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(serverHashes), requireSuccessfulDeletion: false, @@ -658,7 +658,7 @@ public extension MessageViewModel.DeletionBehaviours { let deleteRequests: [Network.PreparedRequest] = try cellViewModels .compactMap { $0.openGroupServerMessageId } .map { messageId in - try OpenGroupAPI.preparedMessageDelete( + try Network.SOGS.preparedMessageDelete( id: messageId, roomToken: roomToken, authMethod: authMethod, @@ -674,7 +674,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { deleteRequestsChunk in .preparedRequest( - try OpenGroupAPI.preparedBatch( + try Network.SOGS.preparedBatch( requests: deleteRequestsChunk, authMethod: authMethod, using: dependencies diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 745e1e4418..d51fb9fd78 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -78,38 +78,6 @@ public extension Authentication { } } } - - /// Used when interacting with a community - struct community: AuthenticationMethod { - public let openGroupCapabilityInfo: LibSession.OpenGroupCapabilityInfo - public let forceBlinded: Bool - - public var server: String { openGroupCapabilityInfo.server } - public var publicKey: String { openGroupCapabilityInfo.publicKey } - public var hasCapabilities: Bool { !openGroupCapabilityInfo.capabilities.isEmpty } - public var supportsBlinding: Bool { openGroupCapabilityInfo.capabilities.contains(.blind) } - - public var info: Info { - .community( - server: server, - publicKey: publicKey, - hasCapabilities: hasCapabilities, - supportsBlinding: supportsBlinding, - forceBlinded: forceBlinded - ) - } - - public init(info: LibSession.OpenGroupCapabilityInfo, forceBlinded: Bool = false) { - self.openGroupCapabilityInfo = info - self.forceBlinded = forceBlinded - } - - // MARK: - SignatureGenerator - - public func generateSignature(with verificationBytes: [UInt8], using dependencies: Dependencies) throws -> Authentication.Signature { - throw CryptoError.signatureGenerationFailed - } - } } // MARK: - Convenience @@ -119,6 +87,19 @@ fileprivate struct GroupAuthData: Codable, FetchableRecord { let authData: Data? } +public extension Authentication.community { + init(info: LibSession.OpenGroupCapabilityInfo, forceBlinded: Bool = false) { + self.init( + roomToken: info.roomToken, + server: info.server, + publicKey: info.publicKey, + hasCapabilities: !info.capabilities.isEmpty, + supportsBlinding: info.capabilities.contains(.blind), + forceBlinded: forceBlinded + ) + } +} + public extension Authentication { static func with( _ db: ObservingDatabase, diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index ed44a08963..99d5314260 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -738,7 +738,7 @@ public class ExtensionHelper: ExtensionHelperType { } public func loadMessages() async throws { - typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + typealias MessageData = (namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) /// Retrieve all conversation file paths /// @@ -781,7 +781,7 @@ public class ExtensionHelper: ExtensionHelperType { do { let sortedMessages: [MessageData] = try configMessageHashes - .reduce([SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [SnodeAPI.Namespace: [SnodeReceivedMessage]], hash: String) in + .reduce([Network.SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [Network.SnodeAPI.Namespace: [SnodeReceivedMessage]], hash: String) in let path: String = URL(fileURLWithPath: this.conversationsPath) .appendingPathComponent(conversationHash) .appendingPathComponent(this.conversationConfigDir) diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 130facfd6c..8cdfca3459 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -541,7 +541,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { ) ) let expectedRequest: Network.PreparedRequest = mockStorage.read { db in - try OpenGroupAPI.preparedDownload( + try Network.SOGS.preparedDownload( fileId: "12", roomToken: "testRoom", authMethod: Authentication.community( diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 79692802c7..1a0770edc9 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -42,17 +42,22 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { MockNetwork.batchResponseData( with: [ ( - OpenGroupAPI.Endpoint.capabilities, - OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() + Network.SOGS.Endpoint.capabilities, + Network.SOGS.CapabilitiesResponse( + capabilities: [ + Capability.Variant.blind.rawValue, + Capability.Variant.reactions.rawValue + ] + ).batchSubResponse() ), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2", infoUpdates: 12, @@ -191,9 +196,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(1)) - expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer])) + expect(openGroups?.map { $0.server }).to(equal([Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal([""])) - expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey])) + expect(openGroups?.map { $0.publicKey }).to(equal([Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false])) expect(openGroups?.map { $0.name }).to(equal([""])) } @@ -206,9 +211,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -228,9 +233,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(1)) - expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer])) + expect(openGroups?.map { $0.server }).to(equal([Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal([""])) - expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey])) + expect(openGroups?.map { $0.publicKey }).to(equal([Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false])) expect(openGroups?.map { $0.name }).to(equal(["TestExisting"])) } @@ -239,9 +244,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("sends the correct request") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -249,13 +254,13 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ) .insert(db) } - let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( info: LibSession.OpenGroupCapabilityInfo( roomToken: "", - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, capabilities: [] ), forceBlinded: false @@ -322,7 +327,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let capabilities: [Capability]? = mockStorage.read { db in try Capability.fetchAll(db) } expect(capabilities?.count).to(equal(2)) expect(capabilities?.map { $0.openGroupServer }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(capabilities?.map { $0.variant }).to(equal([.blind, .reactions])) expect(capabilities?.map { $0.isMissing }).to(equal([false, false])) } @@ -341,13 +346,13 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(3)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal(["", "testRoom", "testRoom2"])) expect(openGroups?.map { $0.publicKey }) .to(equal([ - OpenGroupAPI.defaultServerPublicKey, - OpenGroupAPI.defaultServerPublicKey, - OpenGroupAPI.defaultServerPublicKey + Network.SOGS.defaultServerPublicKey, + Network.SOGS.defaultServerPublicKey, + Network.SOGS.defaultServerPublicKey ])) expect(openGroups?.map { $0.isActive }).to(equal([false, false, false])) expect(openGroups?.map { $0.name }).to(equal(["", "TestRoomName", "TestRoomName2"])) @@ -357,9 +362,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("does not override existing rooms that were returned") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -372,15 +377,15 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .thenReturn( MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( Network.BatchSubResponse( code: 200, headers: [:], body: [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestReplacementName" ) @@ -405,10 +410,10 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(2)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }.sorted()).to(equal(["", "testRoom"])) expect(openGroups?.map { $0.publicKey }) - .to(equal([OpenGroupAPI.defaultServerPublicKey, OpenGroupAPI.defaultServerPublicKey])) + .to(equal([Network.SOGS.defaultServerPublicKey, Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false, false])) expect(openGroups?.map { $0.name }.sorted()).to(equal(["", "TestExisting"])) } @@ -435,7 +440,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer ), timestamp: 1234567890 ) @@ -450,9 +455,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("schedules a display picture download if the imageId has changed") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", imageId: "10", @@ -482,7 +487,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer ), timestamp: 1234567890 ) @@ -501,17 +506,22 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { MockNetwork.batchResponseData( with: [ ( - OpenGroupAPI.Endpoint.capabilities, - OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() + Network.SOGS.Endpoint.capabilities, + Network.SOGS.CapabilitiesResponse( + capabilities: [ + Capability.Variant.blind.rawValue, + Capability.Variant.reactions.rawValue + ] + ).batchSubResponse() ), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2" ) @@ -538,9 +548,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("does not schedule a display picture download if the imageId matches and the image has already been downloaded") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", imageId: "12", @@ -579,14 +589,14 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .toNot(call(matchingParameters: .all) { $0.setDefaultRoomInfo([ ( - room: OpenGroupAPI.Room.mock.with( + room: Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), openGroup: OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestRoomName", userCount: 0, @@ -594,16 +604,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ) ), ( - room: OpenGroupAPI.Room.mock.with( + room: Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2", infoUpdates: 12, imageId: "12" ), openGroup: OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestRoomName2", imageId: "12", diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 309aa452d8..209274b6fd 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -882,7 +882,7 @@ class LibSessionGroupInfoSpec: QuickSpec { ) } - let expectedRequest: Network.PreparedRequest<[String: Bool]> = try SnodeAPI.preparedDeleteMessages( + let expectedRequest: Network.PreparedRequest<[String: Bool]> = try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["1234"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift similarity index 61% rename from SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift rename to SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift index 35b3ba1679..7b04fa35a1 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift @@ -8,7 +8,7 @@ import Nimble @testable import SessionMessagingKit -class CryptoOpenGroupAPISpec: QuickSpec { +class CryptoOpenGroupSpec: QuickSpec { override class func spec() { // MARK: Configuration @@ -21,161 +21,8 @@ class CryptoOpenGroupAPISpec: QuickSpec { } ) - // MARK: - Crypto for OpenGroupAPI - describe("Crypto for OpenGroupAPI") { - // MARK: -- when generating a blinded15 key pair - context("when generating a blinded15 key pair") { - // MARK: ---- successfully generates - it("successfully generates") { - let result = crypto.generate( - .blinded15KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ - expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind15PublicKey)) - expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind15SecretKey)) - } - - // MARK: ---- fails if the edKeyPair secret key length wrong - it("fails if the ed25519SecretKey length wrong") { - let result = crypto.generate( - .blinded15KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Array(Data(hex: String(TestConstants.edSecretKey.prefix(4)))) - ) - ) - - expect(result).to(beNil()) - } - } - - // MARK: -- when generating a blinded25 key pair - context("when generating a blinded25 key pair") { - // MARK: ---- successfully generates - it("successfully generates") { - let result = crypto.generate( - .blinded25KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ - expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind25PublicKey)) - expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind25SecretKey)) - } - - // MARK: ---- fails if the edKeyPair secret key length wrong - it("fails if the ed25519SecretKey length wrong") { - let result = crypto.generate( - .blinded25KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes - ) - ) - - expect(result).to(beNil()) - } - } - - // MARK: -- when generating a signatureBlind15 - context("when generating a signatureBlind15") { - // MARK: ---- generates a correct signature - it("generates a correct signature") { - let result = crypto.generate( - .signatureBlind15( - message: "TestMessage".bytes, - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - - expect(result?.toHexString()) - .to(equal( - "245003f1627ebdfc6099c32597d426ef84d1b301861a5ffbbac92dde6c608334" + - "ceb56a022a094a9a664fae034b50eed40bd1bfb262c7e542c979eec265ae3f07" - )) - } - } - - // MARK: -- when generating a signatureBlind25 - context("when generating a signatureBlind25") { - // MARK: ---- generates a correct signature - it("generates a correct signature") { - let result = crypto.generate( - .signatureBlind25( - message: "TestMessage".bytes, - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - expect(result?.toHexString()) - .to(equal( - "9ff9b7fb7d435c7a2c0b0b2ae64963baaf394386b9f7c7f924eeac44ec0f74c7" + - "fe6304c73a9b3a65491f81e44b545e54631e83e9a412eaed5fd4db2e05ec830c" - )) - } - } - - // MARK: -- when checking if a session id matches a blinded id - context("when checking if a session id matches a blinded id") { - // MARK: ---- returns true when a blind15 id matches - it("returns true when a blind15 id matches") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "15\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beTrue()) - } - - // MARK: ---- returns true when a blind25 id matches - it("returns true when a blind25 id matches") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "25\(TestConstants.blind25PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beTrue()) - } - - // MARK: ---- returns false if given an invalid session id - it("returns false if given an invalid session id") { - let result = crypto.verify( - .sessionId( - "AB\(TestConstants.publicKey)", - matchesBlindedId: "15\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beFalse()) - } - - // MARK: ---- returns false if given an invalid blinded id - it("returns false if given an invalid blinded id") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "AB\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beFalse()) - } - } - + // MARK: - Crypto for Open Group + describe("Crypto for Open Group") { // MARK: -- when encrypting with the session blinding protocol context("when encrypting with the session blinding protocol") { // MARK: ---- can encrypt for a blind15 recipient correctly diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift similarity index 73% rename from SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift rename to SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift index 0fc98cf1a8..a3a918bcff 100644 --- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift @@ -7,34 +7,8 @@ import Nimble @testable import SessionMessagingKit -class CapabilitiesSpec: QuickSpec { +class CapabilitySpec: QuickSpec { override class func spec() { - // MARK: - Capabilities - describe("Capabilities") { - // MARK: -- when initializing - context("when initializing") { - // MARK: ---- assigns values correctly - it("assigns values correctly") { - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( - capabilities: [.sogs], - missing: [.sogs] - ) - - expect(capabilities.capabilities).to(equal([.sogs])) - expect(capabilities.missing).to(equal([.sogs])) - } - - it("defaults missing to nil") { - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( - capabilities: [.sogs] - ) - - expect(capabilities.capabilities).to(equal([.sogs])) - expect(capabilities.missing).to(beNil()) - } - } - } - // MARK: - a Capability describe("a Capability") { // MARK: -- when initializing diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 2cd025dc58..f0933e8d92 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -4,13 +4,13 @@ import UIKit import Combine import GRDB import SessionUtil -import SessionNetworkingKit import SessionUtilitiesKit import Quick import Nimble @testable import SessionMessagingKit +@testable import SessionNetworkingKit class OpenGroupManagerSpec: QuickSpec { override class func spec() { @@ -61,12 +61,12 @@ class OpenGroupManagerSpec: QuickSpec { infoUpdates: 10, sequenceNumber: 5 ) - @TestState var testPollInfo: OpenGroupAPI.RoomPollInfo! = OpenGroupAPI.RoomPollInfo.mock.with( + @TestState var testPollInfo: Network.SOGS.RoomPollInfo! = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: .mock ) - @TestState var testMessage: OpenGroupAPI.Message! = OpenGroupAPI.Message( + @TestState var testMessage: Network.SOGS.Message! = Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -92,14 +92,14 @@ class OpenGroupManagerSpec: QuickSpec { base64EncodedSignature: nil, reactions: nil ) - @TestState var testDirectMessage: OpenGroupAPI.DirectMessage! = { + @TestState var testDirectMessage: Network.SOGS.DirectMessage! = { let proto = SNProtoContent.builder() let protoDataBuilder = SNProtoDataMessage.builder() proto.setSigTimestamp(1234567890000) protoDataBuilder.setBody("TestMessage") proto.setDataMessage(try! protoDataBuilder.build()) - return OpenGroupAPI.DirectMessage( + return Network.SOGS.DirectMessage( id: 128, sender: "15\(TestConstants.blind15PublicKey)", recipient: "15\(TestConstants.blind15PublicKey)", @@ -969,7 +969,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -983,7 +983,7 @@ class OpenGroupManagerSpec: QuickSpec { outboxLatestMessageId: 0 ).insert(db) try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom1", publicKey: TestConstants.publicKey, isActive: true, @@ -1004,7 +1004,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try openGroupManager.delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer), skipLibSessionUpdate: true ) } @@ -1018,7 +1018,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try openGroupManager.delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer), skipLibSessionUpdate: true ) } @@ -1027,7 +1027,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.read { db in try OpenGroup .select(.isActive) - .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer)) + .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer)) .asRequest(of: Bool.self) .fetchOne(db) } @@ -1043,7 +1043,10 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager .handleCapabilities( db, - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []), + capabilities: Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"], + missing: [] + ), on: "http://127.0.0.1" ) } @@ -1180,10 +1183,10 @@ class OpenGroupManagerSpec: QuickSpec { context("and updating the moderator list") { // MARK: ------ successfully updates it("successfully updates") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: ["TestMod"], hiddenModerators: [], admins: [], @@ -1227,10 +1230,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ updates for hidden moderators it("updates for hidden moderators") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: ["TestMod2"], admins: [], @@ -1274,7 +1277,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ does not insert mods if no moderators are provided it("does not insert mods if no moderators are provided") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10 ) @@ -1299,10 +1302,10 @@ class OpenGroupManagerSpec: QuickSpec { context("and updating the admin list") { // MARK: ------ successfully updates it("successfully updates") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: [], admins: ["TestAdmin"], @@ -1346,10 +1349,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ updates for hidden admins it("updates for hidden admins") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: [], admins: [], @@ -1393,7 +1396,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ does not insert an admin if no admins are provided it("does not insert an admin if no admins are provided") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: nil @@ -1475,10 +1478,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ schedules a download for the room image it("schedules a download for the room image") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( token: "test", name: "test", imageId: "10" @@ -1543,7 +1546,7 @@ class OpenGroupManagerSpec: QuickSpec { ).insert(db) } - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: nil @@ -1596,10 +1599,10 @@ class OpenGroupManagerSpec: QuickSpec { ).insert(db) } - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( token: "test", name: "test", infoUpdates: 10, @@ -1700,7 +1703,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: nil, posted: 123, @@ -1763,7 +1766,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: nil, posted: 123, @@ -1797,7 +1800,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -1842,7 +1845,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 2, sender: "05\(TestConstants.publicKey)", posted: 122, @@ -1883,7 +1886,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -1913,7 +1916,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -2030,7 +2033,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores messages with non base64 encoded data it("ignores messages with non base64 encoded data") { - testDirectMessage = OpenGroupAPI.DirectMessage( + testDirectMessage = Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2134,7 +2137,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleDirectMessages( db, messages: [ - OpenGroupAPI.DirectMessage( + Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2290,7 +2293,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleDirectMessages( db, messages: [ - OpenGroupAPI.DirectMessage( + Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2526,9 +2529,9 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(true) mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -2536,13 +2539,13 @@ class OpenGroupManagerSpec: QuickSpec { ) .insert(db) } - let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( info: LibSession.OpenGroupCapabilityInfo( roomToken: "", - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, capabilities: [] ), forceBlinded: false @@ -2566,7 +2569,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms it("does not start a job to retrieve the default rooms if we already have rooms") { mockAppGroupDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) - cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)]) + cache.setDefaultRoomInfo([(room: Network.SOGS.Room.mock, openGroup: OpenGroup.mock)]) cache.defaultRoomsPublisher.sinkUntilComplete() expect(mockNetwork) @@ -2579,7 +2582,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - Convenience Extensions -extension OpenGroupAPI.Room { +extension Network.SOGS.Room { func with( token: String? = nil, name: String? = nil, @@ -2589,8 +2592,8 @@ extension OpenGroupAPI.Room { hiddenModerators: [String]? = nil, admins: [String]? = nil, hiddenAdmins: [String]? = nil - ) -> OpenGroupAPI.Room { - return OpenGroupAPI.Room( + ) -> Network.SOGS.Room { + return Network.SOGS.Room( token: (token ?? self.token), name: (name ?? self.name), roomDescription: self.roomDescription, @@ -2620,13 +2623,13 @@ extension OpenGroupAPI.Room { } } -extension OpenGroupAPI.RoomPollInfo { +extension Network.SOGS.RoomPollInfo { func with( token: String? = nil, activeUsers: Int64? = nil, - details: OpenGroupAPI.Room? = .mock - ) -> OpenGroupAPI.RoomPollInfo { - return OpenGroupAPI.RoomPollInfo( + details: Network.SOGS.Room? = .mock + ) -> Network.SOGS.RoomPollInfo { + return Network.SOGS.RoomPollInfo( token: (token ?? self.token), activeUsers: (activeUsers ?? self.activeUsers), admin: self.admin, @@ -2658,133 +2661,49 @@ extension OpenGroup: Mocked { infoUpdates: 0 ) } - -extension OpenGroupAPI.Capabilities: Mocked { - static var mock: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil) -} - -extension OpenGroupAPI.Room: Mocked { - static var mock: OpenGroupAPI.Room = OpenGroupAPI.Room( - token: "test", - name: "testRoom", - roomDescription: nil, - infoUpdates: 1, - messageSequence: 1, - created: 1, - activeUsers: 1, - activeUsersCutoff: 1, - imageId: nil, - pinnedMessages: nil, - admin: false, - globalAdmin: false, - admins: [], - hiddenAdmins: nil, - moderator: false, - globalModerator: false, - moderators: [], - hiddenModerators: nil, - read: true, - defaultRead: nil, - defaultAccessible: nil, - write: true, - defaultWrite: nil, - upload: true, - defaultUpload: nil - ) -} - -extension OpenGroupAPI.RoomPollInfo: Mocked { - static var mock: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo( - token: "test", - activeUsers: 1, - admin: false, - globalAdmin: false, - moderator: false, - globalModerator: false, - read: true, - defaultRead: nil, - defaultAccessible: nil, - write: true, - defaultWrite: nil, - upload: true, - defaultUpload: false, - details: .mock - ) -} - -extension OpenGroupAPI.Message: Mocked { - static var mock: OpenGroupAPI.Message = OpenGroupAPI.Message( - id: 100, - sender: TestConstants.blind15PublicKey, - posted: 1, - edited: nil, - deleted: nil, - seqNo: 1, - whisper: false, - whisperMods: false, - whisperTo: nil, - base64EncodedData: nil, - base64EncodedSignature: nil, - reactions: nil - ) -} - -extension OpenGroupAPI.SendDirectMessageResponse: Mocked { - static var mock: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( - id: 1, - sender: TestConstants.blind15PublicKey, - recipient: "testRecipient", - posted: 1122, - expires: 2233 - ) -} - -extension OpenGroupAPI.DirectMessage: Mocked { - static var mock: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage( - id: 101, - sender: TestConstants.blind15PublicKey, - recipient: "testRecipient", - posted: 1212, - expires: 2323, - base64EncodedMessage: "TestMessage".data(using: .utf8)!.base64EncodedString() - ) -} extension Network.BatchResponse { static let mockUnblindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), [OpenGroupAPI.Message].mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomPollInfo("testRoom", 0), Network.SOGS.RoomPollInfo.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomMessagesRecent("testRoom"), [Network.SOGS.Message].mockBatchSubResponse()) ] ) static let mockBlindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), OpenGroupAPI.Message.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.inboxSince(id: 0), OpenGroupAPI.DirectMessage.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.outboxSince(id: 0), OpenGroupAPI.DirectMessage.self.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomPollInfo("testRoom", 0), Network.SOGS.RoomPollInfo.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomMessagesRecent("testRoom"), Network.SOGS.Message.mockBatchSubResponse()), + (Network.SOGS.Endpoint.inboxSince(id: 0), Network.SOGS.DirectMessage.mockBatchSubResponse()), + (Network.SOGS.Endpoint.outboxSince(id: 0), Network.SOGS.DirectMessage.self.mockBatchSubResponse()) ] ) static let mockCapabilitiesResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()) ] ) static let mockRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockBanAndDeleteAllResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomDeleteMessages("testRoon", sessionId: ""), NoResponse.mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomDeleteMessages("testRoon", sessionId: ""), NoResponse.mockBatchSubResponse()) + ] + ) + + static let mockCapabilitiesAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( + with: [ + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift deleted file mode 100644 index 7dba9ab779..0000000000 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -import Quick -import Nimble - -@testable import SessionMessagingKit - -class SOGSErrorSpec: QuickSpec { - override class func spec() { - // MARK: - a SOGSError - describe("a SOGSError") { - // MARK: -- generates the error description correctly - it("generates the error description correctly") { - expect(OpenGroupAPIError.decryptionFailed.description).to(equal("Couldn't decrypt response.")) - expect(OpenGroupAPIError.signingFailed.description).to(equal("Couldn't sign message.")) - expect(OpenGroupAPIError.noPublicKey.description).to(equal("Couldn't find server public key.")) - expect(OpenGroupAPIError.invalidEmoji.description).to(equal("The emoji is invalid.")) - expect(OpenGroupAPIError.invalidPoll.description).to(equal("Poller in invalid state.")) - } - } - } -} diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index f731e18ecf..e42508adee 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -138,7 +138,7 @@ class MessageReceiverGroupsSpec: QuickSpec { .thenReturn(Data([1, 2, 3])) keychain .when { try $0.data(forKey: .pushNotificationEncryptionKey) } - .thenReturn(Data((0.. = mockStorage.write { db in + let expectedRequest: Network.PreparedRequest = mockStorage.write { db in _ = try SessionThread.upsert( db, id: groupId.hexString, @@ -856,10 +856,17 @@ class MessageReceiverGroupsSpec: QuickSpec { groupIdentityPrivateKey: groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ) + ) + ], using: dependencies ) @@ -910,7 +917,7 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -------- subscribes for push notifications it("subscribes for push notifications") { - let expectedRequest: Network.PreparedRequest = mockStorage.write { db in + let expectedRequest: Network.PreparedRequest = mockStorage.write { db in _ = try SessionThread.upsert( db, id: groupId.hexString, @@ -929,10 +936,17 @@ class MessageReceiverGroupsSpec: QuickSpec { authData: inviteMessage.memberAuthData, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data(hex: Data([5, 4, 3, 2, 1]).toHexString()), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupMember( + groupSessionId: groupId, + authData: inviteMessage.memberAuthData + ) + ) + ], using: dependencies ) @@ -2821,7 +2835,7 @@ class MessageReceiverGroupsSpec: QuickSpec { deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" deleteContentMessage.sentTimestampMs = 1234567800000 - let preparedRequest: Network.PreparedRequest<[String: Bool]> = try! SnodeAPI + let preparedRequest: Network.PreparedRequest<[String: Bool]> = try! Network.SnodeAPI .preparedDeleteMessages( serverHashes: ["TestMessageHash3"], requireSuccessfulDeletion: false, @@ -3092,11 +3106,18 @@ class MessageReceiverGroupsSpec: QuickSpec { .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) - let expectedRequest: Network.PreparedRequest = mockStorage.read { db in - try PushNotificationAPI.preparedUnsubscribe( - db, + let expectedRequest: Network.PreparedRequest = mockStorage.read { db in + try Network.PushNotification.preparedUnsubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupMember( + groupSessionId: groupId, + authData: Data([1, 2, 3]) + ) + ) + ], using: dependencies ) }! diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 69d1a6d505..ddebdba938 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -159,7 +159,7 @@ class MessageSenderGroupsSpec: QuickSpec { .thenReturn(Data([1, 2, 3])) keychain .when { try $0.data(forKey: .pushNotificationEncryptionKey) } - .thenReturn(Data((0.. = try SnodeAPI.preparedSequence( + let preparedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [ - try SnodeAPI + try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: groupId.hexString, @@ -731,7 +731,7 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ------ and trying to subscribe for push notifications context("and trying to subscribe for push notifications") { - @TestState var expectedRequest: Network.PreparedRequest! + @TestState var expectedRequest: Network.PreparedRequest! beforeEach { // Need to set `isUsingFullAPNs` to true to generate the `expectedRequest` @@ -760,10 +760,17 @@ class MessageSenderGroupsSpec: QuickSpec { groupIdentityPrivateKey: groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ) + ) + ], using: dependencies ) @@ -1024,9 +1031,9 @@ class MessageSenderGroupsSpec: QuickSpec { "LPczVOFKOPs+rrB3aUpMsNUnJHOEhW9g6zi/UPjuCWTnnvpxlMTpHaTFlMTp+NjQ6dKi86jZJ" + "l3oiJEA5h5pBE5oOJHQNvtF8GOcsYwrIFTZKnI7AGkBSu1TxP0xLWwTUzjOGMgmKvlIgkQ6e9" + "r3JBmU=" - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( + let expectedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [] - .appending(try SnodeAPI.preparedUnrevokeSubaccounts( + .appending(try Network.SnodeAPI.preparedUnrevokeSubaccounts( subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], authMethod: Authentication.groupAdmin( groupSessionId: groupId, @@ -1034,7 +1041,7 @@ class MessageSenderGroupsSpec: QuickSpec { ), using: dependencies )) - .appending(try SnodeAPI.preparedSendMessage( + .appending(try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: groupId.hexString, data: Data(base64Encoded: requestDataString)!, @@ -1048,7 +1055,7 @@ class MessageSenderGroupsSpec: QuickSpec { ), using: dependencies )) - .appending(try SnodeAPI.preparedDeleteMessages( + .appending(try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["testHash"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( @@ -1239,9 +1246,9 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ---- includes the unrevoke subaccounts as part of the config sync sequence it("includes the unrevoke subaccounts as part of the config sync sequence") { - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( + let expectedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [] - .appending(try SnodeAPI + .appending(try Network.SnodeAPI .preparedUnrevokeSubaccounts( subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], authMethod: Authentication.groupAdmin( @@ -1251,7 +1258,7 @@ class MessageSenderGroupsSpec: QuickSpec { using: dependencies ) ) - .appending(try SnodeAPI.preparedDeleteMessages( + .appending(try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["testHash"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( @@ -1472,25 +1479,25 @@ extension Network.BatchResponse { fileprivate static let mockConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) fileprivate static let mockAddMemberConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) fileprivate static let mockAddMemberHistoricConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index a7db4da5de..191d60c4d0 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -11,7 +11,7 @@ class MockOGMCache: Mock, OGMCacheType { mock() } - var pendingChanges: [OpenGroupAPI.PendingChange] { + var pendingChanges: [OpenGroupManager.PendingChange] { get { return mock() } set { mockNoReturn(args: [newValue]) } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index 794fa81829..cfc5968bd2 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -43,7 +43,7 @@ class MockPoller: Mock, PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift index d20f1da875..05af305032 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift @@ -39,7 +39,7 @@ class MockSwarmPoller: Mock, SwarmPollerType & Pol pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift index 94fed80795..dad09fce75 100644 --- a/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift +++ b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift @@ -11,7 +11,7 @@ import SessionUtilitiesKit internal extension Crypto.Generator { static func sessionId( name: String, - response: SnodeAPI.ONSResolveResponse + response: Network.SnodeAPI.ONSResolveResponse ) -> Crypto.Generator { return Crypto.Generator( id: "sessionId_for_ONS_response", @@ -59,84 +59,3 @@ internal extension Crypto.Generator { } } -// MARK: - Version Blinded ID - -public extension Crypto.Generator { - static func versionBlinded07KeyPair( - ed25519SecretKey: [UInt8] - ) -> Crypto.Generator { - return Crypto.Generator( - id: "versionBlinded07KeyPair", - args: [ed25519SecretKey] - ) { - var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) - var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) - var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 64) - - guard - cEd25519SecretKey.count == 64, - session_blind_version_key_pair( - &cEd25519SecretKey, - &cBlindedPubkey, - &cBlindedSeckey - ) - else { throw CryptoError.keyGenerationFailed } - - return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) - } - } - - static func signatureVersionBlind07( - timestamp: UInt64, - method: String, - path: String, - body: String?, - ed25519SecretKey: [UInt8] - ) -> Crypto.Generator<[UInt8]> { - return Crypto.Generator( - id: "signatureVersionBlind07", - args: [timestamp, method, path, body, ed25519SecretKey] - ) { - var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) - guard - cEd25519SecretKey.count == 64, - var cMethod: [CChar] = method.cString(using: .utf8), - var cPath: [CChar] = path.cString(using: .utf8) - else { - throw CryptoError.signatureGenerationFailed - } - - var cSignature: [UInt8] = [UInt8](repeating: 0, count: 64) - - if let body: String = body { - var cBody: [UInt8] = Array(body.bytes) - guard session_blind_version_sign_request( - &cEd25519SecretKey, - timestamp, - &cMethod, - &cPath, - &cBody, - cBody.count, - &cSignature - ) else { - throw CryptoError.signatureGenerationFailed - } - } else { - guard session_blind_version_sign_request( - &cEd25519SecretKey, - timestamp, - &cMethod, - &cPath, - nil, - 0, - &cSignature - ) - else { - throw CryptoError.signatureGenerationFailed - } - } - - return cSignature - } - } -} diff --git a/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift deleted file mode 100644 index a54cbc083f..0000000000 --- a/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import GRDB -import SessionUtilitiesKit - -public struct SnodeReceivedMessageInfo: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { - public static var databaseTableName: String { "snodeReceivedMessageInfo" } - - public typealias Columns = CodingKeys - public enum CodingKeys: String, CodingKey, ColumnExpression { - case swarmPublicKey - case snodeAddress - case namespace - case hash - case expirationDateMs - case wasDeletedOrInvalid - } - - /// The public key for the swarm this message info was retrieved from - public let swarmPublicKey: String - - /// The address for the snode this message info was retrieved from (in the form of `{server}:{port}`) - public let snodeAddress: String - - /// The namespace this message info was retrieved from - public let namespace: Int - - /// The is the hash for the received message - public let hash: String - - /// This is the timestamp (in milliseconds since epoch) when the message hash should expire - /// - /// **Note:** If no value exists this will default to 15 days from now (since the service node caches messages for - /// 14 days for standard messages) - public let expirationDateMs: Int64 - - /// This flag indicates whether the message associated with this message hash was deleted or whether this message - /// hash is potentially invalid (if a poll results in 100% of the `SnodeReceivedMessageInfo` entries being seen as - /// duplicates then we assume that the `lastHash` value provided when retrieving messages was invalid and mark - /// it as such) - /// - /// This flag can also be used to refetch messages from a swarm without impacting the hash-based deduping mechanism - /// as if a hash with this value set to `true` is received when pollig then the value gets reset to `false` - /// - /// **Note:** When retrieving the `lastNotExpired` we will ignore any entries where this flag is `true` - public var wasDeletedOrInvalid: Bool -} - -// MARK: - Convenience - -public extension SnodeReceivedMessageInfo { - init( - snode: LibSession.Snode, - swarmPublicKey: String, - namespace: SnodeAPI.Namespace, - hash: String, - expirationDateMs: Int64? - ) { - self.swarmPublicKey = swarmPublicKey - self.snodeAddress = snode.address - self.namespace = namespace.rawValue - self.hash = hash - self.expirationDateMs = (expirationDateMs ?? 0) - self.wasDeletedOrInvalid = false - } -} - -// MARK: - GRDB Interactions - -public extension SnodeReceivedMessageInfo { - /// This method fetches the last non-expired hash from the database for message retrieval - static func fetchLastNotExpired( - _ db: ObservingDatabase, - for snode: LibSession.Snode, - namespace: SnodeAPI.Namespace, - swarmPublicKey: String, - using dependencies: Dependencies - ) throws -> SnodeReceivedMessageInfo? { - let currentOffsetTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - - return try SnodeReceivedMessageInfo - .filter(SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false) - .filter( - SnodeReceivedMessageInfo.Columns.swarmPublicKey == swarmPublicKey && - SnodeReceivedMessageInfo.Columns.snodeAddress == snode.address && - SnodeReceivedMessageInfo.Columns.namespace == namespace.rawValue - ) - .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > currentOffsetTimestampMs) - .order(Column.rowID.desc) - .fetchOne(db) - } - - /// There are some cases where the latest message can be removed from a swarm, if we then try to poll for that message the swarm - /// will see it as invalid and start returning messages from the beginning which can result in a lot of wasted, duplicate downloads - /// - /// This method should be called when deleting a message, handling an UnsendRequest or when receiving a poll response which contains - /// solely duplicate messages (for the specific service node - if even one message in a response is new for that service node then this shouldn't - /// be called if if the message has already been received and processed by a separate service node) - static func handlePotentialDeletedOrInvalidHash( - _ db: ObservingDatabase, - potentiallyInvalidHashes: [String], - otherKnownValidHashes: [String] = [] - ) throws { - if !potentiallyInvalidHashes.isEmpty { - _ = try SnodeReceivedMessageInfo - .filter(potentiallyInvalidHashes.contains(SnodeReceivedMessageInfo.Columns.hash)) - .updateAll( - db, - SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: true) - ) - } - - // If we have any server hashes which we know are valid (eg. we fetched the oldest messages) then - // mark them all as valid to prevent the case where we just slowly work backwards from the latest - // message, polling for one earlier each time - if !otherKnownValidHashes.isEmpty { - _ = try SnodeReceivedMessageInfo - .filter(otherKnownValidHashes.contains(SnodeReceivedMessageInfo.Columns.hash)) - .updateAll( - db, - SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: false) - ) - } - } - - func storeUpdatedLastHash(_ db: ObservingDatabase) -> Bool { - do { - _ = try self.inserted(db) - return true - } - catch { return false } - } -} diff --git a/SessionNetworkingKit/FileServer/AppVersionResponse.swift b/SessionNetworkingKit/FileServer/AppVersionResponse.swift deleted file mode 100644 index ae5c33739b..0000000000 --- a/SessionNetworkingKit/FileServer/AppVersionResponse.swift +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public class AppVersionResponse: AppVersionInfo { - enum CodingKeys: String, CodingKey { - case prerelease - } - - public let prerelease: AppVersionInfo? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]?, - prerelease: AppVersionInfo? - ) { - self.prerelease = prerelease - - super.init( - version: version, - updated: updated, - name: name, - notes: notes, - assets: assets - ) - } - - required init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(prerelease, forKey: .prerelease) - } -} - -// MARK: - AppVersionInfo - -public class AppVersionInfo: Codable { - enum CodingKeys: String, CodingKey { - case version = "result" - case updated - case name - case notes - case assets - } - - public struct Asset: Codable { - enum CodingKeys: String, CodingKey { - case name - case url - } - - public let name: String - public let url: String - } - - public let version: String - public let updated: TimeInterval? - public let name: String? - public let notes: String? - public let assets: [Asset]? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]? - ) { - self.version = version - self.updated = updated - self.name = name - self.notes = notes - self.assets = assets - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(version, forKey: .version) - try container.encodeIfPresent(updated, forKey: .updated) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(notes, forKey: .notes) - try container.encodeIfPresent(assets, forKey: .assets) - } -} diff --git a/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift b/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift index e69de29bb2..2dced50d25 100644 --- a/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift +++ b/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift @@ -0,0 +1,89 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +// MARK: - Version Blinded ID + +public extension Crypto.Generator { + static func versionBlinded07KeyPair( + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator { + return Crypto.Generator( + id: "versionBlinded07KeyPair", + args: [ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) + var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 64) + + guard + cEd25519SecretKey.count == 64, + session_blind_version_key_pair( + &cEd25519SecretKey, + &cBlindedPubkey, + &cBlindedSeckey + ) + else { throw CryptoError.keyGenerationFailed } + + return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) + } + } + + static func signatureVersionBlind07( + timestamp: UInt64, + method: String, + path: String, + body: String?, + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator<[UInt8]> { + return Crypto.Generator( + id: "signatureVersionBlind07", + args: [timestamp, method, path, body, ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + guard + cEd25519SecretKey.count == 64, + var cMethod: [CChar] = method.cString(using: .utf8), + var cPath: [CChar] = path.cString(using: .utf8) + else { + throw CryptoError.signatureGenerationFailed + } + + var cSignature: [UInt8] = [UInt8](repeating: 0, count: 64) + + if let body: String = body { + var cBody: [UInt8] = Array(body.bytes) + guard session_blind_version_sign_request( + &cEd25519SecretKey, + timestamp, + &cMethod, + &cPath, + &cBody, + cBody.count, + &cSignature + ) else { + throw CryptoError.signatureGenerationFailed + } + } else { + guard session_blind_version_sign_request( + &cEd25519SecretKey, + timestamp, + &cMethod, + &cPath, + nil, + 0, + &cSignature + ) + else { + throw CryptoError.signatureGenerationFailed + } + } + + return cSignature + } + } +} diff --git a/SessionNetworkingKit/FileServer/FileServer.swift b/SessionNetworkingKit/FileServer/FileServer.swift index 2458124898..90fdec9e08 100644 --- a/SessionNetworkingKit/FileServer/FileServer.swift +++ b/SessionNetworkingKit/FileServer/FileServer.swift @@ -1,34 +1,16 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import SessionUtilitiesKit -// MARK: - FileServer Convenience - public extension Network { enum FileServer { - fileprivate static let fileServer = "http://filev2.getsession.org" - fileprivate static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" - fileprivate static let legacyFileServer = "http://88.99.175.227" - fileprivate static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - - public enum Endpoint: EndpointType { - case file - case fileIndividual(String) - case directUrl(URL) - case sessionVersion - - public static var name: String { "FileServerAPI.Endpoint" } - - public var path: String { - switch self { - case .file: return "file" - case .fileIndividual(let fileId): return "file/\(fileId)" - case .directUrl(let url): return url.path.removingPrefix("/") - case .sessionVersion: return "session_version" - } - } - } + internal static let fileServer = "http://filev2.getsession.org" + internal static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" + internal static let legacyFileServer = "http://88.99.175.227" + internal static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" static func fileServerPubkey(url: String? = nil) -> String { switch url?.contains(legacyFileServer) { @@ -54,6 +36,16 @@ public extension Network { public static func downloadUrlString(for fileId: String) -> String { return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" } + + public static func fileId(for downloadUrl: String?) -> String? { + return downloadUrl + .map { urlString -> String? in + urlString + .split(separator: "/") // stringlint:ignore + .last + .map { String($0) } + } + } } static func preparedUpload( diff --git a/SessionNetworkingKit/FileServer/FileServerAPI.swift b/SessionNetworkingKit/FileServer/FileServerAPI.swift index e69de29bb2..4b3304b3dd 100644 --- a/SessionNetworkingKit/FileServer/FileServerAPI.swift +++ b/SessionNetworkingKit/FileServer/FileServerAPI.swift @@ -0,0 +1,50 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +private typealias FileServer = Network.FileServer +private typealias Endpoint = Network.FileServer.Endpoint + +public extension Network.FileServer { + static func preparedUpload( + data: Data, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: .file, + destination: .serverUpload( + server: FileServer.fileServer, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ), + body: data + ), + responseType: FileUploadResponse.self, + requestTimeout: Network.fileUploadTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + static func preparedDownload( + url: URL, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: .directUrl(url), + destination: .serverDownload( + url: url, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ) + ), + responseType: Data.self, + requestTimeout: Network.fileUploadTimeout, + using: dependencies + ) + } +} diff --git a/SessionNetworkingKit/FileServer/FileServerEndpoint.swift b/SessionNetworkingKit/FileServer/FileServerEndpoint.swift index e69de29bb2..5f23b85624 100644 --- a/SessionNetworkingKit/FileServer/FileServerEndpoint.swift +++ b/SessionNetworkingKit/FileServer/FileServerEndpoint.swift @@ -0,0 +1,23 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.FileServer { + enum Endpoint: EndpointType { + case file + case fileIndividual(String) + case directUrl(URL) + case sessionVersion + + public static var name: String { "FileServer.Endpoint" } + + public var path: String { + switch self { + case .file: return "file" + case .fileIndividual(let fileId): return "file/\(fileId)" + case .directUrl(let url): return url.path.removingPrefix("/") + case .sessionVersion: return "session_version" + } + } + } +} diff --git a/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift b/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift index ae5c33739b..12c1d7fcbb 100644 --- a/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift +++ b/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift @@ -2,96 +2,98 @@ import Foundation -public class AppVersionResponse: AppVersionInfo { - enum CodingKeys: String, CodingKey { - case prerelease - } - - public let prerelease: AppVersionInfo? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]?, - prerelease: AppVersionInfo? - ) { - self.prerelease = prerelease +public extension Network.FileServer { + class AppVersionResponse: AppVersionInfo { + enum CodingKeys: String, CodingKey { + case prerelease + } - super.init( - version: version, - updated: updated, - name: name, - notes: notes, - assets: assets - ) - } - - required init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + public let prerelease: AppVersionInfo? - self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]?, + prerelease: AppVersionInfo? + ) { + self.prerelease = prerelease + + super.init( + version: version, + updated: updated, + name: name, + notes: notes, + assets: assets + ) + } - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) + + try super.init(from: decoder) + } - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(prerelease, forKey: .prerelease) - } -} - -// MARK: - AppVersionInfo - -public class AppVersionInfo: Codable { - enum CodingKeys: String, CodingKey { - case version = "result" - case updated - case name - case notes - case assets + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(prerelease, forKey: .prerelease) + } } - public struct Asset: Codable { + // MARK: - AppVersionInfo + + class AppVersionInfo: Codable { enum CodingKeys: String, CodingKey { + case version = "result" + case updated case name - case url + case notes + case assets } - public let name: String - public let url: String - } - - public let version: String - public let updated: TimeInterval? - public let name: String? - public let notes: String? - public let assets: [Asset]? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]? - ) { - self.version = version - self.updated = updated - self.name = name - self.notes = notes - self.assets = assets - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + public struct Asset: Codable { + enum CodingKeys: String, CodingKey { + case name + case url + } + + public let name: String + public let url: String + } + + public let version: String + public let updated: TimeInterval? + public let name: String? + public let notes: String? + public let assets: [Asset]? - try container.encode(version, forKey: .version) - try container.encodeIfPresent(updated, forKey: .updated) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(notes, forKey: .notes) - try container.encodeIfPresent(assets, forKey: .assets) + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]? + ) { + self.version = version + self.updated = updated + self.name = name + self.notes = notes + self.assets = assets + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(version, forKey: .version) + try container.encodeIfPresent(updated, forKey: .updated) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(notes, forKey: .notes) + try container.encodeIfPresent(assets, forKey: .assets) + } } } diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index c445d3c433..a0a2be6d27 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -158,7 +158,7 @@ class LibSessionNetwork: NetworkType { return getSwarm(for: swarmPublicKey) .tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [weak self, dependencies] snode in - try SnodeAPI + try Network.SnodeAPI .preparedGetNetworkTime(from: snode, using: dependencies) .send(using: dependencies) .tryFlatMap { _, timestampMs in @@ -175,7 +175,7 @@ class LibSessionNetwork: NetworkType { ) .map { info, response -> (ResponseInfoType, Data?) in ( - SnodeAPI.LatestTimestampResponseInfo( + Network.SnodeAPI.LatestTimestampResponseInfo( code: info.code, headers: info.headers, timestampMs: timestampMs @@ -188,7 +188,7 @@ class LibSessionNetwork: NetworkType { } } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> { typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return dependencies @@ -213,14 +213,14 @@ class LibSessionNetwork: NetworkType { ctx ) } - .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, AppVersionResponse) in + .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, Network.FileServer.AppVersionResponse) in try LibSessionNetwork.throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData, using: dependencies) guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return ( Network.ResponseInfo(code: statusCode), - try AppVersionResponse.decoded(from: data, using: dependencies) + try Network.FileServer.AppVersionResponse.decoded(from: data, using: dependencies) ) } .eraseToAnyPublisher() diff --git a/SessionNetworkingKit/Models/FileUploadResponse.swift b/SessionNetworkingKit/Models/FileUploadResponse.swift index 41ba747b0f..0f7b328d84 100644 --- a/SessionNetworkingKit/Models/FileUploadResponse.swift +++ b/SessionNetworkingKit/Models/FileUploadResponse.swift @@ -1,5 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import Foundation + public struct FileUploadResponse: Codable { public let id: String diff --git a/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift b/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift new file mode 100644 index 0000000000..d0b3158e38 --- /dev/null +++ b/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift @@ -0,0 +1,41 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension Crypto.Generator { + static func plaintextWithPushNotificationPayload( + payload: Data, + encKey: Data + ) -> Crypto.Generator { + return Crypto.Generator( + id: "plaintextWithPushNotificationPayload", + args: [payload, encKey] + ) { + var cPayload: [UInt8] = Array(payload) + var cEncKey: [UInt8] = Array(encKey) + var maybePlaintext: UnsafeMutablePointer? = nil + var plaintextLen: Int = 0 + + guard + cEncKey.count == 32, + session_decrypt_push_notification( + &cPayload, + cPayload.count, + &cEncKey, + &maybePlaintext, + &plaintextLen + ), + plaintextLen > 0, + let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) + else { throw CryptoError.decryptionFailed } + + free(UnsafeMutableRawPointer(mutating: maybePlaintext)) + + return plaintext + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift b/SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift similarity index 97% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift rename to SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift index 9e7d30447b..6fc0c0ff54 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension PushNotificationAPI { - public class AuthenticatedRequest: Encodable { +extension Network.PushNotification { + class AuthenticatedRequest: Encodable { private enum CodingKeys: String, CodingKey { case pubkey case subaccount diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift similarity index 81% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift rename to SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift index 2267c9e130..817e19185d 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift +++ b/SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift @@ -1,10 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionNetworkingKit -extension PushNotificationAPI { - public struct NotificationMetadata: Codable, Equatable { +public extension Network.PushNotification { + struct NotificationMetadata: Codable, Equatable { private enum CodingKeys: String, CodingKey { case accountId = "@" case hash = "#" @@ -22,7 +21,7 @@ extension PushNotificationAPI { public let hash: String /// The swarm namespace in which this message arrived. - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace /// The swarm timestamp when the message was created (unix epoch milliseconds) public let createdTimestampMs: Int64 @@ -43,18 +42,19 @@ extension PushNotificationAPI { // MARK: - Decodable -extension PushNotificationAPI.NotificationMetadata { +extension Network.PushNotification.NotificationMetadata { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) /// There was a bug at one point where the metadata would include a `null` value for the namespace because we were storing /// messages in a namespace that the storage server didn't have an explicit `namespace_id` for, as a result we need to assume /// that the `namespace` value may not be present in the payload - let namespace: SnodeAPI.Namespace = try container.decodeIfPresent(Int.self, forKey: .namespace) - .map { SnodeAPI.Namespace(rawValue: $0) } + let namespace: Network.SnodeAPI.Namespace = try container + .decodeIfPresent(Int.self, forKey: .namespace) + .map { Network.SnodeAPI.Namespace(rawValue: $0) } .defaulting(to: .unknown) - self = PushNotificationAPI.NotificationMetadata( + self = Network.PushNotification.NotificationMetadata( accountId: try container.decode(String.self, forKey: .accountId), hash: try container.decode(String.self, forKey: .hash), namespace: namespace, @@ -68,9 +68,9 @@ extension PushNotificationAPI.NotificationMetadata { // MARK: - Convenience -public extension PushNotificationAPI.NotificationMetadata { - static var invalid: PushNotificationAPI.NotificationMetadata { - PushNotificationAPI.NotificationMetadata( +public extension Network.PushNotification.NotificationMetadata { + static var invalid: Network.PushNotification.NotificationMetadata { + Network.PushNotification.NotificationMetadata( accountId: "", hash: "", namespace: .unknown, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift similarity index 96% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift rename to SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift index 971b969560..f28f2eb92c 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionNetworkingKit import SessionUtilitiesKit -extension PushNotificationAPI { +public extension Network.PushNotification { struct SubscribeRequest: Encodable { class Subscription: AuthenticatedRequest { private enum CodingKeys: String, CodingKey { @@ -18,7 +17,7 @@ extension PushNotificationAPI { } /// List of integer namespace (-32768 through 32767). These must be sorted in ascending order. - private let namespaces: [SnodeAPI.Namespace] + private let namespaces: [Network.SnodeAPI.Namespace] /// If provided and true then notifications will include the body of the message (as long as it isn't too large); if false then the body will /// not be included in notifications. @@ -68,7 +67,7 @@ extension PushNotificationAPI { // MARK: - Initialization init( - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], includeMessageData: Bool, serviceInfo: ServiceInfo, notificationsEncryptionKey: Data, @@ -109,9 +108,7 @@ extension PushNotificationAPI { private let subscriptions: [Subscription] - public init( - subscriptions: [Subscription] - ) { + init(subscriptions: [Subscription]) { self.subscriptions = subscriptions } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift similarity index 85% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift rename to SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift index bff4193f7d..59c8404399 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift @@ -2,17 +2,17 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { struct SubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration - let success: Bool? + public let success: Bool? /// Value is `true` upon an initial registration - let added: Bool? + public let added: Bool? /// Value is `true` upon a renewal/update registration - let updated: Bool? + public let updated: Bool? /// This will be one of the errors found here: /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 @@ -24,13 +24,13 @@ public extension PushNotificationAPI { /// SERVICE_TIMEOUT = 3 // The backend service did not response /// ERROR = 4 // There was some other error processing the subscription (details in the string) /// INTERNAL_ERROR = 5 // An internal program error occured processing the request - let error: Int? + public let error: Int? /// Includes additional information about the error - let message: String? + public let message: String? } - let subResponses: [SubResponse] + public let subResponses: [SubResponse] public init(from decoder: Decoder) throws { guard diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift similarity index 98% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift rename to SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift index 4f4b7da70c..911632d92b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionNetworkingKit import SessionUtilitiesKit -extension PushNotificationAPI { +extension Network.PushNotification { struct UnsubscribeRequest: Encodable { class Subscription: AuthenticatedRequest { private enum CodingKeys: String, CodingKey { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift similarity index 85% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift rename to SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift index c89aa19f3a..4e5e0f9e56 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift @@ -2,17 +2,17 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { struct UnsubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration - let success: Bool? + public let success: Bool? /// Value is `true` upon an initial registration - let added: Bool? + public let added: Bool? /// Value is `true` upon a renewal/update registration - let updated: Bool? + public let updated: Bool? /// This will be one of the errors found here: /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 @@ -24,13 +24,13 @@ public extension PushNotificationAPI { /// SERVICE_TIMEOUT = 3 // The backend service did not response /// ERROR = 4 // There was some other error processing the subscription (details in the string) /// INTERNAL_ERROR = 5 // An internal program error occured processing the request - let error: Int? + public let error: Int? /// Includes additional information about the error - let message: String? + public let message: String? } - let subResponses: [SubResponse] + public let subResponses: [SubResponse] public init(from decoder: Decoder) throws { guard diff --git a/SessionNetworkingKit/PushNotification/PushNotification.swift b/SessionNetworkingKit/PushNotification/PushNotification.swift new file mode 100644 index 0000000000..b080f7154e --- /dev/null +++ b/SessionNetworkingKit/PushNotification/PushNotification.swift @@ -0,0 +1,29 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +// MARK: - KeychainStorage + +public extension KeychainStorage.DataKey { static let pushNotificationEncryptionKey: Self = "PNEncryptionKeyKey" } + +// MARK: - Log.Category + +public extension Log.Category { + static let pushNotificationAPI: Log.Category = .create("PushNotificationAPI", defaultLevel: .info) +} + +// MARK: - Network.PushNotification + +public extension Network { + enum PushNotification { + internal static let encryptionKeyLength: Int = 32 + internal static let maxRetryCount: Int = 4 + public static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) + + internal static let server: String = "https://push.getsession.org" + internal static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" + } +} diff --git a/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift new file mode 100644 index 0000000000..dfc2cbee59 --- /dev/null +++ b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift @@ -0,0 +1,194 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import UserNotifications +import SessionUtilitiesKit + +public extension Network.PushNotification { + static func preparedSubscribe( + token: Data, + swarms: [(sessionId: SessionId, authMethod: AuthenticationMethod)], + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { + throw NetworkError.invalidPreparedRequest + } + + guard let notificationsEncryptionKey: Data = try? dependencies[singleton: .keychain].getOrGenerateEncryptionKey( + forKey: .pushNotificationEncryptionKey, + length: encryptionKeyLength, + cat: .pushNotificationAPI, + legacyKey: "PNEncryptionKeyKey", + legacyService: "PNKeyChainService" + ) else { + Log.error(.pushNotificationAPI, "Unable to retrieve PN encryption key.") + throw KeychainStorageError.keySpecInvalid + } + + return try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.subscribe, + body: SubscribeRequest( + subscriptions: swarms.map { sessionId, authMethod -> SubscribeRequest.Subscription in + SubscribeRequest.Subscription( + namespaces: { + switch sessionId.prefix { + case .group: return [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ] + default: return [ + .default, + .configUserProfile, + .configContacts, + .configConvoInfoVolatile, + .configUserGroups + ] + } + }(), + /// Note: Unfortunately we always need the message content because without the content + /// control messages can't be distinguished from visible messages which results in the + /// 'generic' notification being shown when receiving things like typing indicator updates + includeMessageData: true, + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + notificationsEncryptionKey: notificationsEncryptionKey, + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ) + ), + responseType: SubscribeResponse.self, + retryCount: Network.PushNotification.maxRetryCount, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + zip(response.subResponses, swarms).forEach { subResponse, swarm in + guard subResponse.success != true else { return } + + Log.error(.pushNotificationAPI, "Couldn't subscribe for push notifications for: \(swarm.sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): Log.error(.pushNotificationAPI, "Couldn't subscribe for push notifications due to error: \(error).") + } + } + ) + } + + static func preparedUnsubscribe( + token: Data, + swarms: [(sessionId: SessionId, authMethod: AuthenticationMethod)], + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.unsubscribe, + body: UnsubscribeRequest( + subscriptions: swarms.map { sessionId, authMethod -> UnsubscribeRequest.Subscription in + UnsubscribeRequest.Subscription( + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ) + ), + responseType: UnsubscribeResponse.self, + retryCount: Network.PushNotification.maxRetryCount, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + zip(response.subResponses, swarms).forEach { subResponse, swarm in + guard subResponse.success != true else { return } + + Log.error(.pushNotificationAPI, "Couldn't unsubscribe for push notifications for: \(swarm.sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): Log.error(.pushNotificationAPI, "Couldn't unsubscribe for push notifications due to error: \(error).") + } + } + ) + } + + // MARK: - Notification Handling + + static func processNotification( + notificationContent: UNNotificationContent, + using dependencies: Dependencies + ) -> (data: Data?, metadata: NotificationMetadata, result: ProcessResult) { + // Make sure the notification is from the updated push server + guard notificationContent.userInfo["spns"] != nil else { + return (nil, .invalid, .legacyFailure) + } + + guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else { + return (nil, .invalid, .failureNoContent) + } + + // Decrypt and decode the payload + let notification: BencodeResponse + + do { + guard let encryptedData: Data = Data(base64Encoded: base64EncodedEncString) else { + throw CryptoError.invalidBase64EncodedData + } + + let notificationsEncryptionKey: Data = try dependencies[singleton: .keychain].getOrGenerateEncryptionKey( + forKey: .pushNotificationEncryptionKey, + length: encryptionKeyLength, + cat: .pushNotificationAPI, + legacyKey: "PNEncryptionKeyKey", + legacyService: "PNKeyChainService" + ) + let decryptedData: Data = try dependencies[singleton: .crypto].tryGenerate( + .plaintextWithPushNotificationPayload( + payload: encryptedData, + encKey: notificationsEncryptionKey + ) + ) + notification = try BencodeDecoder(using: dependencies) + .decode(BencodeResponse.self, from: decryptedData) + } + catch { + Log.error(.pushNotificationAPI, "Failed to decrypt or decode notification due to error: \(error)") + return (nil, .invalid, .failure) + } + + // If the metadata says that the message was too large then we should show the generic + // notification (this is a valid case) + guard !notification.info.dataTooLong else { return (nil, notification.info, .successTooLong) } + + // Check that the body we were given is valid and not empty + guard + let notificationData: Data = notification.data, + notification.info.dataLength == notificationData.count, + !notificationData.isEmpty + else { + Log.error(.pushNotificationAPI, "Get notification data failed") + return (nil, notification.info, .failureNoContent) + } + + // Success, we have the notification content + return (notificationData, notification.info, .success) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift b/SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift similarity index 80% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift rename to SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift index a5d92afb4c..5fdb987b04 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift +++ b/SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift @@ -6,12 +6,12 @@ import Foundation import SessionNetworkingKit import SessionUtilitiesKit -public extension PushNotificationAPI { +public extension Network.PushNotification { enum Endpoint: EndpointType { case subscribe case unsubscribe - public static var name: String { "PushNotificationAPI.Endpoint" } + public static var name: String { "PushNotification.Endpoint" } public var path: String { switch self { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift b/SessionNetworkingKit/PushNotification/Types/ProcessResult.swift similarity index 84% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift rename to SessionNetworkingKit/PushNotification/Types/ProcessResult.swift index 07496de265..a33e84f6c0 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift +++ b/SessionNetworkingKit/PushNotification/Types/ProcessResult.swift @@ -2,7 +2,7 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { enum ProcessResult { case success case successTooLong diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift similarity index 68% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift rename to SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift index c63988f5d1..480c90bf65 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift @@ -1,12 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionNetworkingKit import SessionUtilitiesKit -// MARK: Request - PushNotificationAPI - -public extension Request where Endpoint == PushNotificationAPI.Endpoint { +public extension Request where Endpoint == Network.PushNotification.Endpoint { init( method: HTTPMethod, endpoint: Endpoint, @@ -18,10 +15,10 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { endpoint: endpoint, destination: try .server( method: method, - server: PushNotificationAPI.server, + server: Network.PushNotification.server, queryParameters: queryParameters, headers: headers, - x25519PublicKey: PushNotificationAPI.serverPublicKey + x25519PublicKey: Network.PushNotification.serverPublicKey ), body: body ) diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift b/SessionNetworkingKit/PushNotification/Types/Service.swift similarity index 84% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift rename to SessionNetworkingKit/PushNotification/Types/Service.swift index c930c0a176..fa33fe3cde 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift +++ b/SessionNetworkingKit/PushNotification/Types/Service.swift @@ -8,15 +8,15 @@ import SessionUtilitiesKit // MARK: - FeatureStorage public extension FeatureStorage { - static let pushNotificationService: FeatureConfig = Dependencies.create( + static let pushNotificationService: FeatureConfig = Dependencies.create( identifier: "pushNotificationService", defaultOption: .apns ) } -// MARK: - PushNotificationAPI.Service +// MARK: - Network.PushNotification.Service -public extension PushNotificationAPI { +public extension Network.PushNotification { enum Service: String, Codable, CaseIterable, FeatureOption { case apns case sandbox = "apns-sandbox" // Use for push notifications in Testnet diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift b/SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift similarity index 91% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift rename to SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift index 8b1ccfeaed..b534816f35 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift +++ b/SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift @@ -2,7 +2,7 @@ import Foundation -extension PushNotificationAPI { +extension Network.PushNotification { struct ServiceInfo: Codable { private enum CodingKeys: String, CodingKey { case token diff --git a/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift b/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift index e69de29bb2..9b48b1c7f4 100644 --- a/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift +++ b/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift @@ -0,0 +1,153 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension Crypto.Generator { + /// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` + static func blinded15KeyPair( + serverPublicKey: String, + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator { + return Crypto.Generator( + id: "blinded15KeyPair", + args: [serverPublicKey, ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) + var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 32) + + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_blind15_key_pair( + &cEd25519SecretKey, + &cServerPublicKey, + &cBlindedPubkey, + &cBlindedSeckey + ) + else { throw CryptoError.keyGenerationFailed } + + return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) + } + } + + /// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` + static func blinded25KeyPair( + serverPublicKey: String, + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator { + return Crypto.Generator( + id: "blinded25KeyPair", + args: [serverPublicKey, ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var cBlindedPubkey: [UInt8] = [UInt8](repeating: 0, count: 32) + var cBlindedSeckey: [UInt8] = [UInt8](repeating: 0, count: 32) + + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_blind25_key_pair( + &cEd25519SecretKey, + &cServerPublicKey, + &cBlindedPubkey, + &cBlindedSeckey + ) + else { throw CryptoError.keyGenerationFailed } + + return KeyPair(publicKey: cBlindedPubkey, secretKey: cBlindedSeckey) + } + } + + static func signatureBlind15( + message: [UInt8], + serverPublicKey: String, + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator<[UInt8]> { + return Crypto.Generator( + id: "signatureBlind15", + args: [message, serverPublicKey, ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = ed25519SecretKey + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var cMessage: [UInt8] = message + var cSignature: [UInt8] = [UInt8](repeating: 0, count: 64) + + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_blind15_sign( + &cEd25519SecretKey, + &cServerPublicKey, + &cMessage, + cMessage.count, + &cSignature + ) + else { throw CryptoError.signatureGenerationFailed } + + return cSignature + } + } + + static func signatureBlind25( + message: [UInt8], + serverPublicKey: String, + ed25519SecretKey: [UInt8] + ) -> Crypto.Generator<[UInt8]> { + return Crypto.Generator( + id: "signatureBlind25", + args: [message, serverPublicKey, ed25519SecretKey] + ) { + var cEd25519SecretKey: [UInt8] = ed25519SecretKey + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var cMessage: [UInt8] = message + var cSignature: [UInt8] = [UInt8](repeating: 0, count: 64) + + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_blind25_sign( + &cEd25519SecretKey, + &cServerPublicKey, + &cMessage, + cMessage.count, + &cSignature + ) + else { throw CryptoError.signatureGenerationFailed } + + return cSignature + } + } +} + +public extension Crypto.Verification { + /// This method should be used to check if a users standard sessionId matches a blinded one + static func sessionId( + _ standardSessionId: String, + matchesBlindedId blindedSessionId: String, + serverPublicKey: String + ) -> Crypto.Verification { + return Crypto.Verification( + id: "sessionId", + args: [standardSessionId, blindedSessionId, serverPublicKey] + ) { + guard + var cStandardSessionId: [CChar] = standardSessionId.cString(using: .utf8), + var cBlindedSessionId: [CChar] = blindedSessionId.cString(using: .utf8), + var cServerPublicKey: [CChar] = serverPublicKey.cString(using: .utf8) + else { return false } + + return session_id_matches_blinded_id( + &cStandardSessionId, + &cBlindedSessionId, + &cServerPublicKey + ) + } + } +} diff --git a/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift b/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift index 031c9c9753..1e5ddf28c1 100644 --- a/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift +++ b/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift @@ -3,13 +3,13 @@ import Foundation extension Network.SOGS { - public struct Capabilities: Codable, Equatable { - public let capabilities: [Network.SOGS.Capability.Variant] - public let missing: [Network.SOGS.Capability.Variant]? + public struct CapabilitiesResponse: Codable, Equatable { + public let capabilities: [String] + public let missing: [String]? // MARK: - Initialization - public init(capabilities: [Network.SOGS.Capability.Variant], missing: [Network.SOGS.Capability.Variant]? = nil) { + public init(capabilities: [String], missing: [String]? = nil) { self.capabilities = capabilities self.missing = missing } diff --git a/SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift b/SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift new file mode 100644 index 0000000000..c5b82413bf --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift @@ -0,0 +1,9 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct DeleteInboxResponse: Codable { + let deleted: UInt64 + } +} diff --git a/SessionNetworkingKit/SOGS/Models/DirectMessage.swift b/SessionNetworkingKit/SOGS/Models/DirectMessage.swift new file mode 100644 index 0000000000..507eb5c576 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/DirectMessage.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct DirectMessage: Codable { + enum CodingKeys: String, CodingKey { + case id + case sender + case recipient + case posted = "posted_at" + case expires = "expires_at" + case base64EncodedMessage = "message" + } + + /// The unique integer message id + public let id: Int64 + + /// The (blinded) Session ID of the sender of the message + public let sender: String + + /// The (blinded) Session ID of the recipient of the message + public let recipient: String + + /// Unix timestamp when the message was received by SOGS + public let posted: TimeInterval + + /// Unix timestamp when SOGS will expire and delete the message + public let expires: TimeInterval + + /// The encrypted message body + public let base64EncodedMessage: String + } +} diff --git a/SessionNetworkingKit/SOGS/Models/PinnedMessage.swift b/SessionNetworkingKit/SOGS/Models/PinnedMessage.swift new file mode 100644 index 0000000000..332a8bb34a --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/PinnedMessage.swift @@ -0,0 +1,22 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct PinnedMessage: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case id + case pinnedAt = "pinned_at" + case pinnedBy = "pinned_by" + } + + /// The numeric message id + let id: Int64 + + /// The unix timestamp when the message was pinned + let pinnedAt: TimeInterval + + /// The session ID of the admin who pinned this message (which is not necessarily the same as the author of the message) + let pinnedBy: String + } +} diff --git a/SessionNetworkingKit/SOGS/Models/ReactionResponse.swift b/SessionNetworkingKit/SOGS/Models/ReactionResponse.swift new file mode 100644 index 0000000000..6e4992d688 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/ReactionResponse.swift @@ -0,0 +1,44 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct ReactionAddResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case added + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was added (true) or already present (false). + public let added: Bool + + /// The seqNo after the reaction is added. + public let seqNo: Int64? + } + + public struct ReactionRemoveResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was removed (true) or was not present to begin with (false). + public let removed: Bool + + /// The seqNo after the reaction is removed. + public let seqNo: Int64? + } + + public struct ReactionRemoveAllResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field shows the total number of reactions that were deleted. + public let removed: Int64 + + /// The seqNo after the reactions is all removed. + public let seqNo: Int64? + } +} diff --git a/SessionNetworkingKit/SOGS/Models/Room.swift b/SessionNetworkingKit/SOGS/Models/Room.swift new file mode 100644 index 0000000000..6f188c5ce7 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/Room.swift @@ -0,0 +1,191 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct Room: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case token + case name + case roomDescription = "description" + case infoUpdates = "info_updates" + case messageSequence = "message_sequence" + case created + + case activeUsers = "active_users" + case activeUsersCutoff = "active_users_cutoff" + case imageId = "image_id" + case pinnedMessages = "pinned_messages" + + case admin + case globalAdmin = "global_admin" + case admins + case hiddenAdmins = "hidden_admins" + + case moderator + case globalModerator = "global_moderator" + case moderators + case hiddenModerators = "hidden_moderators" + + case read + case defaultRead = "default_read" + case defaultAccessible = "default_accessible" + case write + case defaultWrite = "default_write" + case upload + case defaultUpload = "default_upload" + } + + /// The room token as used in a URL, e.g. "sudoku" + public let token: String + + /// The room name typically shown to users, e.g. "Sodoku Solvers" + public let name: String + + /// Text description of the room, e.g. "All the best sodoku discussion!" + public let roomDescription: String? + + /// Monotonic integer counter that increases whenever the room's metadata changes + public let infoUpdates: Int64 + + /// Monotonic room post counter that increases each time a message is posted, edited, or deleted in this room + /// + /// Note that changes to this field do not imply an update the room's info_updates value, nor vice versa + public let messageSequence: Int64 + + /// Unix timestamp (as a float) of the room creation time. Note that unlike earlier versions of SOGS, this is a proper + /// seconds-since-epoch unix timestamp, not a javascript-style millisecond value + public let created: TimeInterval + + /// Number of recently active users in the room over a recent time period (as given in the active_users_cutoff value) + /// + /// Users are considered "active" if they have accessed the room (checking for new messages, etc.) at least once in the given period + /// + /// **Note:** changes to this field do not update the room's info_updates value + public let activeUsers: Int64 + + /// The length of time (in seconds) of the active_users period. Defaults to a week (604800), but the open group administrator can configure it + public let activeUsersCutoff: Int64 + + /// File ID of an uploaded file containing the room's image + /// + /// Omitted if there is no image + public let imageId: String? + + /// Array of pinned message information (omitted entirely if there are no pinned messages) + public let pinnedMessages: [PinnedMessage]? + + /// This flag is `true` if the current user has admin permissions in the room + public let admin: Bool + + /// This flag is `true` if the current user is a global admin + /// + /// This is not exclusive of `globalModerator`/`moderator`/`admin` (a global admin will have all four set to `true`) + public let globalAdmin: Bool + + /// Array of Session IDs of the room's publicly viewable moderators + /// + /// This does not include room moderator nor hidden admins + public let admins: [String] + + /// Array of Session IDs of the room's publicly hidden admins + /// + /// This field is only included if the requesting user has moderator or admin permissions, and is omitted if empty + public let hiddenAdmins: [String]? + + /// This flag is `true` if the current user has moderator permissions in the room + public let moderator: Bool + + /// This flag is `true` if the current user is a global moderator + /// + /// This is not exclusive of `moderator` (a global moderator will have both flags set to `true`) + public let globalModerator: Bool + + /// Array of Session IDs of the room's publicly viewable moderators + /// + /// This does not include room administrators nor hidden moderators + public let moderators: [String] + + /// Array of Session IDs of the room's publicly hidden moderators + /// + /// This field is only included if the requesting user has moderator or admin permissions, and is omitted if empty + public let hiddenModerators: [String]? + + /// This flag indicates whether the **current** user has permission to read the room's messages + /// + /// **Note:** If this value is `false` the user only has access the room metadata + public let read: Bool + + /// This field indicates whether new users have read permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultRead: Bool? + + /// This field indicates whether new users have access permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultAccessible: Bool? + + /// This flag indicates whether the **current** user has permission to post messages in the room + public let write: Bool + + /// This field indicates whether new users have write permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultWrite: Bool? + + /// This flag indicates whether the **current** user has permission to upload files to the room + public let upload: Bool + + /// This field indicates whether new users have upload permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultUpload: Bool? + } +} + +// MARK: - Decoding + +extension Network.SOGS.Room { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + // This logic is to future-proof the transition from int-based to string-based image ids + let maybeImageId: String? = ( + ((try? container.decode(Int64.self, forKey: .imageId)).map { "\($0)" }) ?? + (try? container.decode(String.self, forKey: .imageId)) + ) + + self = Network.SOGS.Room( + token: try container.decode(String.self, forKey: .token), + name: try container.decode(String.self, forKey: .name), + roomDescription: try? container.decode(String.self, forKey: .roomDescription), + infoUpdates: try container.decode(Int64.self, forKey: .infoUpdates), + messageSequence: try container.decode(Int64.self, forKey: .messageSequence), + created: try container.decode(TimeInterval.self, forKey: .created), + + activeUsers: try container.decode(Int64.self, forKey: .activeUsers), + activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff), + imageId: maybeImageId, + pinnedMessages: try? container.decode([Network.SOGS.PinnedMessage].self, forKey: .pinnedMessages), + + admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false), + globalAdmin: ((try? container.decode(Bool.self, forKey: .globalAdmin)) ?? false), + admins: try container.decode([String].self, forKey: .admins), + hiddenAdmins: try? container.decode([String].self, forKey: .hiddenAdmins), + + moderator: ((try? container.decode(Bool.self, forKey: .moderator)) ?? false), + globalModerator: ((try? container.decode(Bool.self, forKey: .globalModerator)) ?? false), + moderators: try container.decode([String].self, forKey: .moderators), + hiddenModerators: try? container.decode([String].self, forKey: .hiddenModerators), + + read: try container.decode(Bool.self, forKey: .read), + defaultRead: try? container.decode(Bool.self, forKey: .defaultRead), + defaultAccessible: try? container.decode(Bool.self, forKey: .defaultAccessible), + write: try container.decode(Bool.self, forKey: .write), + defaultWrite: try? container.decode(Bool.self, forKey: .defaultWrite), + upload: try container.decode(Bool.self, forKey: .upload), + defaultUpload: try? container.decode(Bool.self, forKey: .defaultUpload) + ) + } +} diff --git a/SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift b/SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift new file mode 100644 index 0000000000..a2b87f424d --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift @@ -0,0 +1,143 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + /// This only contains ephemeral data + public struct RoomPollInfo: Codable { + enum CodingKeys: String, CodingKey { + case token + case activeUsers = "active_users" + + case admin + case globalAdmin = "global_admin" + + case moderator + case globalModerator = "global_moderator" + + case read + case defaultRead = "default_read" + case defaultAccessible = "default_accessible" + case write + case defaultWrite = "default_write" + case upload + case defaultUpload = "default_upload" + + case details + } + + /// The room token as used in a URL, e.g. "sudoku" + public let token: String + + /// Number of recently active users in the room over a recent time period (as given in the active_users_cutoff value) + /// + /// Users are considered "active" if they have accessed the room (checking for new messages, etc.) at least once in the given period + /// + /// **Note:** changes to this field do not update the room's info_updates value + public let activeUsers: Int64 + + /// This flag is `true` if the current user has admin permissions in the room + public let admin: Bool + + /// This flag is `true` if the current user is a global admin + /// + /// This is not exclusive of `globalModerator`/`moderator`/`admin` (a global admin will have all four set to `true`) + public let globalAdmin: Bool + + /// This flag is `true` if the current user has moderator permissions in the room + public let moderator: Bool + + /// This flag is `true` if the current user is a global moderator + /// + /// This is not exclusive of `moderator` (a global moderator will have both flags set to `true`) + public let globalModerator: Bool + + /// This flag indicates whether the **current** user has permission to read the room's messages + /// + /// **Note:** If this value is `false` the user only has access the room metadata + public let read: Bool + + /// This field indicates whether new users have read permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultRead: Bool? + + /// This field indicates whether new users have access permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultAccessible: Bool? + + /// This flag indicates whether the **current** user has permission to post messages in the room + public let write: Bool + + /// This field indicates whether new users have write permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultWrite: Bool? + + /// This flag indicates whether the **current** user has permission to upload files to the room + public let upload: Bool + + /// This field indicates whether new users have upload permissions in the room + /// + /// It is included in the response only if the requesting user has moderator or admin permissions + public let defaultUpload: Bool? + + /// The full room metadata (as would be returned by the `/rooms/{roomToken}` endpoint) + /// + /// Only populated and different if the `info_updates` counter differs from the provided `info_updated` value + public let details: Room? + } +} + +// MARK: - Convenience + +public extension Network.SOGS.RoomPollInfo { + init(room: Network.SOGS.Room) { + self.init( + token: room.token, + activeUsers: room.activeUsers, + admin: room.admin, + globalAdmin: room.globalAdmin, + moderator: room.moderator, + globalModerator: room.globalModerator, + read: room.read, + defaultRead: room.defaultRead, + defaultAccessible: room.defaultAccessible, + write: room.write, + defaultWrite: room.defaultWrite, + upload: room.upload, + defaultUpload: room.defaultUpload, + details: room + ) + } +} + +// MARK: - Decoding + +extension Network.SOGS.RoomPollInfo { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = Network.SOGS.RoomPollInfo( + token: try container.decode(String.self, forKey: .token), + activeUsers: try container.decode(Int64.self, forKey: .activeUsers), + + admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false), + globalAdmin: ((try? container.decode(Bool.self, forKey: .globalAdmin)) ?? false), + + moderator: ((try? container.decode(Bool.self, forKey: .moderator)) ?? false), + globalModerator: ((try? container.decode(Bool.self, forKey: .globalModerator)) ?? false), + + read: try container.decode(Bool.self, forKey: .read), + defaultRead: try? container.decode(Bool.self, forKey: .defaultRead), + defaultAccessible: try? container.decode(Bool.self, forKey: .defaultAccessible), + write: try container.decode(Bool.self, forKey: .write), + defaultWrite: try? container.decode(Bool.self, forKey: .defaultWrite), + upload: try container.decode(Bool.self, forKey: .upload), + defaultUpload: try? container.decode(Bool.self, forKey: .defaultUpload), + + details: try? container.decode(Network.SOGS.Room.self, forKey: .details) + ) + } +} diff --git a/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift b/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift index d1beafbddb..5b902a0941 100644 --- a/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift +++ b/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift @@ -1,10 +1,9 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionNetworkingKit import SessionUtilitiesKit -extension OpenGroupAPI { +extension Network.SOGS { public struct Message: Codable, Equatable { enum CodingKeys: String, CodingKey { case id @@ -56,7 +55,7 @@ extension OpenGroupAPI { // MARK: - Decoder -extension OpenGroupAPI.Message { +extension Network.SOGS.Message { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -102,7 +101,7 @@ extension OpenGroupAPI.Message { } } - self = OpenGroupAPI.Message( + self = Network.SOGS.Message( id: try container.decode(Int64.self, forKey: .id), sender: try container.decodeIfPresent(String.self, forKey: .sender), posted: try container.decodeIfPresent(TimeInterval.self, forKey: .posted), @@ -119,11 +118,11 @@ extension OpenGroupAPI.Message { } } -extension OpenGroupAPI.Message.Reaction { +extension Network.SOGS.Message.Reaction { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - self = OpenGroupAPI.Message.Reaction( + self = Network.SOGS.Message.Reaction( count: try container.decode(Int64.self, forKey: .count), reactors: try container.decodeIfPresent([String].self, forKey: .reactors), you: ((try container.decodeIfPresent(Bool.self, forKey: .you)) ?? false), diff --git a/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift index 19df350f9e..4a72a11423 100644 --- a/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct SendDirectMessageRequest: Codable { let message: Data diff --git a/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift b/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift index a8e998f8ac..a076f4edd4 100644 --- a/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift +++ b/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift @@ -2,8 +2,8 @@ import Foundation -extension OpenGroupAPI { - public struct SendDirectMessageResponse: Codable, Equatable { +public extension Network.SOGS { + struct SendDirectMessageResponse: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case sender diff --git a/SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift new file mode 100644 index 0000000000..b6520ad7cd --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift @@ -0,0 +1,78 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct SendSOGSMessageRequest: Codable { + enum CodingKeys: String, CodingKey { + case data + case signature + case whisperTo = "whisper_to" + case whisperMods = "whisper_mods" + case fileIds = "files" + } + + /// The serialized message body (encoded in base64 when encoding) + let data: Data + + /// A 64-byte Ed25519 signature of the message body, signed by the current user's keys (encoded in base64 when + /// encoding - ie. 88 base64 chars) + let signature: Data + + /// If present this indicates that this message is a whisper that should only be shown to the given user (via their sessionId) + let whisperTo: String? + + /// If `true`, then this message will be visible to moderators but not ordinary users + /// + /// If this and `whisper_to` are used together then the message will be visible to the given user and any room + /// moderators (this can be used, for instance, to issue a warning to a user that only the user and other mods can see) + /// + /// **Note:** Only moderators may set this flag + let whisperMods: Bool? + + /// Array of file IDs of new files uploaded as attachments of this post + /// + /// This is required to preserve uploads for the default expiry period (15 days, unless otherwise configured by the SOGS + /// administrator); uploaded files that are not attached to a post will be deleted much sooner + /// + /// If any of the given file ids are already associated with another message then the association is ignored (i.e. the files remain + /// associated with the original message) + /// + /// When submitting a message edit this field must contain the IDs of any newly uploaded files that are part of the edit; existing + /// attachment IDs may also be included, but are not required + /// + /// **Note:** The SOGS API actually expects an array of Int64 (ie. what is returned when uploading a file to SOGS) but + /// when uploading direct to the FileServer we get a string id back. In order to avoid supporting both cases we convert + /// the id returned by SOGS to a string and send those through - luckily SOGS converts the values to ints so supports + /// receipving an array of String values + let fileIds: [String]? + + // MARK: - Initialization + + init( + data: Data, + signature: Data, + whisperTo: String? = nil, + whisperMods: Bool? = nil, + fileIds: [String]? = nil + ) { + self.data = data + self.signature = signature + self.whisperTo = whisperTo + self.whisperMods = whisperMods + self.fileIds = fileIds + } + + // MARK: - Encodable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .data) + try container.encode(signature.base64EncodedString(), forKey: .signature) + try container.encodeIfPresent(whisperTo, forKey: .whisperTo) + try container.encodeIfPresent(whisperMods, forKey: .whisperMods) + try container.encodeIfPresent(fileIds, forKey: .fileIds) + } + } +} diff --git a/SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift new file mode 100644 index 0000000000..640a5d92d8 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift @@ -0,0 +1,35 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct UpdateMessageRequest: Codable { + /// The serialized message body (encoded in base64 when encoding) + let data: Data + + /// A 64-byte Ed25519 signature of the message body, signed by the current user's keys (encoded in base64 when + /// encoding - ie. 88 base64 chars) + let signature: Data + + /// Array of file IDs of new files uploaded as attachments of this post + /// + /// This is required to preserve uploads for the default expiry period (15 days, unless otherwise configured by the SOGS + /// administrator); uploaded files that are not attached to a post will be deleted much sooner + /// + /// If any of the given file ids are already associated with another message then the association is ignored (i.e. the files remain + /// associated with the original message) + /// + /// This field must contain the IDs of any newly uploaded files that are part of the edit; existing attachment IDs may also be + /// included, but are not required + let fileIds: [Int64]? + + // MARK: - Encodable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .data) + try container.encode(signature.base64EncodedString(), forKey: .signature) + } + } +} diff --git a/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift b/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift index caff1a17de..249bf8db0c 100644 --- a/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserBanRequest: Codable { /// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator` /// of all of the given rooms diff --git a/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift b/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift index ece21d2baa..8151ede9ee 100644 --- a/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserModeratorRequest: Codable { /// List of room tokens to which the moderator status should be applied. The invoking user must be an admin of all of the given rooms. /// diff --git a/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift b/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift index b0e8a2ab99..d1524d3e4a 100644 --- a/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserUnbanRequest: Codable { /// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator` /// of all of the given rooms diff --git a/SessionNetworkingKit/SOGS/SOGS.swift b/SessionNetworkingKit/SOGS/SOGS.swift index e69de29bb2..0c5e5ce75e 100644 --- a/SessionNetworkingKit/SOGS/SOGS.swift +++ b/SessionNetworkingKit/SOGS/SOGS.swift @@ -0,0 +1,15 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network { + enum SOGS { + public static let legacyDefaultServerIP = "116.203.70.33" + public static let defaultServer = "https://open.getsession.org" + public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" + public static let validTimestampVarianceThreshold: TimeInterval = (6 * 60 * 60) + internal static let maxInactivityPeriodForPolling: TimeInterval = (14 * 24 * 60 * 60) + + public static let workQueue = DispatchQueue(label: "SOGS.workQueue", qos: .userInitiated) // It's important that this is a serial queue + } +} diff --git a/SessionNetworkingKit/SOGS/SOGSAPI.swift b/SessionNetworkingKit/SOGS/SOGSAPI.swift index efe47a4c8c..fcdc911e9d 100644 --- a/SessionNetworkingKit/SOGS/SOGSAPI.swift +++ b/SessionNetworkingKit/SOGS/SOGSAPI.swift @@ -3,25 +3,15 @@ // stringlint:disable import Foundation -import SessionNetworkingKit import SessionUtilitiesKit -public enum OpenGroupAPI { - public struct RoomInfo: Codable { +public extension Network.SOGS { + struct PollRoomInfo: Codable { let roomToken: String let infoUpdates: Int64 let sequenceNumber: Int64 } - // MARK: - Settings - - public static let legacyDefaultServerIP = "116.203.70.33" - public static let defaultServer = "https://open.getsession.org" - public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - public static let validTimestampVarianceThreshold: TimeInterval = (6 * 60 * 60) - - public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue - // MARK: - Batching & Polling /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open @@ -32,10 +22,11 @@ public enum OpenGroupAPI { /// - Messages (includes additions and deletions) /// - Inbox for the server /// - Outbox for the server - public static func preparedPoll( - roomInfo: [RoomInfo], + static func preparedPoll( + roomInfo: [PollRoomInfo], lastInboxMessageId: Int64, lastOutboxMessageId: Int64, + checkForCommunityMessageRequests: Bool, hasPerformedInitialPoll: Bool, timeSinceLastPoll: TimeInterval, authMethod: AuthenticationMethod, @@ -60,7 +51,7 @@ public enum OpenGroupAPI { // 'maxInactivityPeriod' then just retrieve recent messages instead // of trying to get all messages since the last one retrieved !hasPerformedInitialPoll && - timeSinceLastPoll > CommunityPoller.maxInactivityPeriod + timeSinceLastPoll > maxInactivityPeriodForPolling ) ) @@ -93,7 +84,7 @@ public enum OpenGroupAPI { !supportsBlinding ? [] : [ // Inbox (only check the inbox if the user want's community message requests) - (!dependencies.mutate(cache: .libSession) { $0.get(.checkForCommunityMessageRequests) } ? nil : + (!checkForCommunityMessageRequests ? nil : (lastInboxMessageId == 0 ? try preparedInbox(authMethod: authMethod, using: dependencies) : try preparedInboxSince( @@ -117,13 +108,13 @@ public enum OpenGroupAPI { ) ) - return try OpenGroupAPI + return try Network.SOGS .preparedBatch( requests: preparedRequests, authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one @@ -133,7 +124,7 @@ public enum OpenGroupAPI { /// /// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided /// with the request body. - public static func preparedBatch( + static func preparedBatch( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -149,7 +140,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests @@ -178,7 +169,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Capabilities @@ -190,20 +181,20 @@ public enum OpenGroupAPI { /// /// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch` /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` - public static func preparedCapabilities( + static func preparedCapabilities( authMethod: AuthenticationMethod, using dependencies: Dependencies - ) throws -> Network.PreparedRequest { + ) throws -> Network.PreparedRequest { return try Network.PreparedRequest( request: Request( endpoint: .capabilities, authMethod: authMethod ), - responseType: Capabilities.self, + responseType: CapabilitiesResponse.self, additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Room @@ -211,7 +202,7 @@ public enum OpenGroupAPI { /// Returns a list of available rooms on the server /// /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included - public static func preparedRooms( + static func preparedRooms( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[Room]> { @@ -224,11 +215,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Returns the details of a single room - public static func preparedRoom( + static func preparedRoom( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -242,14 +233,14 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls a room for metadata updates /// /// The endpoint polls room metadata for this room, always including the instantaneous room details (such as the user's permission and current /// number of active users), and including the full room metadata if the room's info_updated counter has changed from the provided value - public static func preparedRoomPollInfo( + static func preparedRoomPollInfo( lastUpdated: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -264,22 +255,22 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } - public typealias CapabilitiesAndRoomResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), + typealias CapabilitiesAndRoomResponse = ( + capabilities: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse), room: (info: ResponseInfoType, data: Room) ) /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `room` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRoom( + static func preparedCapabilitiesAndRoom( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try OpenGroupAPI + return try Network.SOGS .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the @@ -290,9 +281,9 @@ public enum OpenGroupAPI { authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRoomResponse: Any? = response.data .first(where: { key, _ in switch key { @@ -305,7 +296,7 @@ public enum OpenGroupAPI { guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, + let capabilities: Network.SOGS.CapabilitiesResponse = maybeCapabilities?.body, let roomInfo: ResponseInfoType = maybeRoom, let room: Room = maybeRoom?.body else { throw NetworkError.parsingFailed } @@ -317,18 +308,18 @@ public enum OpenGroupAPI { } } - public typealias CapabilitiesAndRoomsResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), + typealias CapabilitiesAndRoomsResponse = ( + capabilities: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse), rooms: (info: ResponseInfoType, data: [Room]) ) /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `rooms` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRooms( + static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try OpenGroupAPI + return try Network.SOGS .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the @@ -339,9 +330,9 @@ public enum OpenGroupAPI { authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data .first(where: { key, _ in switch key { @@ -353,7 +344,7 @@ public enum OpenGroupAPI { guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, + let capabilities: Network.SOGS.CapabilitiesResponse = maybeCapabilities?.body, let roomsInfo: ResponseInfoType = maybeRooms, let roomsResponse: Network.BatchSubResponse<[Room]> = maybeRooms, !roomsResponse.failedToParseBody @@ -370,7 +361,7 @@ public enum OpenGroupAPI { // MARK: - Messages /// Posts a new message to a room - public static func preparedSend( + static func preparedSend( plaintext: Data, roomToken: String, whisperTo: String?, @@ -390,7 +381,7 @@ public enum OpenGroupAPI { request: Request( method: .post, endpoint: Endpoint.roomMessage(roomToken), - body: SendMessageRequest( + body: SendSOGSMessageRequest( data: plaintext, signature: Data(signResult.signature), whisperTo: whisperTo, @@ -403,11 +394,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Returns a single message by ID - public static func preparedMessage( + static func preparedMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -422,13 +413,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Edits a message, replacing its existing content with new content and a new signature /// /// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room - public static func preparedMessageUpdate( + static func preparedMessageUpdate( id: Int64, plaintext: Data, fileIds: [Int64]?, @@ -458,11 +449,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove a message by its message id - public static func preparedMessageDelete( + static func preparedMessageDelete( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -478,7 +469,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves recent messages posted to this room @@ -486,7 +477,7 @@ public enum OpenGroupAPI { /// Returns the most recent limit messages (100 if no limit is given). This only returns extant messages, and always returns the latest /// versions: that is, deleted message indicators and pre-editing versions of messages are not returned. Messages are returned in order /// from most recent to least recent - public static func preparedRecentMessages( + static func preparedRecentMessages( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -505,7 +496,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves messages from the room preceding a given id. @@ -514,7 +505,7 @@ public enum OpenGroupAPI { /// through batches of ever-older messages. As with .../recent, messages are returned in order from most recent to least recent. /// /// As with .../recent, this endpoint does not include deleted messages and always returns the current version, for edited messages. - public static func preparedMessagesBefore( + static func preparedMessagesBefore( messageId: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -534,7 +525,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves message updates from a room. This is the main message polling endpoint in SOGS. @@ -543,7 +534,7 @@ public enum OpenGroupAPI { /// sequence counter. Returns limit messages at a time (100 if no limit is given). Returned messages include any new messages, updates /// to existing messages (i.e. edits), and message deletions made to the room since the given update id. Messages are returned in "update" /// order, that is, in the order in which the change was applied to the room, from oldest the newest. - public static func preparedMessagesSince( + static func preparedMessagesSince( seqNo: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -563,7 +554,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server @@ -579,7 +570,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedMessagesDeleteAll( + static func preparedMessagesDeleteAll( sessionId: String, roomToken: String, authMethod: AuthenticationMethod, @@ -595,13 +586,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Reactions /// Returns the list of all reactors who have added a particular reaction to a particular message. - public static func preparedReactors( + static func preparedReactors( emoji: String, id: Int64, roomToken: String, @@ -611,7 +602,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -624,14 +615,14 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Adds a reaction to the given message in this room. The user must have read access in the room. /// /// Reactions are short strings of 1-12 unicode codepoints, typically emoji (or character sequences to produce an emoji variant, /// such as 👨🏿‍🦰, which is composed of 4 unicode "characters" but usually renders as a single emoji "Man: Dark Skin Tone, Red Hair"). - public static func preparedReactionAdd( + static func preparedReactionAdd( emoji: String, id: Int64, roomToken: String, @@ -641,7 +632,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -654,12 +645,12 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes a reaction from a post this room. The user must have read access in the room. This only removes the user's own reaction /// but does not affect the reactions of other users. - public static func preparedReactionDelete( + static func preparedReactionDelete( emoji: String, id: Int64, roomToken: String, @@ -669,7 +660,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -682,13 +673,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes all reactions of all users from a post in this room. The calling must have moderator permissions in the room. This endpoint /// can either remove a single reaction (e.g. remove all 🍆 reactions) by specifying it after the message id (following a /), or remove all /// reactions from the post by not including the / suffix of the URL. - public static func preparedReactionDeleteAll( + static func preparedReactionDeleteAll( emoji: String, id: Int64, roomToken: String, @@ -698,7 +689,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -711,7 +702,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Pinning @@ -726,7 +717,7 @@ public enum OpenGroupAPI { /// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned /// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the /// order in which pinned messages should be displayed - public static func preparedPinMessage( + static func preparedPinMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -742,13 +733,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove a message from this room's pinned message list /// /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinMessage( + static func preparedUnpinMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -764,13 +755,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes _all_ pinned messages from this room /// /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinAll( + static func preparedUnpinAll( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -785,12 +776,12 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Files - public static func preparedUpload( + static func preparedUpload( data: Data, roomToken: String, fileName: String? = nil, @@ -816,10 +807,10 @@ public enum OpenGroupAPI { requestTimeout: Network.fileUploadTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } - public static func downloadUrlString( + static func downloadUrlString( for fileId: String, server: String, roomToken: String @@ -827,18 +818,20 @@ public enum OpenGroupAPI { return "\(server)/\(Endpoint.roomFileIndividual(roomToken, fileId).path)" } - public static func preparedDownload( + static func preparedDownload( url: URL, roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - guard let fileId: String = Attachment.fileId(for: url.absoluteString) else { throw NetworkError.invalidURL } + guard let fileId: String = Network.FileServer.fileId(for: url.absoluteString) else { + throw NetworkError.invalidURL + } return try preparedDownload(fileId: fileId, roomToken: roomToken, authMethod: authMethod, using: dependencies) } - public static func preparedDownload( + static func preparedDownload( fileId: String, roomToken: String, authMethod: AuthenticationMethod, @@ -854,7 +847,7 @@ public enum OpenGroupAPI { requestTimeout: Network.fileDownloadTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Inbox/Outbox (Message Requests) @@ -862,7 +855,7 @@ public enum OpenGroupAPI { /// Retrieves all of the user's current DMs (up to limit) /// /// **Note:** `inbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInbox( + static func preparedInbox( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[DirectMessage]?> { @@ -875,13 +868,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages /// /// **Note:** `inboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInboxSince( + static func preparedInboxSince( id: Int64, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -895,11 +888,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove all message requests from inbox, this methrod will return the number of messages deleted - public static func preparedClearInbox( + static func preparedClearInbox( requestTimeout: TimeInterval = Network.defaultTimeout, requestAndPathBuildTimeout: TimeInterval? = nil, authMethod: AuthenticationMethod, @@ -917,13 +910,13 @@ public enum OpenGroupAPI { requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Delivers a direct message to a user via their blinded Session ID /// /// The body of this request is a JSON object containing a message key with a value of the encrypted-then-base64-encoded message to deliver - public static func preparedSend( + static func preparedSend( ciphertext: Data, toInboxFor blindedSessionId: String, authMethod: AuthenticationMethod, @@ -942,13 +935,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves all of the user's sent DMs (up to limit) /// /// **Note:** `outbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutbox( + static func preparedOutbox( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[DirectMessage]?> { @@ -961,13 +954,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages /// /// **Note:** `outboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutboxSince( + static func preparedOutboxSince( id: Int64, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -981,7 +974,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Users @@ -1017,7 +1010,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserBan( + static func preparedUserBan( sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, @@ -1039,7 +1032,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes a user ban from specific rooms, or from the server globally @@ -1066,7 +1059,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserUnban( + static func preparedUserUnban( sessionId: String, from roomTokens: [String]?, authMethod: AuthenticationMethod, @@ -1086,7 +1079,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Appoints or removes a moderator or admin @@ -1140,7 +1133,7 @@ public enum OpenGroupAPI { /// - server: The server to perform the permission changes on /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserModeratorUpdate( + static func preparedUserModeratorUpdate( sessionId: String, moderator: Bool? = nil, admin: Bool? = nil, @@ -1170,18 +1163,18 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedUserBanAndDeleteAllMessages( + static func preparedUserBanAndDeleteAllMessages( sessionId: String, roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest> { - return try OpenGroupAPI + return try Network.SOGS .preparedSequence( requests: [ preparedUserBan( @@ -1200,7 +1193,7 @@ public enum OpenGroupAPI { authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Authentication @@ -1222,7 +1215,7 @@ public enum OpenGroupAPI { !publicKey.isEmpty, let nonce: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(16)), let timestampBytes: [UInt8] = "\(timestamp)".data(using: .ascii).map({ Array($0) }) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } /// Get a hash of any body content let bodyHash: [UInt8]? = { @@ -1273,7 +1266,7 @@ public enum OpenGroupAPI { !dependencies[cache: .general].ed25519SecretKey.isEmpty, !dependencies[cache: .general].ed25519Seed.isEmpty, case .community(_, let publicKey, let hasCapabilities, let supportsBlinding, let forceBlinded) = authMethod.info - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } // If we have no capabilities or if the server supports blinded keys then sign using the blinded key if forceBlinded || !hasCapabilities || supportsBlinding { @@ -1291,7 +1284,7 @@ public enum OpenGroupAPI { ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey ) ) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.blinded15, publicKey: blinded15KeyPair.publicKey).hexString, @@ -1313,7 +1306,7 @@ public enum OpenGroupAPI { .ed25519KeyPair(seed: dependencies[cache: .general].ed25519Seed) ), case .standard(let signatureResult) = signature - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.unblinded, publicKey: ed25519KeyPair.publicKey).hexString, @@ -1335,7 +1328,7 @@ public enum OpenGroupAPI { let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( .signatureXed25519(data: messageBytes, curve25519PrivateKey: x25519SecretKey) ) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.standard, publicKey: x25519PublicKey).hexString, @@ -1350,7 +1343,7 @@ public enum OpenGroupAPI { using dependencies: Dependencies ) throws -> Network.Destination { guard let signingData: AdditionalSigningData = preparedRequest.additionalSignatureData as? AdditionalSigningData else { - throw OpenGroupAPIError.signingFailed + throw SOGSError.signingFailed } return try preparedRequest.destination @@ -1358,7 +1351,7 @@ public enum OpenGroupAPI { } } -private extension OpenGroupAPI { +private extension Network.SOGS { struct AdditionalSigningData { let authMethod: AuthenticationMethod @@ -1369,7 +1362,7 @@ private extension OpenGroupAPI { } private extension Network.Destination { - func signed(data: OpenGroupAPI.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { + func signed(data: Network.SOGS.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { switch self { case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: throw NetworkError.unauthorised case .cached: return self @@ -1384,8 +1377,8 @@ private extension Network.Destination { } private extension Network.Destination.ServerInfo { - func signed(_ data: OpenGroupAPI.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { - return updated(with: try OpenGroupAPI.signatureHeaders( + func signed(_ data: Network.SOGS.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { + return updated(with: try Network.SOGS.signatureHeaders( url: url, method: method, body: body, diff --git a/SessionNetworkingKit/SOGS/SOGSEndpoint.swift b/SessionNetworkingKit/SOGS/SOGSEndpoint.swift index e5e0b9bd34..218bfadbe3 100644 --- a/SessionNetworkingKit/SOGS/SOGSEndpoint.swift +++ b/SessionNetworkingKit/SOGS/SOGSEndpoint.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionNetworkingKit -extension OpenGroupAPI { - public enum Endpoint: EndpointType { +public extension Network.SOGS { + enum Endpoint: EndpointType { // Utility case onion @@ -61,7 +60,7 @@ extension OpenGroupAPI { case userUnban(String) case userModerator(String) - public static var name: String { "OpenGroupAPI.Endpoint" } + public static var name: String { "SOGS.Endpoint" } public static var batchRequestVariant: Network.BatchRequest.Child.Variant = .sogs public static var excludedSubRequestHeaders: [HTTPHeader] = [ .sogsPubKey, .sogsTimestamp, .sogsNonce, .sogsSignature diff --git a/SessionNetworkingKit/SOGS/SOGSError.swift b/SessionNetworkingKit/SOGS/SOGSError.swift index d5ab81cbe1..bf91640de0 100644 --- a/SessionNetworkingKit/SOGS/SOGSError.swift +++ b/SessionNetworkingKit/SOGS/SOGSError.swift @@ -4,7 +4,7 @@ import Foundation -public enum OpenGroupAPIError: Error, CustomStringConvertible { +public enum SOGSError: Error, CustomStringConvertible { case decryptionFailed case signingFailed case noPublicKey diff --git a/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift b/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift index 29189d3cef..0c100cfbd1 100644 --- a/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift +++ b/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import SessionNetworkingKit public extension HTTPHeader { static let sogsPubKey: HTTPHeader = "X-SOGS-Pubkey" diff --git a/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift b/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift index 4eb7f6c206..dd6c86e3c5 100644 --- a/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift +++ b/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import SessionNetworkingKit public extension HTTPQueryParam { static let publicKey: HTTPQueryParam = "public_key" diff --git a/SessionNetworkingKit/SOGS/Types/Personalization.swift b/SessionNetworkingKit/SOGS/Types/Personalization.swift new file mode 100644 index 0000000000..5b24626fe9 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Types/Personalization.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public enum Personalization: String { + case sharedKeys = "sogs.shared_keys" + case authHeader = "sogs.auth_header" + + var bytes: [UInt8] { + return Array(self.rawValue.utf8) + } + } +} diff --git a/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift b/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift index 5c8d72187f..5373174584 100644 --- a/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift +++ b/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift @@ -1,13 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import GRDB -import SessionNetworkingKit import SessionUtilitiesKit -// MARK: Request - OpenGroupAPI - -public extension Request where Endpoint == OpenGroupAPI.Endpoint { +public extension Request where Endpoint == Network.SOGS.Endpoint { init( method: HTTPMethod = .get, endpoint: Endpoint, diff --git a/SessionNetworkingKit/SOGS/Types/UpdateTypes.swift b/SessionNetworkingKit/SOGS/Types/UpdateTypes.swift new file mode 100644 index 0000000000..61baf5ee47 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Types/UpdateTypes.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + enum UpdateTypes: String { + case reaction = "r" + } +} diff --git a/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift index c28f83d126..1d11aa7888 100644 --- a/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift +++ b/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift @@ -17,20 +17,20 @@ public extension Network.SessionNetwork { using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try Network.PreparedRequest( - request: Request( - endpoint: Network.NetworkAPI.Endpoint.info, + request: Request( + endpoint: Network.SessionNetwork.Endpoint.info, destination: .server( method: .get, - server: Network.NetworkAPI.networkAPIServer, + server: Network.SessionNetwork.networkAPIServer, queryParameters: [:], - x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey + x25519PublicKey: Network.SessionNetwork.networkAPIServerPublicKey ) ), responseType: Info.self, requestAndPathBuildTimeout: Network.defaultTimeout, using: dependencies ) - .signed(with: SessionNetworkAPI.signRequest, using: dependencies) + .signed(with: Network.SessionNetwork.signRequest, using: dependencies) } // MARK: - Authentication diff --git a/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift index a54cbc083f..eb8857f776 100644 --- a/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift +++ b/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift @@ -55,7 +55,7 @@ public extension SnodeReceivedMessageInfo { init( snode: LibSession.Snode, swarmPublicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, hash: String, expirationDateMs: Int64? ) { @@ -75,7 +75,7 @@ public extension SnodeReceivedMessageInfo { static func fetchLastNotExpired( _ db: ObservingDatabase, for snode: LibSession.Snode, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, swarmPublicKey: String, using dependencies: Dependencies ) throws -> SnodeReceivedMessageInfo? { diff --git a/SessionNetworkingKit/StorageServer/Models/AppVersionResponse.swift b/SessionNetworkingKit/StorageServer/Models/AppVersionResponse.swift deleted file mode 100644 index ae5c33739b..0000000000 --- a/SessionNetworkingKit/StorageServer/Models/AppVersionResponse.swift +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public class AppVersionResponse: AppVersionInfo { - enum CodingKeys: String, CodingKey { - case prerelease - } - - public let prerelease: AppVersionInfo? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]?, - prerelease: AppVersionInfo? - ) { - self.prerelease = prerelease - - super.init( - version: version, - updated: updated, - name: name, - notes: notes, - assets: assets - ) - } - - required init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(prerelease, forKey: .prerelease) - } -} - -// MARK: - AppVersionInfo - -public class AppVersionInfo: Codable { - enum CodingKeys: String, CodingKey { - case version = "result" - case updated - case name - case notes - case assets - } - - public struct Asset: Codable { - enum CodingKeys: String, CodingKey { - case name - case url - } - - public let name: String - public let url: String - } - - public let version: String - public let updated: TimeInterval? - public let name: String? - public let notes: String? - public let assets: [Asset]? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]? - ) { - self.version = version - self.updated = updated - self.name = name - self.notes = notes - self.assets = assets - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(version, forKey: .version) - try container.encodeIfPresent(updated, forKey: .updated) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(notes, forKey: .notes) - try container.encodeIfPresent(assets, forKey: .assets) - } -} diff --git a/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift index 6e6e1e64af..bd720328f5 100644 --- a/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift @@ -5,22 +5,22 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { +extension Network.SnodeAPI { + final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case beforeMs = "before" case namespace } let beforeMs: UInt64 - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? override var verificationBytes: [UInt8] { /// Ed25519 signature of `("delete_before" || namespace || before)`, signed by /// `pubkey`. Must be base64 encoded (json) or bytes (OMQ). `namespace` is the stringified /// version of the given non-default namespace parameter (i.e. "-42" or "all"), or the empty /// string for the default namespace (whether explicitly given or not). - SnodeAPI.Endpoint.deleteAllBefore.path.bytes + Network.SnodeAPI.Endpoint.deleteAllBefore.path.bytes .appending( contentsOf: (namespace == nil ? "all" : @@ -34,7 +34,7 @@ extension SnodeAPI { public init( beforeMs: UInt64, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift index 57f0c28f2b..3dc0d5bdbe 100644 --- a/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { +extension Network.SnodeAPI { + final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case namespace } @@ -14,7 +14,7 @@ extension SnodeAPI { /// /// **Note:** If omitted when sending the request, messages are deleted from the default namespace /// only (namespace 0) - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace override var verificationBytes: [UInt8] { /// Ed25519 signature of `( "delete_all" || namespace || timestamp )`, where @@ -22,7 +22,7 @@ extension SnodeAPI { /// not), and otherwise the stringified version of the namespace parameter (i.e. "99" or "-42" or "all"). /// The signature must be signed by the ed25519 pubkey in `pubkey` (omitting the leading prefix). /// Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.deleteAll.path.bytes + Network.SnodeAPI.Endpoint.deleteAll.path.bytes .appending(contentsOf: namespace.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -30,7 +30,7 @@ extension SnodeAPI { // MARK: - Init public init( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift index 4adc127240..c1736499ac 100644 --- a/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class DeleteMessagesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class DeleteMessagesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" case requireSuccessfulDeletion = "required" @@ -17,7 +17,7 @@ extension SnodeAPI { /// Ed25519 signature of `("delete" || messages...)`; this signs the value constructed /// by concatenating "delete" and all `messages` values, using `pubkey` to sign. Must be base64 /// encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.deleteMessages.path.bytes + Network.SnodeAPI.Endpoint.deleteMessages.path.bytes .appending(contentsOf: messageHashes.joined().bytes) } diff --git a/SessionNetworkingKit/StorageServer/Models/FileUploadResponse.swift b/SessionNetworkingKit/StorageServer/Models/FileUploadResponse.swift deleted file mode 100644 index 41ba747b0f..0000000000 --- a/SessionNetworkingKit/StorageServer/Models/FileUploadResponse.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -public struct FileUploadResponse: Codable { - public let id: String - - public init(id: String) { - self.id = id - } -} - -// MARK: - Codable - -extension FileUploadResponse { - public init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - // Note: SOGS returns an 'int' value but we want to avoid handling both cases so parse - // that and convert the value to a string so we can be consistent (SOGS is able to handle - // an array of Strings for the `files` param when posting a message just fine) - if let intValue: Int64 = try? container.decode(Int64.self, forKey: .id) { - self = FileUploadResponse(id: "\(intValue)") - return - } - - self = FileUploadResponse( - id: try container.decode(String.self, forKey: .id) - ) - } -} diff --git a/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift b/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift index 091729fb3b..d1f85ebf2b 100644 --- a/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class GetExpiriesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class GetExpiriesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" } @@ -16,7 +16,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("get_expiries" || timestamp || messages[0] || ... || messages[N])` /// where `timestamp` is expressed as a string (base10). The signature must be base64 encoded (json) or bytes (bt). - SnodeAPI.Endpoint.getExpiries.path.bytes + Network.SnodeAPI.Endpoint.getExpiries.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: messageHashes.joined().bytes) } diff --git a/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift index 3b7e7ca05b..c0c3f0ef7b 100644 --- a/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class GetMessagesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class GetMessagesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case lastHash = "last_hash" case namespace @@ -13,7 +13,7 @@ extension SnodeAPI { } let lastHash: String - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? let maxCount: Int64? let maxSize: Int64? @@ -22,7 +22,7 @@ extension SnodeAPI { /// namespace), or `("retrieve" || timestamp)` when fetching from the default namespace. Both /// namespace and timestamp are the base10 expressions of the relevant values. Must be base64 /// encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.getMessages.path.bytes + Network.SnodeAPI.Endpoint.getMessages.path.bytes .appending(contentsOf: namespace?.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -31,7 +31,7 @@ extension SnodeAPI { public init( lastHash: String, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod, timestampMs: UInt64, maxCount: Int64? = nil, diff --git a/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift b/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift index 71428bab9d..d29488ffc2 100644 --- a/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift @@ -2,8 +2,8 @@ import Foundation -extension SnodeAPI { - public struct GetNetworkTimestampResponse: Decodable { +public extension Network.SnodeAPI { + struct GetNetworkTimestampResponse: Decodable { enum CodingKeys: String, CodingKey { case timestamp case version diff --git a/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift index 70dc7aa3a8..ab008a94bb 100644 --- a/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift @@ -2,9 +2,9 @@ import Foundation -extension SnodeAPI { +extension Network.SnodeAPI { /// This is the legacy unauthenticated message retrieval request - public struct LegacyGetMessagesRequest: Encodable { + struct LegacyGetMessagesRequest: Encodable { enum CodingKeys: String, CodingKey { case pubkey case lastHash = "last_hash" @@ -15,7 +15,7 @@ extension SnodeAPI { let pubkey: String let lastHash: String - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? let maxCount: Int64? let maxSize: Int64? diff --git a/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift b/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift index 08cfe72ef6..a9c1000119 100644 --- a/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift @@ -2,15 +2,15 @@ import Foundation -extension SnodeAPI { +extension Network.SnodeAPI { /// This is the legacy unauthenticated message store request - public struct LegacySendMessagesRequest: Encodable { + struct LegacySendMessagesRequest: Encodable { enum CodingKeys: String, CodingKey { case namespace } let message: SnodeMessage - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace // MARK: - Coding diff --git a/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift b/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift index eaef290853..2e0534cf18 100644 --- a/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift @@ -2,8 +2,8 @@ import Foundation -extension SnodeAPI { - public struct ONSResolveRequest: Encodable { +extension Network.SnodeAPI { + struct ONSResolveRequest: Encodable { enum CodingKeys: String, CodingKey { case type case base64EncodedNameHash = "name_hash" diff --git a/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift b/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift index 8ca850a123..527efed87a 100644 --- a/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift +++ b/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class ONSResolveResponse: SnodeResponse { +public extension Network.SnodeAPI { + class ONSResolveResponse: SnodeResponse { internal struct Result: Codable { enum CodingKeys: String, CodingKey { case nonce diff --git a/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift b/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift index 1b9d6ea453..93ff3933a2 100644 --- a/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift @@ -2,20 +2,22 @@ import Foundation -public struct OxenDaemonRPCRequest: Encodable { - private enum CodingKeys: String, CodingKey { - case endpoint - case body = "params" - } - - private let endpoint: String - private let body: T - - public init( - endpoint: SnodeAPI.Endpoint, - body: T - ) { - self.endpoint = endpoint.path - self.body = body +extension Network.SnodeAPI { + struct OxenDaemonRPCRequest: Encodable { + private enum CodingKeys: String, CodingKey { + case endpoint + case body = "params" + } + + private let endpoint: String + private let body: T + + public init( + endpoint: Network.SnodeAPI.Endpoint, + body: T + ) { + self.endpoint = endpoint.path + self.body = body + } } } diff --git a/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift b/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift index a7f1690e1a..7495a14258 100644 --- a/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class RevokeSubaccountRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class RevokeSubaccountRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case subaccountsToRevoke = "revoke" } @@ -14,7 +14,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("revoke_subaccount" || timestamp || SUBACCOUNT_TAG_BYTES...)`; this /// signs the subaccount token, using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.revokeSubaccount.path.bytes + Network.SnodeAPI.Endpoint.revokeSubaccount.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: Array(subaccountsToRevoke.joined())) } diff --git a/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift b/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift index b97ae8d672..69063606ac 100644 --- a/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift @@ -3,14 +3,14 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class SendMessageRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class SendMessageRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case namespace } let message: SnodeMessage - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace override var verificationBytes: [UInt8] { /// Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and @@ -18,7 +18,7 @@ extension SnodeAPI { /// base64 encoded for json requests; binary for OMQ requests. For non-05 type pubkeys (i.e. non /// session ids) the signature will be verified using `pubkey`. For 05 pubkeys, see the following /// option. - SnodeAPI.Endpoint.sendMessage.path.bytes + Network.SnodeAPI.Endpoint.sendMessage.path.bytes .appending(contentsOf: namespace.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -27,7 +27,7 @@ extension SnodeAPI { public init( message: SnodeMessage, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift b/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift index 4bcaa17c42..307e612059 100644 --- a/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift @@ -3,7 +3,7 @@ import Foundation import SessionUtilitiesKit -public class SnodeAuthenticatedRequestBody: Encodable { +class SnodeAuthenticatedRequestBody: Encodable { private enum CodingKeys: String, CodingKey { case pubkey case subaccount diff --git a/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift b/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift index e6a05c0c19..083dec184b 100644 --- a/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift @@ -3,7 +3,7 @@ import Foundation import SessionUtilitiesKit -internal extension SnodeAPI { +extension Network.SnodeAPI { struct BatchRequest: Encodable { let requests: [Child] @@ -38,7 +38,7 @@ internal extension SnodeAPI { case params } - let endpoint: SnodeAPI.Endpoint + let endpoint: Network.SnodeAPI.Endpoint /// The `jsonBodyEncoder` is used to avoid having to make `BatchSubRequest` a generic type (haven't found /// a good way to keep `BatchSubRequest` encodable using protocols unfortunately so need this work around) diff --git a/SessionNetworkingKit/StorageServer/Models/SnodeMessage.swift b/SessionNetworkingKit/StorageServer/Models/SnodeMessage.swift deleted file mode 100644 index a9fbcdd1e3..0000000000 --- a/SessionNetworkingKit/StorageServer/Models/SnodeMessage.swift +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -public final class SnodeMessage: Codable { - private enum CodingKeys: String, CodingKey { - case recipient = "pubkey" - case data - case ttl - case timestampMs = "timestamp" - } - - /// The hex encoded public key of the recipient. - public let recipient: String - - /// The content of the message. - public let data: String - - /// The time to live for the message in milliseconds. - public let ttl: UInt64 - - /// When the proof of work was calculated. - /// - /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. - public let timestampMs: UInt64 - - // MARK: - Initialization - - public init(recipient: String, data: Data, ttl: UInt64, timestampMs: UInt64) { - self.recipient = recipient - self.data = data.base64EncodedString() - self.ttl = ttl - self.timestampMs = timestampMs - } -} - -// MARK: - Codable - -extension SnodeMessage { - public convenience init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self.init( - recipient: try container.decode(String.self, forKey: .recipient), - data: try Data(base64Encoded: try container.decode(String.self, forKey: .data)) ?? { - throw NetworkError.parsingFailed - }(), - ttl: try container.decode(UInt64.self, forKey: .ttl), - timestampMs: try container.decode(UInt64.self, forKey: .timestampMs) - ) - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(recipient, forKey: .recipient) - try container.encode(data, forKey: .data) - try container.encode(ttl, forKey: .ttl) - try container.encode(timestampMs, forKey: .timestampMs) - } -} diff --git a/SessionNetworkingKit/StorageServer/Models/SnodeReceivedMessage.swift b/SessionNetworkingKit/StorageServer/Models/SnodeReceivedMessage.swift deleted file mode 100644 index 99604b49e8..0000000000 --- a/SessionNetworkingKit/StorageServer/Models/SnodeReceivedMessage.swift +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SessionUtilitiesKit - -public struct SnodeReceivedMessage: Codable, CustomDebugStringConvertible { - /// Service nodes cache messages for 14 days so default the expiration for message hashes to '15' days - /// so we don't end up indefinitely storing records which will never be used - public static let defaultExpirationMs: Int64 = ((15 * 24 * 60 * 60) * 1000) - - /// The storage server allows the timestamp within requests to be off by `60s` before erroring - public static let serverClockToleranceMs: Int64 = ((1 * 60) * 1000) - - public let snode: LibSession.Snode? - public let swarmPublicKey: String - public let namespace: SnodeAPI.Namespace - public let hash: String - public let timestampMs: Int64 - public let expirationTimestampMs: Int64 - public let data: Data - - public var info: SnodeReceivedMessageInfo? { - snode.map { snode in - SnodeReceivedMessageInfo( - snode: snode, - swarmPublicKey: swarmPublicKey, - namespace: namespace, - hash: hash, - expirationDateMs: expirationTimestampMs - ) - } - } - - public init?( - snode: LibSession.Snode?, - publicKey: String, - namespace: SnodeAPI.Namespace, - rawMessage: GetMessagesResponse.RawMessage - ) { - guard let data: Data = Data(base64Encoded: rawMessage.base64EncodedDataString) else { - Log.error(.network, "Failed to decode data for message: \(rawMessage).") - return nil - } - - self.snode = snode - self.swarmPublicKey = publicKey - self.namespace = namespace - self.hash = rawMessage.hash - self.timestampMs = rawMessage.timestampMs - self.expirationTimestampMs = (rawMessage.expirationMs ?? SnodeReceivedMessage.defaultExpirationMs) - self.data = data - } - - public var debugDescription: String { - """ - SnodeReceivedMessage( - swarmPublicKey: \(swarmPublicKey), - namespace: \(namespace), - hash: \(hash), - expirationTimestampMs: \(expirationTimestampMs), - timestampMs: \(timestampMs), - data: \(data.base64EncodedString()) - ) - """ - } -} diff --git a/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift b/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift index ab23b427b3..c88d159c8f 100644 --- a/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift @@ -9,13 +9,13 @@ public struct SnodeRequest: Encodable { case body = "params" } - internal let endpoint: SnodeAPI.Endpoint + internal let endpoint: Network.SnodeAPI.Endpoint internal let body: T // MARK: - Initialization public init( - endpoint: SnodeAPI.Endpoint, + endpoint: Network.SnodeAPI.Endpoint, body: T ) { self.endpoint = endpoint diff --git a/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift b/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift index 63fb0f6b40..3f8cb6f30d 100644 --- a/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UnrevokeSubaccountRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UnrevokeSubaccountRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case subaccountsToUnrevoke = "unrevoke" } @@ -14,7 +14,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("unrevoke_subaccount" || timestamp || subaccount)`; this signs /// the subaccount token, using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.unrevokeSubaccount.path.bytes + Network.SnodeAPI.Endpoint.unrevokeSubaccount.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: Array(subaccountsToUnrevoke.joined())) } diff --git a/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift index 184e767764..dbc4fbf3ff 100644 --- a/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift @@ -5,8 +5,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UpdateExpiryAllRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UpdateExpiryAllRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case expiryMs = "expiry" case namespace @@ -19,14 +19,14 @@ extension SnodeAPI { /// /// **Note:** If omitted when sending the request, message expiries are updated from the default namespace /// only (namespace 0) - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? override var verificationBytes: [UInt8] { /// Ed25519 signature of `("expire_all" || namespace || expiry)`, signed by `pubkey`. Must be /// base64 encoded (json) or bytes (OMQ). namespace should be the stringified namespace for /// non-default namespace expiries (i.e. "42", "-99", "all"), or an empty string for the default /// namespace (whether or not explicitly provided). - SnodeAPI.Endpoint.expireAll.path.bytes + Network.SnodeAPI.Endpoint.expireAll.path.bytes .appending( contentsOf: (namespace == nil ? "all" : @@ -40,7 +40,7 @@ extension SnodeAPI { public init( expiryMs: UInt64, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod ) { self.expiryMs = expiryMs diff --git a/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift index ba3546abac..6e237557d0 100644 --- a/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift @@ -5,8 +5,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UpdateExpiryRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UpdateExpiryRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" case expiryMs = "expiry" @@ -39,7 +39,7 @@ extension SnodeAPI { /// ` || messages[N])` where `expiry` is the expiry timestamp expressed as a string. /// `ShortenOrExtend` is string signature must be base64 "shorten" if the shorten option is given (and true), /// "extend" if `extend` is true, and empty otherwise. The signature must be base64 encoded (json) or bytes (bt). - SnodeAPI.Endpoint.expire.path.bytes + Network.SnodeAPI.Endpoint.expire.path.bytes .appending(contentsOf: (shorten == true ? "shorten".bytes : [])) .appending(contentsOf: (extend == true ? "extend".bytes : [])) .appending(contentsOf: "\(expiryMs)".data(using: .ascii)?.bytes) diff --git a/SessionNetworkingKit/StorageServer/SnodeAPI.swift b/SessionNetworkingKit/StorageServer/SnodeAPI.swift index d7d9ef9e3d..68e7357e4a 100644 --- a/SessionNetworkingKit/StorageServer/SnodeAPI.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPI.swift @@ -9,24 +9,788 @@ import Punycode import SessionUtilitiesKit public extension Network { - enum StorageServer { + enum SnodeAPI { + // MARK: - Settings + public static let maxRetryCount: Int = 8 + + // MARK: - Batching & Polling + + public typealias PollResponse = [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] + + public static func preparedPoll( + _ db: ObservingDatabase, + namespaces: [SnodeAPI.Namespace], + refreshingConfigHashes: [String] = [], + from snode: LibSession.Snode, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + // Determine the maxSize each namespace in the request should take up + var requests: [any ErasedPreparedRequest] = [] + let namespaceMaxSizeMap: [SnodeAPI.Namespace: Int64] = SnodeAPI.Namespace.maxSizeMap(for: namespaces) + let fallbackSize: Int64 = (namespaceMaxSizeMap.values.min() ?? 1) + + // If we have any config hashes to refresh TTLs then add those requests first + if !refreshingConfigHashes.isEmpty { + let updatedExpiryMS: Int64 = ( + dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + (30 * 24 * 60 * 60 * 1000) // 30 days + ) + requests.append( + try SnodeAPI.preparedUpdateExpiry( + serverHashes: refreshingConfigHashes, + updatedExpiryMs: updatedExpiryMS, + extendOnly: true, + ignoreValidationFailure: true, + explicitTargetNode: snode, + authMethod: authMethod, + using: dependencies + ) + ) + } + + // Add the various 'getMessages' requests + requests.append( + contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in + try SnodeAPI.preparedGetMessages( + db, + namespace: namespace, + snode: snode, + maxSize: namespaceMaxSizeMap[namespace] + .defaulting(to: fallbackSize), + authMethod: authMethod, + using: dependencies + ) + } + ) + + return try preparedBatch( + requests: requests, + requireAllBatchResponses: true, + snode: snode, + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + ) + .map { (_: ResponseInfoType, batchResponse: Network.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] in + let messageResponses: [Network.BatchSubResponse] = batchResponse + .compactMap { $0 as? Network.BatchSubResponse } + + return zip(namespaces, messageResponses) + .reduce(into: [:]) { result, next in + guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return } + + result[next.0] = (next.1, messageResponse) + } + } + } + + public static func preparedBatch( + requests: [any ErasedPreparedRequest], + requireAllBatchResponses: Bool, + snode: LibSession.Snode? = nil, + swarmPublicKey: String, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: { + switch snode { + case .none: + return try Request( + endpoint: .batch, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) + ) + + case .some(let snode): + return try Request( + endpoint: .batch, + snode: snode, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) + ) + } + }(), + responseType: Network.BatchResponse.self, + requireAllBatchResponses: requireAllBatchResponses, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + public static func preparedSequence( + requests: [any ErasedPreparedRequest], + requireAllBatchResponses: Bool, + swarmPublicKey: String, + snodeRetrievalRetryCount: Int, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .sequence, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests), + snodeRetrievalRetryCount: snodeRetrievalRetryCount + ), + responseType: Network.BatchResponse.self, + requireAllBatchResponses: requireAllBatchResponses, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + // MARK: - Retrieve + + public typealias PreparedGetMessagesResponse = (messages: [SnodeReceivedMessage], lastHash: String?) + + public static func preparedGetMessages( + _ db: ObservingDatabase, + namespace: SnodeAPI.Namespace, + snode: LibSession.Snode, + maxSize: Int64? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let maybeLastHash: String? = try SnodeReceivedMessageInfo + .fetchLastNotExpired( + db, + for: snode, + namespace: namespace, + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + )? + .hash + let preparedRequest: Network.PreparedRequest = try { + // Check if this namespace requires authentication + guard namespace.requiresReadAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .getMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: LegacyGetMessagesRequest( + pubkey: try authMethod.swarmPublicKey, + lastHash: (maybeLastHash ?? ""), + namespace: namespace, + maxCount: nil, + maxSize: maxSize + ) + ), + responseType: GetMessagesResponse.self, + using: dependencies + ) + } + + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .getMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: GetMessagesRequest( + lastHash: (maybeLastHash ?? ""), + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), + maxSize: maxSize + ) + ), + responseType: GetMessagesResponse.self, + using: dependencies + ) + }() + + return preparedRequest + .tryMap { _, response -> (messages: [SnodeReceivedMessage], lastHash: String?) in + return ( + try response.messages.compactMap { rawMessage -> SnodeReceivedMessage? in + SnodeReceivedMessage( + snode: snode, + publicKey: try authMethod.swarmPublicKey, + namespace: namespace, + rawMessage: rawMessage + ) + }, + maybeLastHash + ) + } + } + + public static func getSessionID( + for onsName: String, + using dependencies: Dependencies + ) -> AnyPublisher { + let validationCount = 3 + + // The name must be lowercased + let onsName = onsName.lowercased().idnaEncoded ?? onsName.lowercased() + + // Hash the ONS name using BLAKE2b + guard + let nameHash = dependencies[singleton: .crypto].generate( + .hash(message: Array(onsName.utf8)) + ) + else { + return Fail(error: SnodeAPIError.onsHashingFailed) + .eraseToAnyPublisher() + } + + // Ask 3 different snodes for the Session ID associated with the given name hash + let base64EncodedNameHash = nameHash.toBase64() + + return dependencies[singleton: .network] + .getRandomNodes(count: validationCount) + .tryFlatMap { nodes in + Publishers.MergeMany( + try nodes.map { snode in + try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .oxenDaemonRPCCall, + snode: snode, + body: OxenDaemonRPCRequest( + endpoint: .daemonOnsResolve, + body: ONSResolveRequest( + type: 0, // type 0 means Session + base64EncodedNameHash: base64EncodedNameHash + ) + ) + ), + responseType: ONSResolveResponse.self, + using: dependencies + ) + .tryMap { _, response -> String in + try dependencies[singleton: .crypto].tryGenerate( + .sessionId(name: onsName, response: response) + ) + } + .send(using: dependencies) + .map { _, sessionId in sessionId } + .eraseToAnyPublisher() + } + ) + } + .collect() + .tryMap { results -> String in + guard results.count == validationCount, Set(results).count == 1 else { + throw SnodeAPIError.onsValidationFailed + } + + return results[0] + } + .eraseToAnyPublisher() + } + + public static func preparedGetExpiries( + of serverHashes: [String], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .getExpiries, + swarmPublicKey: try authMethod.swarmPublicKey, + body: GetExpiriesRequest( + messageHashes: serverHashes, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ) + ), + responseType: GetExpiriesResponse.self, + using: dependencies + ) + } + + // MARK: - Store + + public static func preparedSendMessage( + message: SnodeMessage, + in namespace: Namespace, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let request: Network.PreparedRequest = try { + // Check if this namespace requires authentication + guard namespace.requiresWriteAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .sendMessage, + swarmPublicKey: try authMethod.swarmPublicKey, + body: LegacySendMessagesRequest( + message: message, + namespace: namespace + ), + snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + ), + responseType: SendMessagesResponse.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + } + + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .sendMessage, + swarmPublicKey: try authMethod.swarmPublicKey, + body: SendMessageRequest( + message: message, + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ), + snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + ), + responseType: SendMessagesResponse.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + }() + + return request + .tryMap { _, response -> SendMessagesResponse in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + ) + + return response + } + } + + // MARK: - Edit + + public static func preparedUpdateExpiry( + serverHashes: [String], + updatedExpiryMs: Int64, + shortenOnly: Bool? = nil, + extendOnly: Bool? = nil, + ignoreValidationFailure: Bool = false, + explicitTargetNode: LibSession.Snode? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> { + // ShortenOnly and extendOnly cannot be true at the same time + guard shortenOnly == nil || extendOnly == nil else { throw NetworkError.invalidPreparedRequest } + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .expire, + swarmPublicKey: try authMethod.swarmPublicKey, + body: UpdateExpiryRequest( + messageHashes: serverHashes, + expiryMs: UInt64(updatedExpiryMs), + shorten: shortenOnly, + extend: extendOnly, + authMethod: authMethod + ) + ), + responseType: UpdateExpiryResponse.self, + using: dependencies + ) + .tryMap { _, response -> [String: UpdateExpiryResponseResult] in + do { + return try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: serverHashes, + using: dependencies + ) + } + catch { + guard ignoreValidationFailure else { throw error } + + return [:] + } + } + .handleEvents( + receiveOutput: { _, result in + /// Since we have updated the TTL we need to make sure we also update the local + /// `SnodeReceivedMessageInfo.expirationDateMs` values so they match the updated swarm, if + /// we had a specific `snode` we we're sending the request to then we should use those values, otherwise + /// we can just grab the first value from the response and use that + let maybeTargetResult: UpdateExpiryResponseResult? = { + guard let snode: LibSession.Snode = explicitTargetNode else { + return result.first?.value + } + + return result[snode.ed25519PubkeyHex] + }() + guard + let targetResult: UpdateExpiryResponseResult = maybeTargetResult, + let groupedExpiryResult: [UInt64: [String]] = targetResult.changed + .updated(with: targetResult.unchanged) + .groupedByValue() + .nullIfEmpty + else { return } + + dependencies[singleton: .storage].writeAsync { db in + try groupedExpiryResult.forEach { updatedExpiry, hashes in + try SnodeReceivedMessageInfo + .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash)) + .updateAll( + db, + SnodeReceivedMessageInfo.Columns.expirationDateMs + .set(to: updatedExpiry) + ) + } + } + } + ) + } + + public static func preparedRevokeSubaccounts( + subaccountsToRevoke: [[UInt8]], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .revokeSubaccount, + swarmPublicKey: try authMethod.swarmPublicKey, + body: RevokeSubaccountRequest( + subaccountsToRevoke: subaccountsToRevoke, + authMethod: authMethod, + timestampMs: timestampMs + ) + ), + responseType: RevokeSubaccountResponse.self, + using: dependencies + ) + .tryMap { _, response -> Void in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: (subaccountsToRevoke, timestampMs), + using: dependencies + ) + + return () + } + } + + public static func preparedUnrevokeSubaccounts( + subaccountsToUnrevoke: [[UInt8]], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .unrevokeSubaccount, + swarmPublicKey: try authMethod.swarmPublicKey, + body: UnrevokeSubaccountRequest( + subaccountsToUnrevoke: subaccountsToUnrevoke, + authMethod: authMethod, + timestampMs: timestampMs + ) + ), + responseType: UnrevokeSubaccountResponse.self, + using: dependencies + ) + .tryMap { _, response -> Void in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: (subaccountsToUnrevoke, timestampMs), + using: dependencies + ) + + return () + } + } + + // MARK: - Delete + + public static func preparedDeleteMessages( + serverHashes: [String], + requireSuccessfulDeletion: Bool, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: DeleteMessagesRequest( + messageHashes: serverHashes, + requireSuccessfulDeletion: requireSuccessfulDeletion, + authMethod: authMethod + ) + ), + responseType: DeleteMessagesResponse.self, + using: dependencies + ) + .tryMap { _, response -> [String: Bool] in + let validResultMap: [String: Bool] = try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: serverHashes, + using: dependencies + ) + + // If `validResultMap` didn't throw then at least one service node + // deleted successfully so we should mark the hash as invalid so we + // don't try to fetch updates using that hash going forward (if we + // do we would end up re-fetching all old messages) + dependencies[singleton: .storage].writeAsync { db in + try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: serverHashes + ) + } + + return validResultMap + } + } + + + /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. + public static func preparedDeleteAllMessages( + namespace: SnodeAPI.Namespace, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAll, + swarmPublicKey: try authMethod.swarmPublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllMessagesRequest( + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ), + snodeRetrievalRetryCount: 0 + ), + responseType: DeleteAllMessagesResponse.self, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + .tryMap { info, response -> [String: Bool] in + guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else { + throw NetworkError.invalidResponse + } + + return try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: targetInfo.timestampMs, + using: dependencies + ) + } + } + + /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. + public static func preparedDeleteAllMessages( + beforeMs: UInt64, + namespace: SnodeAPI.Namespace, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAllBefore, + swarmPublicKey: try authMethod.swarmPublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllBeforeRequest( + beforeMs: beforeMs, + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ) + ), + responseType: DeleteAllMessagesResponse.self, + retryCount: maxRetryCount, + using: dependencies + ) + .tryMap { _, response -> [String: Bool] in + try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: beforeMs, + using: dependencies + ) + } + } + + // MARK: - Internal API + + public static func preparedGetNetworkTime( + from snode: LibSession.Snode, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request, Endpoint>( + endpoint: .getInfo, + snode: snode, + body: [:] + ), + responseType: GetNetworkTimestampResponse.self, + using: dependencies + ) + .map { _, response in + // Assume we've fetched the networkTime in order to send a message to the specified snode, in + // which case we want to update the 'clockOffsetMs' value for subsequent requests + let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + dependencies.mutate(cache: .snodeAPI) { $0.setClockOffsetMs(offset) } + + return response.timestamp + } + } + + // MARK: - Convenience + + private static func prepareRequest( + request: Request, + responseType: R.Type, + requireAllBatchResponses: Bool = true, + retryCount: Int = 0, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: request, + responseType: responseType, + requireAllBatchResponses: requireAllBatchResponses, + retryCount: retryCount, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + switch response { + case let snodeResponse as SnodeResponse: + // Update the network offset based on the response so subsequent requests have + // the correct network offset time + let offset = (Int64(snodeResponse.timeOffset) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + dependencies.mutate(cache: .snodeAPI) { + $0.setClockOffsetMs(offset) + + // Extract and store hard fork information if returned + guard snodeResponse.hardForkVersion.count > 1 else { return } + + if snodeResponse.hardForkVersion[1] > $0.softfork { + $0.softfork = snodeResponse.hardForkVersion[1] + dependencies[defaults: .standard, key: .softfork] = $0.softfork + } + + if snodeResponse.hardForkVersion[0] > $0.hardfork { + $0.hardfork = snodeResponse.hardForkVersion[0] + dependencies[defaults: .standard, key: .hardfork] = $0.hardfork + $0.softfork = snodeResponse.hardForkVersion[1] + dependencies[defaults: .standard, key: .softfork] = $0.softfork + } + } + + default: break + } + } + ) + } } } -// MARK: - Network.StorageServer.Cache +// MARK: - Publisher Convenience -public extension Cache { - static let storageServer: CacheConfig = Dependencies.create( - identifier: "storageServer", - createInstance: { dependencies in Network.StorageServer.Cache(using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } - ) +public extension Publisher where Output == Set { + func tryMapWithRandomSnode( + using dependencies: Dependencies, + _ transform: @escaping (LibSession.Snode) throws -> T + ) -> AnyPublisher { + return self + .tryMap { swarm -> T in + var remainingSnodes: Set = swarm + let snode: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { + throw SnodeAPIError.insufficientSnodes + }() + + return try transform(snode) + } + .eraseToAnyPublisher() + } + + func tryFlatMapWithRandomSnode( + maxPublishers: Subscribers.Demand = .unlimited, + retry retries: Int = 0, + drainBehaviour: ThreadSafeObject = .alwaysRandom, + using dependencies: Dependencies, + _ transform: @escaping (LibSession.Snode) throws -> P + ) -> AnyPublisher where T == P.Output, P: Publisher, P.Failure == Error { + return self + .mapError { $0 } + .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in + // If we don't want to reuse a specific snode multiple times then just grab a + // random one from the swarm every time + var remainingSnodes: Set = drainBehaviour.performUpdateAndMap { behaviour in + switch behaviour { + case .alwaysRandom: return (behaviour, swarm) + case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash): + // If we've used all of the snodes or the swarm has changed then reset the used list + guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else { + return (behaviour.reset(), swarm) + } + + return (behaviour, swarm.subtracting(usedSnodes)) + } + } + var lastError: Error? + + return Just(()) + .setFailureType(to: Error.self) + .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in + let snode: LibSession.Snode = try drainBehaviour.performUpdateAndMap { behaviour in + switch behaviour { + case .limitedReuse(_, .some(let targetSnode), _, _, _): + return (behaviour.use(snode: targetSnode, from: swarm), targetSnode) + default: break + } + + // Select the next snode + let result: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { + throw SnodeAPIError.ranOutOfRandomSnodes(lastError) + }() + + return (behaviour.use(snode: result, from: swarm), result) + } + + return try transform(snode) + .eraseToAnyPublisher() + } + .mapError { error in + // Prevent nesting the 'ranOutOfRandomSnodes' errors + switch error { + case SnodeAPIError.ranOutOfRandomSnodes: break + default: lastError = error + } + + return error + } + .retry(retries) + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } } -public extension Network.StorageServer { - class Cache: StorageServerCacheType { +// MARK: - SnodeAPI Cache + +public extension Network.SnodeAPI { + class Cache: SnodeAPICacheType { private let dependencies: Dependencies public var hardfork: Int public var softfork: Int @@ -55,10 +819,19 @@ public extension Network.StorageServer { } } +public extension Cache { + static let snodeAPI: CacheConfig = Dependencies.create( + identifier: "snodeAPI", + createInstance: { dependencies in Network.SnodeAPI.Cache(using: dependencies) }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + // MARK: - SnodeAPICacheType /// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol StorageServerImmutableCacheType: ImmutableCacheType { +public protocol SnodeAPIImmutableCacheType: ImmutableCacheType { /// The last seen storage server hard fork version. var hardfork: Int { get } @@ -74,7 +847,7 @@ public protocol StorageServerImmutableCacheType: ImmutableCacheType { func currentOffsetTimestampMs() -> T } -public protocol StorageServerCacheType: StorageServerImmutableCacheType, MutableCacheType { +public protocol SnodeAPICacheType: SnodeAPIImmutableCacheType, MutableCacheType { /// The last seen storage server hard fork version. var hardfork: Int { get set } diff --git a/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift b/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift index 7707eb28a1..6bd1f1b1a9 100644 --- a/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift @@ -4,7 +4,7 @@ import Foundation -public extension Network.StorageServer { +public extension Network.SnodeAPI { enum Endpoint: EndpointType { case sendMessage case getMessages diff --git a/SessionNetworkingKit/StorageServer/SnodeAPIError.swift b/SessionNetworkingKit/StorageServer/SnodeAPIError.swift index 759118a11e..c09b5ed963 100644 --- a/SessionNetworkingKit/StorageServer/SnodeAPIError.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPIError.swift @@ -4,7 +4,7 @@ import Foundation -public enum StorageServerError: Error, CustomStringConvertible { +public enum SnodeAPIError: Error, CustomStringConvertible { case clockOutOfSync case snodePoolUpdatingFailed case inconsistentSnodePools diff --git a/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift b/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift index c2c595f731..ea3399d543 100644 --- a/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift @@ -6,7 +6,7 @@ import Foundation import SessionUtil import SessionUtilitiesKit -public extension Network.StorageServer { +public extension Network.SnodeAPI { enum Namespace: Int, Codable, Hashable, CustomStringConvertible { /// Messages sent to one-to-one conversations are stored in this namespace case `default` = 0 diff --git a/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift b/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift index 79ee173412..4f2c50e84f 100644 --- a/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift +++ b/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift @@ -5,7 +5,7 @@ import Foundation import SessionUtilitiesKit -public extension Request where Endpoint == Network.StorageServer.Endpoint { +public extension Request where Endpoint == Network.SnodeAPI.Endpoint { init( endpoint: Endpoint, snode: LibSession.Snode, @@ -29,7 +29,7 @@ public extension Request where Endpoint == Network.StorageServer.Endpoint { endpoint: Endpoint, swarmPublicKey: String, body: B, - snodeRetrievalRetryCount: Int = Network.StorageServer.maxRetryCount + snodeRetrievalRetryCount: Int = Network.SnodeAPI.maxRetryCount ) throws where T == SnodeRequest { self = try Request( endpoint: endpoint, @@ -49,7 +49,7 @@ public extension Request where Endpoint == Network.StorageServer.Endpoint { swarmPublicKey: String, requiresLatestNetworkTime: Bool, body: B, - snodeRetrievalRetryCount: Int = Network.StorageServer.maxRetryCount + snodeRetrievalRetryCount: Int = Network.SnodeAPI.maxRetryCount ) throws where T == SnodeRequest, B: Encodable & UpdatableTimestamp { self = try Request( endpoint: endpoint, diff --git a/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift b/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift index 99604b49e8..ad2384fbbb 100644 --- a/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift +++ b/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift @@ -15,7 +15,7 @@ public struct SnodeReceivedMessage: Codable, CustomDebugStringConvertible { public let snode: LibSession.Snode? public let swarmPublicKey: String - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace public let hash: String public let timestampMs: Int64 public let expirationTimestampMs: Int64 @@ -36,7 +36,7 @@ public struct SnodeReceivedMessage: Codable, CustomDebugStringConvertible { public init?( snode: LibSession.Snode?, publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, rawMessage: GetMessagesResponse.RawMessage ) { guard let data: Data = Data(base64Encoded: rawMessage.base64EncodedDataString) else { diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 5913775129..a11afeb54c 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -15,22 +15,6 @@ public extension Singleton { ) } -// MARK: - NetworkType - -public protocol NetworkType { - func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> - func getRandomNodes(count: Int) -> AnyPublisher, Error> - - func send( - _ body: Data?, - to destination: Network.Destination, - requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> - - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> -} - // MARK: - Network Constants public class Network { @@ -52,118 +36,18 @@ public enum NetworkStatus { case disconnected } -// MARK: - FileServer Convenience +// MARK: - NetworkType -public extension Network { - enum NetworkAPI { - static let networkAPIServer = "http://networkv1.getsession.org" - static let networkAPIServerPublicKey = "cbf461a4431dc9174dceef4421680d743a2a0e1a3131fc794240bcb0bc3dd449" - - public enum Endpoint: EndpointType { - case info - case price - case token - - public static var name: String { "NetworkAPI.Endpoint" } - - public var path: String { - switch self { - case .info: return "info" - case .price: return "price" - case .token: return "token" - } - } - } - } - - enum FileServer { - fileprivate static let fileServer = "http://filev2.getsession.org" - fileprivate static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" - fileprivate static let legacyFileServer = "http://88.99.175.227" - fileprivate static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - - public enum Endpoint: EndpointType { - case file - case fileIndividual(String) - case directUrl(URL) - case sessionVersion - - public static var name: String { "FileServerAPI.Endpoint" } - - public var path: String { - switch self { - case .file: return "file" - case .fileIndividual(let fileId): return "file/\(fileId)" - case .directUrl(let url): return url.path.removingPrefix("/") - case .sessionVersion: return "session_version" - } - } - } - - static func fileServerPubkey(url: String? = nil) -> String { - switch url?.contains(legacyFileServer) { - case true: return legacyFileServerPublicKey - default: return fileServerPublicKey - } - } - - static func isFileServerUrl(url: URL) -> Bool { - return ( - url.absoluteString.starts(with: fileServer) || - url.absoluteString.starts(with: legacyFileServer) - ) - } - - public static func downloadUrlString(for url: String, fileId: String) -> String { - switch url.contains(legacyFileServer) { - case true: return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" - default: return downloadUrlString(for: fileId) - } - } - - public static func downloadUrlString(for fileId: String) -> String { - return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" - } - } +public protocol NetworkType { + func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> + func getRandomNodes(count: Int) -> AnyPublisher, Error> - static func preparedUpload( - data: Data, - requestAndPathBuildTimeout: TimeInterval? = nil, - using dependencies: Dependencies - ) throws -> PreparedRequest { - return try PreparedRequest( - request: Request( - endpoint: FileServer.Endpoint.file, - destination: .serverUpload( - server: FileServer.fileServer, - x25519PublicKey: FileServer.fileServerPublicKey, - fileName: nil - ), - body: data - ), - responseType: FileUploadResponse.self, - requestTimeout: Network.fileUploadTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - } + func send( + _ body: Data?, + to destination: Network.Destination, + requestTimeout: TimeInterval, + requestAndPathBuildTimeout: TimeInterval? + ) -> AnyPublisher<(ResponseInfoType, Data?), Error> - static func preparedDownload( - url: URL, - using dependencies: Dependencies - ) throws -> PreparedRequest { - return try PreparedRequest( - request: Request( - endpoint: FileServer.Endpoint.directUrl(url), - destination: .serverDownload( - url: url, - x25519PublicKey: FileServer.fileServerPublicKey, - fileName: nil - ) - ), - responseType: Data.self, - requestTimeout: Network.fileUploadTimeout, - using: dependencies - ) - } + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> } diff --git a/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift b/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift new file mode 100644 index 0000000000..92862325b1 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift @@ -0,0 +1,50 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +// MARK: - Authentication Types + +public extension Authentication { + /// Used when interacting with a community + struct community: AuthenticationMethod { + public let roomToken: String + public let server: String + public let publicKey: String + public let hasCapabilities: Bool + public let supportsBlinding: Bool + public let forceBlinded: Bool + + public var info: Info { + .community( + server: server, + publicKey: publicKey, + hasCapabilities: hasCapabilities, + supportsBlinding: supportsBlinding, + forceBlinded: forceBlinded + ) + } + + public init( + roomToken: String, + server: String, + publicKey: String, + hasCapabilities: Bool, + supportsBlinding: Bool, + forceBlinded: Bool = false + ) { + self.roomToken = roomToken + self.server = server + self.publicKey = publicKey + self.hasCapabilities = hasCapabilities + self.supportsBlinding = supportsBlinding + self.forceBlinded = forceBlinded + } + + // MARK: - SignatureGenerator + + public func generateSignature(with verificationBytes: [UInt8], using dependencies: Dependencies) throws -> Authentication.Signature { + throw CryptoError.signatureGenerationFailed + } + } +} diff --git a/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift b/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift new file mode 100644 index 0000000000..d141b5549f --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift @@ -0,0 +1,180 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class CryptoSOGSAPISpec: QuickSpec { + override class func spec() { + // MARK: Configuration + + @TestState var dependencies: TestDependencies! = TestDependencies() + @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) + @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( + initialSetup: { cache in + cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + } + ) + + // MARK: - Crypto for SOGSAPI + describe("Crypto for SOGSAPI") { + // MARK: -- when generating a blinded15 key pair + context("when generating a blinded15 key pair") { + // MARK: ---- successfully generates + it("successfully generates") { + let result = crypto.generate( + .blinded15KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ + expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind15PublicKey)) + expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind15SecretKey)) + } + + // MARK: ---- fails if the edKeyPair secret key length wrong + it("fails if the ed25519SecretKey length wrong") { + let result = crypto.generate( + .blinded15KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Array(Data(hex: String(TestConstants.edSecretKey.prefix(4)))) + ) + ) + + expect(result).to(beNil()) + } + } + + // MARK: -- when generating a blinded25 key pair + context("when generating a blinded25 key pair") { + // MARK: ---- successfully generates + it("successfully generates") { + let result = crypto.generate( + .blinded25KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ + expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind25PublicKey)) + expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind25SecretKey)) + } + + // MARK: ---- fails if the edKeyPair secret key length wrong + it("fails if the ed25519SecretKey length wrong") { + let result = crypto.generate( + .blinded25KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes + ) + ) + + expect(result).to(beNil()) + } + } + + // MARK: -- when generating a signatureBlind15 + context("when generating a signatureBlind15") { + // MARK: ---- generates a correct signature + it("generates a correct signature") { + let result = crypto.generate( + .signatureBlind15( + message: "TestMessage".bytes, + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + + expect(result?.toHexString()) + .to(equal( + "245003f1627ebdfc6099c32597d426ef84d1b301861a5ffbbac92dde6c608334" + + "ceb56a022a094a9a664fae034b50eed40bd1bfb262c7e542c979eec265ae3f07" + )) + } + } + + // MARK: -- when generating a signatureBlind25 + context("when generating a signatureBlind25") { + // MARK: ---- generates a correct signature + it("generates a correct signature") { + let result = crypto.generate( + .signatureBlind25( + message: "TestMessage".bytes, + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + expect(result?.toHexString()) + .to(equal( + "9ff9b7fb7d435c7a2c0b0b2ae64963baaf394386b9f7c7f924eeac44ec0f74c7" + + "fe6304c73a9b3a65491f81e44b545e54631e83e9a412eaed5fd4db2e05ec830c" + )) + } + } + + // MARK: -- when checking if a session id matches a blinded id + context("when checking if a session id matches a blinded id") { + // MARK: ---- returns true when a blind15 id matches + it("returns true when a blind15 id matches") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "15\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beTrue()) + } + + // MARK: ---- returns true when a blind25 id matches + it("returns true when a blind25 id matches") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "25\(TestConstants.blind25PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beTrue()) + } + + // MARK: ---- returns false if given an invalid session id + it("returns false if given an invalid session id") { + let result = crypto.verify( + .sessionId( + "AB\(TestConstants.publicKey)", + matchesBlindedId: "15\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beFalse()) + } + + // MARK: ---- returns false if given an invalid blinded id + it("returns false if given an invalid blinded id") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "AB\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beFalse()) + } + } + } + } +} diff --git a/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift b/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift new file mode 100644 index 0000000000..4b6ee85475 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift @@ -0,0 +1,38 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class CapabilitiesResponseSpec: QuickSpec { + override class func spec() { + // MARK: - CapabilitiesResponse + describe("CapabilitiesResponse") { + // MARK: -- when initializing + context("when initializing") { + // MARK: ---- assigns values correctly + it("assigns values correctly") { + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"], + missing: ["test"] + ) + + expect(capabilities.capabilities).to(equal(["sogs"])) + expect(capabilities.missing).to(equal(["test"])) + } + + it("defaults missing to nil") { + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"] + ) + + expect(capabilities.capabilities).to(equal(["sogs"])) + expect(capabilities.missing).to(beNil()) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift b/SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift similarity index 92% rename from SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift index 386f2f02bc..f088a86205 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class RoomPollInfoSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class RoomPollInfoSpec: QuickSpec { context("when initializing with a room") { // MARK: ---- copies all the relevant values across it("copies all the relevant values across") { - let room: OpenGroupAPI.Room = OpenGroupAPI.Room( + let room: Network.SOGS.Room = Network.SOGS.Room( token: "testToken", name: "testName", roomDescription: nil, @@ -42,7 +42,7 @@ class RoomPollInfoSpec: QuickSpec { upload: true, defaultUpload: true ) - let roomPollInfo: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo(room: room) + let roomPollInfo: Network.SOGS.RoomPollInfo = Network.SOGS.RoomPollInfo(room: room) expect(roomPollInfo.token).to(equal(room.token)) expect(roomPollInfo.activeUsers).to(equal(room.activeUsers)) @@ -82,7 +82,7 @@ class RoomPollInfoSpec: QuickSpec { } """ let roomData: Data = roomPollInfoJson.data(using: .utf8)! - let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + let result: Network.SOGS.RoomPollInfo = try! JSONDecoder().decode(Network.SOGS.RoomPollInfo.self, from: roomData) expect(result.admin).to(beFalse()) expect(result.globalAdmin).to(beFalse()) @@ -115,7 +115,7 @@ class RoomPollInfoSpec: QuickSpec { } """ let roomData: Data = roomPollInfoJson.data(using: .utf8)! - let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + let result: Network.SOGS.RoomPollInfo = try! JSONDecoder().decode(Network.SOGS.RoomPollInfo.self, from: roomData) expect(result.admin).to(beTrue()) expect(result.globalAdmin).to(beTrue()) diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift b/SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift similarity index 93% rename from SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift index 2fd43d679b..2238a9e021 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class RoomSpec: QuickSpec { override class func spec() { @@ -45,7 +45,7 @@ class RoomSpec: QuickSpec { } """ let roomData: Data = roomJson.data(using: .utf8)! - let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + let result: Network.SOGS.Room = try! JSONDecoder().decode(Network.SOGS.Room.self, from: roomData) expect(result.admin).to(beFalse()) expect(result.globalAdmin).to(beFalse()) @@ -89,7 +89,7 @@ class RoomSpec: QuickSpec { } """ let roomData: Data = roomJson.data(using: .utf8)! - let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + let result: Network.SOGS.Room = try! JSONDecoder().decode(Network.SOGS.Room.self, from: roomData) expect(result.admin).to(beTrue()) expect(result.globalAdmin).to(beTrue()) diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift similarity index 92% rename from SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift index ee3f4d94ac..315f6e0e33 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift @@ -1,13 +1,12 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionNetworkingKit import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SOGSMessageSpec: QuickSpec { override class func spec() { @@ -45,7 +44,7 @@ class SOGSMessageSpec: QuickSpec { } """ messageData = messageJson.data(using: .utf8)! - let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + let result: Network.SOGS.Message? = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(result).toNot(beNil()) expect(result?.whisper).to(beFalse()) @@ -66,7 +65,7 @@ class SOGSMessageSpec: QuickSpec { } """ messageData = messageJson.data(using: .utf8)! - let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + let result: Network.SOGS.Message? = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(result).toNot(beNil()) expect(result?.sender).to(beNil()) @@ -94,7 +93,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -117,7 +116,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -140,7 +139,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -150,7 +149,7 @@ class SOGSMessageSpec: QuickSpec { decoder = JSONDecoder() expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(DependenciesError.missingDependencies)) } @@ -173,7 +172,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -204,7 +203,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(true) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .toNot(beNil()) } @@ -215,7 +214,7 @@ class SOGSMessageSpec: QuickSpec { .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(true) - _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + _ = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(mockCrypto) .to(call(matchingParameters: .all) { @@ -236,7 +235,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(false) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -251,7 +250,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(true) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .toNot(beNil()) } @@ -262,7 +261,7 @@ class SOGSMessageSpec: QuickSpec { .when { $0.verify(.signatureXed25519(.any, curve25519PublicKey: .any, data: .any)) } .thenReturn(true) - _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + _ = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(mockCrypto) .to(call(matchingParameters: .all) { @@ -283,7 +282,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(false) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } diff --git a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift similarity index 86% rename from SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift index 27e96dd206..c7c462f307 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SendDirectMessageRequestSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class SendDirectMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.SendDirectMessageRequest = OpenGroupAPI.SendDirectMessageRequest( + let request: Network.SOGS.SendDirectMessageRequest = Network.SOGS.SendDirectMessageRequest( message: "TestData".data(using: .utf8)! ) let requestData: Data = try! JSONEncoder().encode(request) diff --git a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift similarity index 82% rename from SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift index ec0c15d380..9c00a94671 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift @@ -5,17 +5,17 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit -class SendMessageRequestSpec: QuickSpec { +class SendSOGSMessageRequestSpec: QuickSpec { override class func spec() { - // MARK: - a SendMessageRequest - describe("a SendMessageRequest") { + // MARK: - a SendSOGSMessageRequest + describe("a SendSOGSMessageRequest") { // MARK: -- when initializing context("when initializing") { // MARK: ---- defaults the optional values to nil it("defaults the optional values to nil") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)! ) @@ -30,7 +30,7 @@ class SendMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, whisperTo: nil, @@ -46,7 +46,7 @@ class SendMessageRequestSpec: QuickSpec { // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, whisperTo: nil, diff --git a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift similarity index 87% rename from SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift index 106bd04c52..847cdf7ac2 100644 --- a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class UpdateMessageRequestSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class UpdateMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + let request: Network.SOGS.UpdateMessageRequest = Network.SOGS.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, fileIds: nil @@ -29,7 +29,7 @@ class UpdateMessageRequestSpec: QuickSpec { // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { - let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + let request: Network.SOGS.UpdateMessageRequest = Network.SOGS.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, fileIds: nil diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift similarity index 74% rename from SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift rename to SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift index 73ae2a3cc6..877b6f31da 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift @@ -3,15 +3,14 @@ import Foundation import Combine import GRDB -import SessionNetworkingKit import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit -class OpenGroupAPISpec: QuickSpec { +class SOGSAPISpec: QuickSpec { override class func spec() { // MARK: Configuration @@ -73,24 +72,21 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? - // MARK: - an OpenGroupAPI - describe("an OpenGroupAPI") { + // MARK: - a SOGSAPI + describe("a SOGSAPI") { // MARK: -- when preparing a poll request context("when preparing a poll request") { - @TestState var preparedRequest: Network.PreparedRequest>? + @TestState var preparedRequest: Network.PreparedRequest>? // MARK: ---- generates the correct request it("generates the correct request") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -98,15 +94,15 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -116,20 +112,20 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/batch")) expect(preparedRequest?.method.rawValue).to(equal("POST")) expect(preparedRequest?.batchEndpoints.count).to(equal(3)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomPollInfo("testRoom", 0))) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was no last message it("retrieves recent messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -137,31 +133,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago it("retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 121 @@ -169,31 +165,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, - timeSinceLastPoll: (CommunityPoller.maxInactivityPeriod + 1), + timeSinceLastPoll: (Network.SOGS.maxInactivityPeriodForPolling + 1), authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago it("retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 122 @@ -201,31 +197,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesSince("testRoom", seqNo: 122))) } // MARK: ---- retrieves recent messages if there was a last message and there has already been a poll this session it("retrieves recent messages if there was a last message and there has already been a poll this session") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 123 @@ -233,22 +229,22 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesSince("testRoom", seqNo: 123))) } @@ -257,9 +253,9 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ does not call the inbox and outbox endpoints it("does not call the inbox and outbox endpoints") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -267,38 +263,34 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.outbox)) } } // MARK: ---- when blinded and checking for message requests context("when blinded and checking for message requests") { - beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(true) - } - // MARK: ------ includes the inbox and outbox endpoints it("includes the inbox and outbox endpoints") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -306,31 +298,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inbox)) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outbox)) } // MARK: ------ retrieves recent inbox messages if there was no last message it("retrieves recent inbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -338,30 +330,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inbox)) } // MARK: ------ retrieves inbox messages since the last message if there was one it("retrieves inbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -369,30 +361,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 124, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inboxSince(id: 124))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inboxSince(id: 124))) } // MARK: ------ retrieves recent outbox messages if there was no last message it("retrieves recent outbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -400,30 +392,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outbox)) } // MARK: ------ retrieves outbox messages since the last message if there was one it("retrieves outbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -431,37 +423,33 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 125, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outboxSince(id: 125))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outboxSince(id: 125))) } } // MARK: ---- when blinded and not checking for message requests context("when blinded and not checking for message requests") { - beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(false) - } - // MARK: ------ includes the inbox and outbox endpoints it("does not include the inbox endpoint") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -469,30 +457,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) } // MARK: ------ does not retrieve recent inbox messages if there was no last message it("does not retrieve recent inbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -500,30 +488,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) } // MARK: ------ does not retrieve inbox messages since the last message if there was one it("does not retrieve inbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -531,22 +519,22 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 124, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inboxSince(id: 124))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inboxSince(id: 124))) } } } @@ -555,16 +543,15 @@ class OpenGroupAPISpec: QuickSpec { context("when preparing a capabilities request") { // MARK: ---- generates the request correctly it("generates the request and handles the response correctly") { - var preparedRequest: Network.PreparedRequest? + var preparedRequest: Network.PreparedRequest? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilities( + preparedRequest = try Network.SOGS.preparedCapabilities( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -580,16 +567,15 @@ class OpenGroupAPISpec: QuickSpec { context("when preparing a rooms request") { // MARK: ---- generates the request correctly it("generates the request correctly") { - var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -603,20 +589,19 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a capabilitiesAndRoom request context("when preparing a capabilitiesAndRoom request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -624,9 +609,9 @@ class OpenGroupAPISpec: QuickSpec { }.toNot(throwError()) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.room("testRoom"))) expect(preparedRequest?.path).to(equal("/sequence")) @@ -639,18 +624,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -675,18 +659,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndBanResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -709,18 +692,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockBanAndRoomResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -740,22 +722,21 @@ class OpenGroupAPISpec: QuickSpec { } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when preparing a capabilitiesAndRooms request context("when preparing a capabilitiesAndRooms request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -763,9 +744,9 @@ class OpenGroupAPISpec: QuickSpec { }.toNot(throwError()) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.rooms)) expect(preparedRequest?.path).to(equal("/sequence")) @@ -778,17 +759,16 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomsResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -813,25 +793,24 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn( MockNetwork.batchResponseData(with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), ( - OpenGroupAPI.Endpoint.userBan(""), - OpenGroupAPI.DirectMessage.mockBatchSubResponse() + Network.SOGS.Endpoint.userBan(""), + Network.SOGS.DirectMessage.mockBatchSubResponse() ) ]) ) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -854,17 +833,16 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockBanAndRoomsResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -885,24 +863,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a send message request context("when preparing a send message request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -918,27 +895,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.SendMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.SendMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.SendSOGSMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.SendSOGSMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } @@ -948,24 +924,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -975,24 +950,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1004,24 +978,23 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1032,27 +1005,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.SendMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.SendMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.SendSOGSMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.SendSOGSMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } @@ -1062,24 +1034,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1089,24 +1060,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1118,24 +1088,23 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1144,21 +1113,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an individual message request context("when preparing an individual message request") { - var preparedRequest: Network.PreparedRequest? + var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessage( + preparedRequest = try Network.SOGS.preparedMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1177,18 +1145,17 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1204,26 +1171,25 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UpdateMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UpdateMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.UpdateMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UpdateMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } @@ -1233,23 +1199,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1259,23 +1224,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1287,23 +1251,22 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1314,26 +1277,25 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UpdateMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UpdateMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.UpdateMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UpdateMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } @@ -1343,23 +1305,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1369,23 +1330,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1397,23 +1357,22 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1427,16 +1386,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageDelete( + preparedRequest = try Network.SOGS.preparedMessageDelete( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1455,16 +1413,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessagesDeleteAll( + preparedRequest = try Network.SOGS.preparedMessagesDeleteAll( sessionId: "testUserId", roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1483,16 +1440,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedPinMessage( + preparedRequest = try Network.SOGS.preparedPinMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1511,16 +1467,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUnpinMessage( + preparedRequest = try Network.SOGS.preparedUnpinMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1539,15 +1494,14 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUnpinAll( + preparedRequest = try Network.SOGS.preparedUnpinAll( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1566,16 +1520,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUpload( + preparedRequest = try Network.SOGS.preparedUpload( data: Data([1, 2, 3]), roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1593,23 +1546,22 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the download url string correctly it("generates the download url string correctly") { - expect(OpenGroupAPI.downloadUrlString(for: "1", server: "testserver", roomToken: "roomToken")) + expect(Network.SOGS.downloadUrlString(for: "1", server: "testserver", roomToken: "roomToken")) .to(equal("testserver/room/roomToken/file/1")) } // MARK: ---- generates the download destination correctly when given an id it("generates the download destination correctly when given an id") { expect { - preparedRequest = try OpenGroupAPI.preparedDownload( + preparedRequest = try Network.SOGS.preparedDownload( fileId: "1", roomToken: "roomToken", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1629,16 +1581,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the download request correctly when given a URL it("generates the download request correctly when given a URL") { expect { - preparedRequest = try OpenGroupAPI.preparedDownload( + preparedRequest = try Network.SOGS.preparedDownload( url: URL(string: "http://oxen.io/room/roomToken/file/1")!, roomToken: "roomToken", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1658,19 +1609,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an inbox request context("when preparing an inbox request") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.DirectMessage]?>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedInbox( + preparedRequest = try Network.SOGS.preparedInbox( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1684,20 +1634,19 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an inbox since request context("when preparing an inbox since request") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.DirectMessage]?>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedInboxSince( + preparedRequest = try Network.SOGS.preparedInboxSince( id: 1, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1711,19 +1660,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a clear inbox request context("when preparing an inbox since request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedClearInbox( + preparedRequest = try Network.SOGS.preparedClearInbox( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1737,21 +1685,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a send direct message request context("when preparing a send direct message request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( ciphertext: "test".data(using: .utf8)!, toInboxFor: "testUserId", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1764,7 +1711,7 @@ class OpenGroupAPISpec: QuickSpec { } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when preparing a ban user request context("when preparing a ban user request") { @TestState var preparedRequest: Network.PreparedRequest? @@ -1772,17 +1719,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1796,25 +1742,24 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global ban if no room tokens are provided it("does a global ban if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserBanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserBanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserBanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserBanRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1822,25 +1767,24 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific bans if room tokens are provided it("does room specific bans if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserBanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserBanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserBanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserBanRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -1853,16 +1797,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1876,24 +1819,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global unban if no room tokens are provided it("does a global unban if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserUnbanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserUnbanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserUnbanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserUnbanRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1901,24 +1843,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific unbans if room tokens are provided it("does room specific unbans if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserUnbanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserUnbanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserUnbanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserUnbanRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -1931,19 +1872,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1957,27 +1897,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global update if no room tokens are provided it("does a global update if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserModeratorRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserModeratorRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserModeratorRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserModeratorRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1985,27 +1924,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific updates if room tokens are provided it("does room specific updates if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserModeratorRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserModeratorRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserModeratorRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserModeratorRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -2013,19 +1951,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- fails if neither moderator or admin are set it("fails if neither moderator or admin are set") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: nil, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2038,21 +1975,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a ban and delete all request context("when preparing a ban and delete all request") { - @TestState var preparedRequest: Network.PreparedRequest>? + @TestState var preparedRequest: Network.PreparedRequest>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( + preparedRequest = try Network.SOGS.preparedUserBanAndDeleteAllMessages( sessionId: "testUserId", roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2062,37 +1998,36 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.userBan("testUserId"))) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomDeleteMessages("testRoom", sessionId: "testUserId"))) } } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when signing context("when signing") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? // MARK: ---- fails when there is no ed25519SecretKey it("fails when there is no ed25519SecretKey") { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2102,14 +2037,13 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs correctly it("signs correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2138,19 +2072,18 @@ class OpenGroupAPISpec: QuickSpec { .thenThrow(CryptoError.failedToGenerateOutput) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2161,14 +2094,13 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs correctly it("signs correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies @@ -2197,19 +2129,18 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2221,19 +2152,18 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2242,27 +2172,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when sending context("when sending") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? beforeEach { mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(type: [OpenGroupAPI.Room].self)) + .thenReturn(MockNetwork.response(type: [Network.SOGS.Room].self)) } // MARK: ---- triggers sending correctly it("triggers sending correctly") { - var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? + var response: (info: ResponseInfoType, data: [Network.SOGS.Room])? expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2290,15 +2219,15 @@ extension Network.BatchResponse { static let mockCapabilitiesAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.room("testRoom"), OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockCapabilitiesAndRoomsResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.rooms, [OpenGroupAPI.Room].mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.rooms, [Network.SOGS.Room].mockBatchSubResponse()) ] ) @@ -2306,22 +2235,22 @@ extension Network.BatchResponse { static let mockCapabilitiesAndBanResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()) ] ) static let mockBanAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.room("testRoom"), OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockBanAndRoomsResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.rooms, [OpenGroupAPI.Room].mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.rooms, [Network.SOGS.Room].mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift b/SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift similarity index 78% rename from SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift rename to SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift index ab6c201a14..5b24ce72de 100644 --- a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class PersonalizationSpec: QuickSpec { override class func spec() { @@ -13,9 +13,9 @@ class PersonalizationSpec: QuickSpec { describe("a Personalization") { // MARK: -- generates bytes correctly it("generates bytes correctly") { - expect(OpenGroupAPI.Personalization.sharedKeys.bytes) + expect(Network.SOGS.Personalization.sharedKeys.bytes) .to(equal([115, 111, 103, 115, 46, 115, 104, 97, 114, 101, 100, 95, 107, 101, 121, 115])) - expect(OpenGroupAPI.Personalization.authHeader.bytes) + expect(Network.SOGS.Personalization.authHeader.bytes) .to(equal([115, 111, 103, 115, 46, 97, 117, 116, 104, 95, 104, 101, 97, 100, 101, 114])) } } diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift similarity index 56% rename from SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift rename to SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift index b9e37aca22..33b0929de7 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift @@ -1,13 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionNetworkingKit -import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SOGSEndpointSpec: QuickSpec { override class func spec() { @@ -15,12 +13,12 @@ class SOGSEndpointSpec: QuickSpec { describe("a SOGSEndpoint") { // MARK: -- provides the correct batch request variant it("provides the correct batch request variant") { - expect(OpenGroupAPI.Endpoint.batchRequestVariant).to(equal(.sogs)) + expect(Network.SOGS.Endpoint.batchRequestVariant).to(equal(.sogs)) } // MARK: -- excludes the correct headers from batch sub request it("excludes the correct headers from batch sub request") { - expect(OpenGroupAPI.Endpoint.excludedSubRequestHeaders).to(equal([ + expect(Network.SOGS.Endpoint.excludedSubRequestHeaders).to(equal([ HTTPHeader.sogsPubKey, HTTPHeader.sogsTimestamp, HTTPHeader.sogsNonce, @@ -32,53 +30,53 @@ class SOGSEndpointSpec: QuickSpec { it("generates the path value correctly") { // Utility - expect(OpenGroupAPI.Endpoint.onion.path).to(equal("oxen/v4/lsrpc")) - expect(OpenGroupAPI.Endpoint.batch.path).to(equal("batch")) - expect(OpenGroupAPI.Endpoint.sequence.path).to(equal("sequence")) - expect(OpenGroupAPI.Endpoint.capabilities.path).to(equal("capabilities")) + expect(Network.SOGS.Endpoint.onion.path).to(equal("oxen/v4/lsrpc")) + expect(Network.SOGS.Endpoint.batch.path).to(equal("batch")) + expect(Network.SOGS.Endpoint.sequence.path).to(equal("sequence")) + expect(Network.SOGS.Endpoint.capabilities.path).to(equal("capabilities")) // Rooms - expect(OpenGroupAPI.Endpoint.rooms.path).to(equal("rooms")) - expect(OpenGroupAPI.Endpoint.room("test").path).to(equal("room/test")) - expect(OpenGroupAPI.Endpoint.roomPollInfo("test", 123).path).to(equal("room/test/pollInfo/123")) + expect(Network.SOGS.Endpoint.rooms.path).to(equal("rooms")) + expect(Network.SOGS.Endpoint.room("test").path).to(equal("room/test")) + expect(Network.SOGS.Endpoint.roomPollInfo("test", 123).path).to(equal("room/test/pollInfo/123")) // Messages - expect(OpenGroupAPI.Endpoint.roomMessage("test").path).to(equal("room/test/message")) - expect(OpenGroupAPI.Endpoint.roomMessageIndividual("test", id: 123).path).to(equal("room/test/message/123")) - expect(OpenGroupAPI.Endpoint.roomMessagesRecent("test").path).to(equal("room/test/messages/recent")) - expect(OpenGroupAPI.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) - expect(OpenGroupAPI.Endpoint.roomMessagesSince("test", seqNo: 123).path) + expect(Network.SOGS.Endpoint.roomMessage("test").path).to(equal("room/test/message")) + expect(Network.SOGS.Endpoint.roomMessageIndividual("test", id: 123).path).to(equal("room/test/message/123")) + expect(Network.SOGS.Endpoint.roomMessagesRecent("test").path).to(equal("room/test/messages/recent")) + expect(Network.SOGS.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) + expect(Network.SOGS.Endpoint.roomMessagesSince("test", seqNo: 123).path) .to(equal("room/test/messages/since/123")) - expect(OpenGroupAPI.Endpoint.roomDeleteMessages("test", sessionId: "testId").path) + expect(Network.SOGS.Endpoint.roomDeleteMessages("test", sessionId: "testId").path) .to(equal("room/test/all/testId")) // Pinning - expect(OpenGroupAPI.Endpoint.roomPinMessage("test", id: 123).path).to(equal("room/test/pin/123")) - expect(OpenGroupAPI.Endpoint.roomUnpinMessage("test", id: 123).path).to(equal("room/test/unpin/123")) - expect(OpenGroupAPI.Endpoint.roomUnpinAll("test").path).to(equal("room/test/unpin/all")) + expect(Network.SOGS.Endpoint.roomPinMessage("test", id: 123).path).to(equal("room/test/pin/123")) + expect(Network.SOGS.Endpoint.roomUnpinMessage("test", id: 123).path).to(equal("room/test/unpin/123")) + expect(Network.SOGS.Endpoint.roomUnpinAll("test").path).to(equal("room/test/unpin/all")) // Files - expect(OpenGroupAPI.Endpoint.roomFile("test").path).to(equal("room/test/file")) - expect(OpenGroupAPI.Endpoint.roomFileIndividual("test", "123").path).to(equal("room/test/file/123")) + expect(Network.SOGS.Endpoint.roomFile("test").path).to(equal("room/test/file")) + expect(Network.SOGS.Endpoint.roomFileIndividual("test", "123").path).to(equal("room/test/file/123")) // Inbox/Outbox (Message Requests) - expect(OpenGroupAPI.Endpoint.inbox.path).to(equal("inbox")) - expect(OpenGroupAPI.Endpoint.inboxSince(id: 123).path).to(equal("inbox/since/123")) - expect(OpenGroupAPI.Endpoint.inboxFor(sessionId: "test").path).to(equal("inbox/test")) + expect(Network.SOGS.Endpoint.inbox.path).to(equal("inbox")) + expect(Network.SOGS.Endpoint.inboxSince(id: 123).path).to(equal("inbox/since/123")) + expect(Network.SOGS.Endpoint.inboxFor(sessionId: "test").path).to(equal("inbox/test")) - expect(OpenGroupAPI.Endpoint.outbox.path).to(equal("outbox")) - expect(OpenGroupAPI.Endpoint.outboxSince(id: 123).path).to(equal("outbox/since/123")) + expect(Network.SOGS.Endpoint.outbox.path).to(equal("outbox")) + expect(Network.SOGS.Endpoint.outboxSince(id: 123).path).to(equal("outbox/since/123")) // Users - expect(OpenGroupAPI.Endpoint.userBan("test").path).to(equal("user/test/ban")) - expect(OpenGroupAPI.Endpoint.userUnban("test").path).to(equal("user/test/unban")) - expect(OpenGroupAPI.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) + expect(Network.SOGS.Endpoint.userBan("test").path).to(equal("user/test/ban")) + expect(Network.SOGS.Endpoint.userUnban("test").path).to(equal("user/test/unban")) + expect(Network.SOGS.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) } } } diff --git a/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift b/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift new file mode 100644 index 0000000000..e994dea0b3 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift @@ -0,0 +1,24 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class SOGSErrorSpec: QuickSpec { + override class func spec() { + // MARK: - a SOGSError + describe("a SOGSError") { + // MARK: -- generates the error description correctly + it("generates the error description correctly") { + expect(SOGSError.decryptionFailed.description).to(equal("Couldn't decrypt response.")) + expect(SOGSError.signingFailed.description).to(equal("Couldn't sign message.")) + expect(SOGSError.noPublicKey.description).to(equal("Couldn't find server public key.")) + expect(SOGSError.invalidEmoji.description).to(equal("The emoji is invalid.")) + expect(SOGSError.invalidPoll.description).to(equal("Poller in invalid state.")) + } + } + } +} diff --git a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift index d99134ceba..bbd0834908 100644 --- a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift @@ -39,3 +39,94 @@ extension Network.Destination: Mocked { x25519PublicKey: "" ).withGeneratedUrl(for: MockEndpoint.mock) } + +extension Network.SOGS.CapabilitiesResponse: Mocked { + static var mock: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse(capabilities: [], missing: nil) +} + +extension Network.SOGS.Room: Mocked { + static var mock: Network.SOGS.Room = Network.SOGS.Room( + token: "test", + name: "testRoom", + roomDescription: nil, + infoUpdates: 1, + messageSequence: 1, + created: 1, + activeUsers: 1, + activeUsersCutoff: 1, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: true, + defaultRead: nil, + defaultAccessible: nil, + write: true, + defaultWrite: nil, + upload: true, + defaultUpload: nil + ) +} + +extension Network.SOGS.RoomPollInfo: Mocked { + static var mock: Network.SOGS.RoomPollInfo = Network.SOGS.RoomPollInfo( + token: "test", + activeUsers: 1, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: true, + defaultRead: nil, + defaultAccessible: nil, + write: true, + defaultWrite: nil, + upload: true, + defaultUpload: false, + details: .mock + ) +} + +extension Network.SOGS.Message: Mocked { + static var mock: Network.SOGS.Message = Network.SOGS.Message( + id: 100, + sender: TestConstants.blind15PublicKey, + posted: 1, + edited: nil, + deleted: nil, + seqNo: 1, + whisper: false, + whisperMods: false, + whisperTo: nil, + base64EncodedData: nil, + base64EncodedSignature: nil, + reactions: nil + ) +} + +extension Network.SOGS.SendDirectMessageResponse: Mocked { + static var mock: Network.SOGS.SendDirectMessageResponse = Network.SOGS.SendDirectMessageResponse( + id: 1, + sender: TestConstants.blind15PublicKey, + recipient: "testRecipient", + posted: 1122, + expires: 2233 + ) +} + +extension Network.SOGS.DirectMessage: Mocked { + static var mock: Network.SOGS.DirectMessage = Network.SOGS.DirectMessage( + id: 101, + sender: TestConstants.blind15PublicKey, + recipient: "testRecipient", + posted: 1212, + expires: 2323, + base64EncodedMessage: "TestMessage".data(using: .utf8)!.base64EncodedString() + ) +} diff --git a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index 1412423914..eff21161ac 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -47,7 +47,7 @@ class MockNetwork: Mock, NetworkType { return mock(args: [body, destination, requestTimeout, requestAndPathBuildTimeout]) } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> { return mock(args: [ed25519SecretKey]) } } diff --git a/SessionNotificationServiceExtension/NotificationResolution.swift b/SessionNotificationServiceExtension/NotificationResolution.swift index aef0805a55..c291628c84 100644 --- a/SessionNotificationServiceExtension/NotificationResolution.swift +++ b/SessionNotificationServiceExtension/NotificationResolution.swift @@ -4,11 +4,12 @@ import Foundation import SessionUIKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit enum NotificationResolution: CustomStringConvertible { - case success(PushNotificationAPI.NotificationMetadata) + case success(Network.PushNotification.NotificationMetadata) case successCall case ignoreDueToMainAppRunning @@ -20,15 +21,15 @@ enum NotificationResolution: CustomStringConvertible { case ignoreDueToMessageRequest case ignoreDueToDuplicateMessage case ignoreDueToDuplicateCall - case ignoreDueToContentSize(PushNotificationAPI.NotificationMetadata) + case ignoreDueToContentSize(Network.PushNotification.NotificationMetadata) case errorTimeout case errorNotReadyForExtensions case errorLegacyPushNotification case errorCallFailure - case errorNoContent(PushNotificationAPI.NotificationMetadata) - case errorProcessing(PushNotificationAPI.ProcessResult) - case errorMessageHandling(MessageReceiverError, PushNotificationAPI.NotificationMetadata) + case errorNoContent(Network.PushNotification.NotificationMetadata) + case errorProcessing(Network.PushNotification.ProcessResult) + case errorMessageHandling(MessageReceiverError, Network.PushNotification.NotificationMetadata) case errorOther(Error) public var description: String { @@ -88,7 +89,7 @@ enum NotificationResolution: CustomStringConvertible { } } -internal extension PushNotificationAPI.NotificationMetadata { +internal extension Network.PushNotification.NotificationMetadata { var messageOriginString: String { guard self != .invalid else { return "decryption failure" } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 0fe6785ada..54fc5cd9a9 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -157,7 +157,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: - Notification Handling private func extractNotificationInfo(_ info: NotificationInfo) throws -> NotificationInfo { - let (maybeData, metadata, result) = PushNotificationAPI.processNotification( + let (maybeData, metadata, result) = Network.PushNotification.processNotification( notificationContent: info.content, using: dependencies ) @@ -279,7 +279,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func handleConfigMessage( _ notification: ProcessedNotification, swarmPublicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data @@ -806,7 +806,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // // TODO: [Database Relocation] Need to de-database the 'preparedSubscribe' call for this to work (neeeds the AuthMethod logic to be de-databased) // /// Since this is an API call we need to wait for it to complete before we trigger the `completeSilently` logic // Log.info(.cat, "Group invitation was auto-approved, attempting to subscribe for PNs.") -// try? PushNotificationAPI +// try? Network.PushNotification // .preparedSubscribe( // db, // token: Data(hex: token), @@ -1284,7 +1284,7 @@ private extension NotificationServiceExtension { let content: UNMutableNotificationContent let requestId: String let contentHandler: ((UNNotificationContent) -> Void) - let metadata: PushNotificationAPI.NotificationMetadata + let metadata: Network.PushNotification.NotificationMetadata let data: Data let mainAppUnreadCount: Int @@ -1292,7 +1292,7 @@ private extension NotificationServiceExtension { requestId: String? = nil, content: UNMutableNotificationContent? = nil, contentHandler: ((UNNotificationContent) -> Void)? = nil, - metadata: PushNotificationAPI.NotificationMetadata? = nil, + metadata: Network.PushNotification.NotificationMetadata? = nil, mainAppUnreadCount: Int? = nil ) -> NotificationInfo { return NotificationInfo( @@ -1316,8 +1316,8 @@ private extension NotificationServiceExtension { enum NotificationError: Error { case notReadyForExtension - case processingErrorWithFallback(PushNotificationAPI.ProcessResult, PushNotificationAPI.NotificationMetadata) - case processingError(PushNotificationAPI.ProcessResult, PushNotificationAPI.NotificationMetadata) + case processingErrorWithFallback(Network.PushNotification.ProcessResult, Network.PushNotification.NotificationMetadata) + case processingError(Network.PushNotification.ProcessResult, Network.PushNotification.NotificationMetadata) case timeout } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index b460349b73..60295606b9 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -275,7 +275,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView dependencies[singleton: .network] .getSwarm(for: swarmPublicKey) .tryFlatMapWithRandomSnode(using: dependencies) { snode in - try SnodeAPI + try Network.SnodeAPI .preparedGetNetworkTime(from: snode, using: dependencies) .send(using: dependencies) } diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 7cf704be92..f060ee7e98 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -121,7 +121,7 @@ class OnboardingSpec: AsyncSpec { .thenReturn(MockNetwork.batchResponseData( with: [ ( - SnodeAPI.Endpoint.getMessages, + Network.SnodeAPI.Endpoint.getMessages, GetMessagesResponse( messages: (pendingPushes? .pushData From ad267f0182f5aaf23bbe49c2abec336dde62974c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 14:30:03 +1000 Subject: [PATCH 205/244] Update migration order unit test --- SessionTests/Database/DatabaseSpec.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index c9b824b196..662bb23e3b 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -236,7 +236,8 @@ class DatabaseSpec: QuickSpec { "utilitiesKit.RenameTableSettingToKeyValueStore", "messagingKit.MoveSettingsToLibSession", "messagingKit.RenameAttachments", - "messagingKit.AddProMessageFlag" + "messagingKit.AddProMessageFlag", + "LastProfileUpdateTimestamp" ])) } From 939ebfa72189426f3f8b1b6f987acbc0684af686 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 15:58:30 +1000 Subject: [PATCH 206/244] Fixed a home screen refresh issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added App, Database and Network Lifecycle ObservableEvents • Fixed a bug where log messages included the prefixes twice • Fixed an issue where the home screen wouldn't refresh after a background fetch --- Session.xcodeproj/project.pbxproj | 32 +++++++++++-------- Session/Home/HomeViewModel.swift | 17 +++++++++- ...NotificationAPI+SessionMessagingKit.swift} | 0 ...ft => Threading+SessionMessagingKit.swift} | 0 ...maryDescribable+SessionMessagingKit.swift} | 0 .../LibSession/LibSession+Networking.swift | 8 +++-- .../ObservableKey+SessionNetworkingKit.swift | 23 +++++++++++++ SessionUtilitiesKit/Database/Storage.swift | 3 ++ SessionUtilitiesKit/General/Logging.swift | 3 +- .../ObservableKey+SessionUtilitiesKit.swift | 28 ++++++++++++++++ .../Observations/ObservationManager.swift | 28 +++++++++++++++- 11 files changed, 122 insertions(+), 20 deletions(-) rename SessionMessagingKit/Sending & Receiving/Notifications/{PushNotificationAPI+SMK.swift => PushNotificationAPI+SessionMessagingKit.swift} (100%) rename SessionMessagingKit/Utilities/{Threading+SMK.swift => Threading+SessionMessagingKit.swift} (100%) rename SessionMessagingKitTests/_TestUtilities/{CustomArgSummaryDescribable+SMK.swift => CustomArgSummaryDescribable+SessionMessagingKit.swift} (100%) create mode 100644 SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 582038180a..1980111d9a 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -586,7 +586,7 @@ FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2DD58F2C6DD13C0073D9BE /* DifferenceKit */; }; FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */; }; FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */; }; FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */; }; FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */; }; @@ -671,7 +671,7 @@ FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */; }; FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */; }; FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; @@ -779,7 +779,7 @@ FD6B92DE2E77BDE2004463B5 /* Authentication+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */; }; FD6B92E12E77C1E1004463B5 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E02E77C1DC004463B5 /* PushNotification.swift */; }; FD6B92E22E77C21D004463B5 /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; - FD6B92E42E77C256004463B5 /* PushNotificationAPI+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */; }; + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */; }; FD6B92E62E77C5A2004463B5 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; FD6B92E72E77C5A2004463B5 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; FD6B92E82E77C5B7004463B5 /* PushNotificationEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */; }; @@ -1041,6 +1041,7 @@ FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */; }; FDE521A62E0E6C8C00061B8E /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; @@ -1079,7 +1080,7 @@ FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */; }; FDE754F92C9BB0B0002A2623 /* NotificationActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */; }; FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */; }; - FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */; }; + FDE754FE2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */; }; FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */; }; FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */; }; FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */; }; @@ -1934,7 +1935,7 @@ FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSMKMockExtensions.swift; sourceTree = ""; }; - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SMK.swift"; sourceTree = ""; }; + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SessionMessagingKit.swift"; sourceTree = ""; }; FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPollerCache.swift; sourceTree = ""; }; FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDisplayPictureCache.swift; sourceTree = ""; }; FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGroupPollerCache.swift; sourceTree = ""; }; @@ -2066,7 +2067,7 @@ FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSOGSAPISpec.swift; sourceTree = ""; }; FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Authentication+SOGS.swift"; sourceTree = ""; }; FD6B92E02E77C1DC004463B5 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = ""; }; - FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PushNotificationAPI+SMK.swift"; sourceTree = ""; }; + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PushNotificationAPI+SessionMessagingKit.swift"; sourceTree = ""; }; FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+PushNotification.swift"; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; @@ -2307,6 +2308,7 @@ FDE5219F2E0D22FD00061B8E /* ObservationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationManager.swift; sourceTree = ""; }; FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionMessagingKit.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; @@ -2348,7 +2350,7 @@ FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationConfig.swift; sourceTree = ""; }; FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationActionHandler.swift; sourceTree = ""; }; FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; - FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SMK.swift"; sourceTree = ""; }; + FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SessionMessagingKit.swift"; sourceTree = ""; }; FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _025_AddPendingReadReceipts.swift; sourceTree = ""; }; FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; @@ -3581,7 +3583,7 @@ children = ( FDC13D4E2A16EE41007267C7 /* Types */, FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */, - FD6B92E32E77C250004463B5 /* PushNotificationAPI+SMK.swift */, + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */, ); path = Notifications; sourceTree = ""; @@ -3674,7 +3676,7 @@ C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */, FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, - FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */, + FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */, FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */, ); path = Utilities; @@ -3711,6 +3713,7 @@ isa = PBXGroup; children = ( C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */, + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */, FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */, FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */, @@ -5019,7 +5022,7 @@ isa = PBXGroup; children = ( FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */, - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */, + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */, FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */, FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */, FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */, @@ -6371,6 +6374,7 @@ FD6B92B32E77AA03004463B5 /* Personalization.swift in Sources */, FD6B929B2E77A084004463B5 /* NetworkInfo.swift in Sources */, FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */, + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */, FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */, @@ -6803,12 +6807,12 @@ FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */, FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */, - FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, + FDE754FE2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */, FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */, FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, - FD6B92E42E77C256004463B5 /* PushNotificationAPI+SMK.swift in Sources */, + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SessionMessagingKit.swift in Sources */, FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FDB11A522DCC6B0000BEF49F /* OpenGroupUrlInfo.swift in Sources */, @@ -7075,7 +7079,7 @@ FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */, - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */, @@ -7203,7 +7207,7 @@ FD981BCD2DC81ABF00564172 /* MockExtensionHelper.swift in Sources */, FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */, FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */, - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */, FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */, diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 6ec7b202bb..98d29d990e 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -104,6 +104,8 @@ public class HomeViewModel: NavigatableStateHolder { public var observedKeys: Set { var result: Set = [ + .appLifecycle(.willEnterForeground), + .databaseLifecycle(.resumed), .loadPage(HomeViewModel.self), .messageRequestAccepted, .messageRequestDeleted, @@ -245,7 +247,7 @@ public class HomeViewModel: NavigatableStateHolder { } /// Handle database events first - if let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { + if !dependencies[singleton: .storage].isSuspended, let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { do { var fetchedConversations: [SessionThreadViewModel] = [] let idsNeedingRequery: Set = self.extractIdsNeedingRequery( @@ -354,6 +356,9 @@ public class HomeViewModel: NavigatableStateHolder { Log.critical(.homeViewModel, "Failed to fetch state for events [\(eventList)], due to error: \(error)") } } + else if let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { + Log.warn(.homeViewModel, "Ignored \(databaseEvents.count) database event(s) sent while storage was suspended.") + } /// Then handle non-database events let groupedOtherEvents: [GenericObservableKey: Set]? = splitEvents[.other]? @@ -445,6 +450,15 @@ public class HomeViewModel: NavigatableStateHolder { events: Set, cache: [String: SessionThreadViewModel] ) -> Set { + let requireFullRefresh: Bool = events.contains(where: { event in + event.key == .appLifecycle(.willEnterForeground) || + event.key == .databaseLifecycle(.resumed) + }) + + guard !requireFullRefresh else { + return Set(cache.keys) + } + return events.reduce(into: []) { result, event in switch (event.key.generic, event.value) { case (.conversationUpdated, let event as ConversationEvent): result.insert(event.id) @@ -741,6 +755,7 @@ private extension ObservedEvent { case (.feature(.forceOffline), _): return .other case (.setting(.hasViewedSeed), _): return .other + case (.appLifecycle(.willEnterForeground), _): return .databaseQuery case (.messageRequestUnreadMessageReceived, _), (.messageRequestAccepted, _), (.messageRequestDeleted, _), (.messageRequestMessageRead, _): return .databaseQuery diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SessionMessagingKit.swift similarity index 100% rename from SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SMK.swift rename to SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SessionMessagingKit.swift diff --git a/SessionMessagingKit/Utilities/Threading+SMK.swift b/SessionMessagingKit/Utilities/Threading+SessionMessagingKit.swift similarity index 100% rename from SessionMessagingKit/Utilities/Threading+SMK.swift rename to SessionMessagingKit/Utilities/Threading+SessionMessagingKit.swift diff --git a/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift b/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SessionMessagingKit.swift similarity index 100% rename from SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift rename to SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SessionMessagingKit.swift diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index a0a2be6d27..53fabef8b7 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -704,23 +704,27 @@ public extension LibSession { // MARK: - Functions public func suspendNetworkAccess() { - Log.info(.network, "Network access suspended.") isSuspended = true + Log.info(.network, "Network access suspended.") switch network { case .none: break case .some(let network): network_suspend(network) } + + dependencies.notifyAsync(key: .networkLifecycle(.suspended)) } public func resumeNetworkAccess() { isSuspended = false - Log.info(.network, "Network access resumed.") switch network { case .none: break case .some(let network): network_resume(network) } + + Log.info(.network, "Network access resumed.") + dependencies.notifyAsync(key: .networkLifecycle(.resumed)) } public func getOrCreateNetwork() -> AnyPublisher?, Error> { diff --git a/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift b/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift new file mode 100644 index 0000000000..372aaf717d --- /dev/null +++ b/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift @@ -0,0 +1,23 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +public extension ObservableKey { + static func networkLifecycle(_ event: NetworkLifecycle) -> ObservableKey { + ObservableKey("networkLifecycle-\(event)", .networkLifecycle) + } +} + +public extension GenericObservableKey { + static let networkLifecycle: GenericObservableKey = "networkLifecycle" +} + +// MARK: - NetworkLifecycle + +public enum NetworkLifecycle: String, Sendable { + case suspended + case resumed +} diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index e9b4cfbc4f..09817afb74 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -453,6 +453,8 @@ open class Storage { .defaulting(to: "N/A") Log.verbose(.storage, "Database suspended successfully for \(id) (db: \(dbFileSize), shm: \(dbShmFileSize), wal: \(dbWalFileSize)).") } + + dependencies.notifyAsync(key: .databaseLifecycle(.suspended)) } /// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()` @@ -462,6 +464,7 @@ open class Storage { isSuspended = false Log.info(.storage, "Database access resumed.") + dependencies.notifyAsync(key: .databaseLifecycle(.resumed)) } public func checkpoint(_ mode: Database.CheckpointMode) throws { diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 9bd2646f9e..67b529d8ec 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -730,8 +730,7 @@ public actor Logger: LoggerType { }() /// Clean up the message if needed (replace double periods with single, trim whitespace, truncate pubkeys) - let cleanedMessage: String = logPrefix - .appending(message) + let cleanedMessage: String = message .replacingOccurrences(of: "...", with: "|||") .replacingOccurrences(of: "..", with: ".") .replacingOccurrences(of: "|||", with: "...") diff --git a/SessionUtilitiesKit/Observations/ObservableKey+SessionUtilitiesKit.swift b/SessionUtilitiesKit/Observations/ObservableKey+SessionUtilitiesKit.swift index a2c475978f..e26fa09e6d 100644 --- a/SessionUtilitiesKit/Observations/ObservableKey+SessionUtilitiesKit.swift +++ b/SessionUtilitiesKit/Observations/ObservableKey+SessionUtilitiesKit.swift @@ -5,6 +5,14 @@ import Foundation public extension ObservableKey { + static func appLifecycle(_ event: AppLifecycle) -> ObservableKey { + ObservableKey("appLifecycle-\(event)", .appLifecycle) + } + + static func databaseLifecycle(_ event: DatabaseLifecycle) -> ObservableKey { + ObservableKey("databaseLifecycle-\(event)", .databaseLifecycle) + } + static func feature(_ key: FeatureConfig) -> ObservableKey { ObservableKey(key.identifier, .feature) } @@ -17,6 +25,26 @@ public extension ObservableKey { } public extension GenericObservableKey { + static let appLifecycle: GenericObservableKey = "appLifecycle" + static let databaseLifecycle: GenericObservableKey = "databaseLifecycle" static let feature: GenericObservableKey = "feature" static let featureGroup: GenericObservableKey = "featureGroup" } + +// MARK: - AppLifecycle + +public enum AppLifecycle: String, Sendable { + case didEnterBackground + case willEnterForeground + case didBecomeActive + case willResignActive + case didReceiveMemoryWarning + case willTerminate +} + +// MARK: - DatabaseLifecycle + +public enum DatabaseLifecycle: String, Sendable { + case suspended + case resumed +} diff --git a/SessionUtilitiesKit/Observations/ObservationManager.swift b/SessionUtilitiesKit/Observations/ObservationManager.swift index 4bff64a107..e0b8388818 100644 --- a/SessionUtilitiesKit/Observations/ObservationManager.swift +++ b/SessionUtilitiesKit/Observations/ObservationManager.swift @@ -1,22 +1,48 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import UIKit.UIApplication // MARK: - Singleton public extension Singleton { static let observationManager: SingletonConfig = Dependencies.create( identifier: "observationManager", - createInstance: { dependencies in ObservationManager() } + createInstance: { dependencies in ObservationManager(using: dependencies) } ) } // MARK: - ObservationManager public actor ObservationManager { + private let lifecycleObservations: [any NSObjectProtocol] private var store: [ObservableKey: [UUID: AsyncStream<(event: ObservedEvent, priority: Priority)>.Continuation]] = [:] + // MARK: - Initialization + + init(using dependencies: Dependencies) { + let notifications: [Notification.Name: AppLifecycle] = [ + UIApplication.didEnterBackgroundNotification: .didEnterBackground, + UIApplication.willEnterForegroundNotification: .willEnterForeground, + UIApplication.didBecomeActiveNotification: .didBecomeActive, + UIApplication.willResignActiveNotification: .willResignActive, + UIApplication.didReceiveMemoryWarningNotification: .didReceiveMemoryWarning, + UIApplication.willTerminateNotification: .willTerminate + ] + + lifecycleObservations = notifications.reduce(into: []) { [dependencies] result, next in + let value: AppLifecycle = next.value + + result.append( + NotificationCenter.default.addObserver(forName: next.key, object: nil, queue: .current) { [dependencies] _ in + dependencies.notifyAsync(key: .appLifecycle(value)) + } + ) + } + } + deinit { + NotificationCenter.default.removeObserver(self) store.values.forEach { $0.values.forEach { $0.finish() } } } From 1ccd99b4abfaf2006f477c8e1fe413ec6478d646 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 16:12:09 +1000 Subject: [PATCH 207/244] Fixed a recovery password string which didn't have styling applied --- Session/Settings/RecoveryPasswordScreen.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Session/Settings/RecoveryPasswordScreen.swift b/Session/Settings/RecoveryPasswordScreen.swift index 9afd6dcd93..1fd3034852 100644 --- a/Session/Settings/RecoveryPasswordScreen.swift +++ b/Session/Settings/RecoveryPasswordScreen.swift @@ -66,11 +66,15 @@ struct RecoveryPasswordScreen: View { } .padding(.bottom, Values.smallSpacing) - Text("recoveryPasswordDescription".localized()) - .font(.system(size: Values.smallFontSize)) - .foregroundColor(themeColor: .textPrimary) - .padding(.bottom, Values.mediumSpacing) - .fixedSize(horizontal: false, vertical: true) + AttributedText( + "recoveryPasswordDescription".localizedFormatted( + baseFont: .systemFont(ofSize: Values.smallFontSize) + ) + ) + .font(.system(size: Values.smallFontSize)) + .foregroundColor(themeColor: .textPrimary) + .padding(.bottom, Values.mediumSpacing) + .fixedSize(horizontal: false, vertical: true) if self.showQRCode { QRCodeView( From e78028d11f9ee33a4dbed00273f40c3e23dbae80 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Mon, 15 Sep 2025 14:49:44 +0800 Subject: [PATCH 208/244] Clean up handling of tap to dismiss keyboard event --- .../ConversationVC+Interaction.swift | 28 +++++------- Session/Conversations/ConversationVC.swift | 5 ++- .../Message Cells/CallMessageCell.swift | 22 ++++------ .../Message Cells/InfoMessageCell.swift | 24 +++++----- .../Message Cells/MessageCell.swift | 44 +++++++++++++++++-- .../Message Cells/VisibleMessageCell.swift | 34 ++++++-------- 6 files changed, 87 insertions(+), 70 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 1e24c0950d..8da25fa33e 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -22,7 +22,8 @@ extension ConversationVC: ContextMenuActionDelegate, SendMediaNavDelegate, AttachmentApprovalViewControllerDelegate, - GifPickerViewControllerDelegate + GifPickerViewControllerDelegate, + UIGestureRecognizerDelegate { // MARK: - Open Settings @@ -34,7 +35,7 @@ extension ConversationVC: } // Handle taps outside of tableview cell to dismiss keyboard - @MainActor @objc func handleTableViewTap() { + @MainActor @objc func dismissKeyboardOnTap() { _ = self.snInputView.resignFirstResponder() } @@ -259,6 +260,11 @@ extension ConversationVC: return true } + + // MARK: - UIGestureRecognizerDelegate + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } // MARK: - SendMediaNavDelegate @@ -1061,11 +1067,6 @@ extension ConversationVC: // MARK: MessageCellDelegate - func willHandleItemCellTapped() { - // Dismiss keyboard when cell is tapped - _ = snInputView.resignFirstResponder() - } - func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the unblock modal if needed guard self.viewModel.threadData.threadIsBlocked != true else { @@ -2260,15 +2261,10 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - // Add delay before doing any ui updates - // Delay added to give time for long press actions to dismiss - let delay = completion == nil ? 0 : ContextMenuVC.dismissDuration - - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } - _ = self?.snInputView.becomeFirstResponder() - completion?() - } + if isShowingSearchUI == true { willManuallyCancelSearchUI() } + _ = snInputView.becomeFirstResponder() + + completion?() } func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index f45cc2870d..e111edb99b 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -386,7 +386,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Handle taps outside of tableview cell private lazy var tableViewTapGesture: UITapGestureRecognizer = { let result: UITapGestureRecognizer = UITapGestureRecognizer() - result.addTarget(self, action: #selector(handleTableViewTap)) + result.delegate = self + result.addTarget(self, action: #selector(dismissKeyboardOnTap)) result.cancelsTouchesInView = false return result @@ -544,7 +545,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa } // Gesture - tableView.addGestureRecognizer(tableViewTapGesture) + view.addGestureRecognizer(tableViewTapGesture) self.viewModel.navigatableState.setupBindings(viewController: self, disposables: &self.viewModel.disposables) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 2fa994f359..56b9172269 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -16,6 +16,13 @@ final class CallMessageCell: MessageCell { override var contextSnapshotView: UIView? { return container } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var topConstraint: NSLayoutConstraint = mainStackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) @@ -113,15 +120,6 @@ final class CallMessageCell: MessageCell { mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset) } - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } - // MARK: - Updating override func update( @@ -205,7 +203,7 @@ final class CallMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -216,9 +214,7 @@ final class CallMessageCell: MessageCell { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - delegate?.willHandleItemCellTapped() - + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 8869b6e574..cf8454bb31 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -13,6 +13,13 @@ final class InfoMessageCell: MessageCell { override var contextSnapshotView: UIView? { return label } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var iconContainerViewWidthConstraint = iconContainerView.set(.width, to: InfoMessageCell.iconSize) @@ -77,15 +84,6 @@ final class InfoMessageCell: MessageCell { stackView.pin(.right, to: .right, of: self, withInset: -Values.massiveSpacing) stackView.pin(.bottom, to: .bottom, of: self, withInset: -InfoMessageCell.inset) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } // MARK: - Updating @@ -169,7 +167,7 @@ final class InfoMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -180,11 +178,9 @@ final class InfoMessageCell: MessageCell { isHandlingLongPress = true } - @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - - delegate?.willHandleItemCellTapped() - + if cellViewModel.variant == .infoDisappearingMessagesUpdate && cellViewModel.canDoFollowingSetting() { delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 8d42c776ac..1592f23541 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -10,11 +10,16 @@ public enum SwipeState { case cancelled } +public enum GestureRecognizerType { + case tap, longPress, doubleTap +} + public class MessageCell: UITableViewCell { var dependencies: Dependencies? var viewModel: MessageViewModel? weak var delegate: MessageCellDelegate? open var contextSnapshotView: UIView? { return nil } + open var allowedGestureRecognizers: Set { return [] } // Override to have gestures // MARK: - Lifecycle @@ -41,7 +46,31 @@ public class MessageCell: UITableViewCell { } func setUpGestureRecognizers() { - // To be overridden by subclasses + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGestureRecognizer.numberOfTapsRequired = 1 + + let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGestureRecognizer.numberOfTapsRequired = 2 + + // Only set the dependency if both gestures are allowed + let isTapAndDoubleTapAllowed = allowedGestureRecognizers.contains(.tap) && allowedGestureRecognizers.contains(.doubleTap) + + if isTapAndDoubleTapAllowed { + tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) + } + + if allowedGestureRecognizers.contains(.longPress) { + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressGesture) + } + + if allowedGestureRecognizers.contains(.tap) { + addGestureRecognizer(tapGestureRecognizer) + } + + if allowedGestureRecognizers.contains(.doubleTap) { + addGestureRecognizer(doubleTapGestureRecognizer) + } } // MARK: - Updating @@ -93,6 +122,16 @@ public class MessageCell: UITableViewCell { return CallMessageCell.self } } + + // MARK: - Gesture events + @objc + func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {} + + @objc + func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {} + + @objc + func handleDoubleTap() {} } // MARK: - MessageCellDelegate @@ -108,9 +147,6 @@ protocol MessageCellDelegate: ReactionDelegate { func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) func handleReadMoreButtonTapped(_ cell: UITableViewCell, for cellViewModel: MessageViewModel) - - // Handle taps events outside of `handleItemTapped` contents - func willHandleItemCellTapped() } extension MessageCellDelegate { diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index d2fb437430..0983e67bad 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -25,6 +25,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { override var contextSnapshotView: UIView? { return snContentView } + override var allowedGestureRecognizers: Set { + return [ + .tap, + .longPress, + .doubleTap + ] + } + // Constraints internal lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) @@ -270,21 +278,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - - let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGestureRecognizer.numberOfTapsRequired = 2 - addGestureRecognizer(doubleTapGestureRecognizer) - tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) - } - + // MARK: - Updating override func update( @@ -968,7 +962,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -994,11 +988,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - - delegate?.willHandleItemCellTapped() - + let location = gestureRecognizer.location(in: self) if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile { @@ -1064,7 +1056,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc private func handleDoubleTap() { + override func handleDoubleTap() { guard let cellViewModel: MessageViewModel = self.viewModel else { return } delegate?.handleItemDoubleTapped(cellViewModel) From 88cf55617008c22a36439e96c40061f9585cfd47 Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 16 Sep 2025 09:09:23 +0800 Subject: [PATCH 209/244] Clean up gesture setup --- .../Message Cells/CallMessageCell.swift | 2 +- .../Message Cells/InfoMessageCell.swift | 2 +- .../Message Cells/MessageCell.swift | 35 ++++++++++--------- .../Message Cells/VisibleMessageCell.swift | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 56b9172269..a74406bcd7 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -203,7 +203,7 @@ final class CallMessageCell: MessageCell { // MARK: - Interaction - override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index cf8454bb31..a5196a0b88 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -167,7 +167,7 @@ final class InfoMessageCell: MessageCell { // MARK: - Interaction - override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 1592f23541..ec40f8e72e 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -46,30 +46,31 @@ public class MessageCell: UITableViewCell { } func setUpGestureRecognizers() { - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 + var tapGestureRecognizer: UITapGestureRecognizer? + var doubleTapGestureRecognizer: UITapGestureRecognizer? - let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGestureRecognizer.numberOfTapsRequired = 2 - - // Only set the dependency if both gestures are allowed - let isTapAndDoubleTapAllowed = allowedGestureRecognizers.contains(.tap) && allowedGestureRecognizers.contains(.doubleTap) + if allowedGestureRecognizers.contains(.tap) { + let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) + tapGestureRecognizer = tapGesture + } - if isTapAndDoubleTapAllowed { - tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) + if allowedGestureRecognizers.contains(.doubleTap) { + let doubleTapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGesture.numberOfTapsRequired = 2 + addGestureRecognizer(doubleTapGesture) + doubleTapGestureRecognizer = doubleTapGesture } - + if allowedGestureRecognizers.contains(.longPress) { let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) addGestureRecognizer(longPressGesture) } - if allowedGestureRecognizers.contains(.tap) { - addGestureRecognizer(tapGestureRecognizer) - } - - if allowedGestureRecognizers.contains(.doubleTap) { - addGestureRecognizer(doubleTapGestureRecognizer) + // If we have both tap and double tap gestures then the single tap should fail if a double tap occurs + if let tapGesture: UITapGestureRecognizer = tapGestureRecognizer, let doubleTapGesture: UITapGestureRecognizer = doubleTapGestureRecognizer { + tapGesture.require(toFail: doubleTapGesture) } } @@ -125,7 +126,7 @@ public class MessageCell: UITableViewCell { // MARK: - Gesture events @objc - func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) {} + func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {} @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0983e67bad..0dbde5e460 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -962,7 +962,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - override func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return From ade137aecf22c5d4b76911944dabf6821846e1c3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Sep 2025 11:23:05 +1000 Subject: [PATCH 210/244] Bumped build and version numbers --- Session.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1980111d9a..ac08c3f795 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8300,7 +8300,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 632; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8340,7 +8340,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.14.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8381,7 +8381,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 632; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8416,7 +8416,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.14.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8862,7 +8862,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 632; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8901,7 +8901,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.14.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9449,7 +9449,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 632; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9482,7 +9482,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.14.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", From 4202b5451ca1fb01b3df986dff25ab49e4bc1ad5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Sep 2025 12:11:29 +1000 Subject: [PATCH 211/244] Resolved a TODO that was missed in the last release --- Session/Home/App Review/AppReviewPromptModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift index 9d9d82e2c8..d7c766e66c 100644 --- a/Session/Home/App Review/AppReviewPromptModel.swift +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -17,8 +17,7 @@ struct AppReviewPromptModel { extension AppReviewPromptModel { // Base version where app review prompt became available - // TODO: Update this once a version to include app review prompt is decided - static private let reviewPromptAvailabilityVersion = "2.14.1" // stringlint:ignore + static private let reviewPromptAvailabilityVersion = "2.14.2" // stringlint:ignore /// Determines the initial state of the app review prompt. static func loadInitialAppReviewPromptState(using dependencies: Dependencies) -> AppReviewPromptState? { From d56923a7e58c5240f5ed525d935925cff5dd5fca Mon Sep 17 00:00:00 2001 From: mikoldin Date: Tue, 16 Sep 2025 11:15:40 +0800 Subject: [PATCH 212/244] Fix missing `session_foundation` value for voice call dialog --- Session/Settings/PrivacySettingsViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 86f4dc37b7..89e7efc94c 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -233,7 +233,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), confirmationInfo: ConfirmationModal.Info( title: "callsVoiceAndVideoBeta".localized(), - body: .text("callsVoiceAndVideoModalDescription".localized()), + body: .text("callsVoiceAndVideoModalDescription" + .put(key: "session_foundation", value: Constants.session_foundation) + .localized()), showCondition: .disabled, confirmTitle: "theContinue".localized(), confirmStyle: .danger, From 3f7dba7bd9b9a1c37bed76cc71b60c75ead45ff8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 16 Sep 2025 17:09:26 +1000 Subject: [PATCH 213/244] Started working on adding dev settings to test subscriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Split group and pro developer settings into their own screens • Tweaked the SessionCell so it would respect theming included in subtitles • Added a few more themes for attributed strings (mostly to make dev settings a bit nicer) --- Session.xcodeproj/project.pbxproj | 20 +- .../DeveloperSettingsGroupsViewModel.swift | 391 ++++++++++++++++ .../DeveloperSettingsProViewModel.swift | 439 ++++++++++++++++++ .../DeveloperSettingsViewModel+Testing.swift | 0 .../DeveloperSettingsViewModel.swift | 413 ++-------------- Session/Shared/Views/SessionCell.swift | 4 +- .../Utilities/Localization+Style.swift | 9 + 7 files changed, 899 insertions(+), 377 deletions(-) create mode 100644 Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift create mode 100644 Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift rename Session/Settings/{ => DeveloperSettings}/DeveloperSettingsViewModel+Testing.swift (100%) rename Session/Settings/{ => DeveloperSettings}/DeveloperSettingsViewModel.swift (79%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 582038180a..e6fc67b80f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1041,6 +1041,8 @@ FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */; }; FDE521A62E0E6C8C00061B8E /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; + FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */; }; + FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; @@ -2307,6 +2309,8 @@ FDE5219F2E0D22FD00061B8E /* ObservationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationManager.swift; sourceTree = ""; }; FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionMessagingKit.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsGroupsViewModel.swift; sourceTree = ""; }; + FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsProViewModel.swift; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; @@ -3456,6 +3460,7 @@ C360969125AD1765008B62B2 /* Settings */ = { isa = PBXGroup; children = ( + FDE71B092E7934DC0023F5F9 /* DeveloperSettings */, FD8A5B002DBEFBF9004C689B /* SessionNetworkScreen */, FD37E9CD28A1E682003AE748 /* Views */, 9422569A2C23F8F000C0FDBF /* QRCodeScreen.swift */, @@ -3471,8 +3476,6 @@ FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, - FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, - FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -5059,6 +5062,17 @@ path = JobRunner; sourceTree = ""; }; + FDE71B092E7934DC0023F5F9 /* DeveloperSettings */ = { + isa = PBXGroup; + children = ( + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, + FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, + FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */, + FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */, + ); + path = DeveloperSettings; + sourceTree = ""; + }; FDE7214E287E50D50093DF33 /* Scripts */ = { isa = PBXGroup; children = ( @@ -6922,6 +6936,7 @@ FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */, FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, + FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */, 7B71A98F2925E2A600E54854 /* SessionFooterView.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */, @@ -6989,6 +7004,7 @@ 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */, 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, + FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift new file mode 100644 index 0000000000..61d62d1034 --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift @@ -0,0 +1,391 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import DifferenceKit +import SessionUIKit +import SessionNetworkingKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperSettingsGroupsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: State + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = State.initialState(using: dependencies) + + /// Bind the state + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(DeveloperSettingsGroupsViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + // FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism + let oldState: State = self.internalState + self.internalState = updatedState + self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState)) + } + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case general + + var title: String? { + switch self { + case .general: return nil + } + } + + var style: SessionTableSectionStyle { + switch self { + case .general: return .padding + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case updatedGroupsDisableAutoApprove + case updatedGroupsRemoveMessagesOnKick + case updatedGroupsAllowHistoricAccessOnInvite + case updatedGroupsAllowDisplayPicture + case updatedGroupsAllowDescriptionEditing + case updatedGroupsAllowPromotions + case updatedGroupsAllowInviteById + case updatedGroupsDeleteBeforeNow + case updatedGroupsDeleteAttachmentsBeforeNow + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" + case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" + case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" + case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture" + case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" + case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" + case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" + case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" + case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.updatedGroupsDisableAutoApprove { + case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough + case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough + case .updatedGroupsAllowHistoricAccessOnInvite: + result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough + case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough + case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough + case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough + case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough + case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough + case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow) + } + + return result + } + } + + // MARK: - Content + + public struct State: Equatable, ObservableKeyProvider { + let updatedGroupsDisableAutoApprove: Bool + let updatedGroupsRemoveMessagesOnKick: Bool + let updatedGroupsAllowHistoricAccessOnInvite: Bool + let updatedGroupsAllowDisplayPicture: Bool + let updatedGroupsAllowDescriptionEditing: Bool + let updatedGroupsAllowPromotions: Bool + let updatedGroupsAllowInviteById: Bool + let updatedGroupsDeleteBeforeNow: Bool + let updatedGroupsDeleteAttachmentsBeforeNow: Bool + + @MainActor public func sections(viewModel: DeveloperSettingsGroupsViewModel, previousState: State) -> [SectionModel] { + DeveloperSettingsGroupsViewModel.sections( + state: self, + previousState: previousState, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .feature(.updatedGroupsDisableAutoApprove), + .feature(.updatedGroupsRemoveMessagesOnKick), + .feature(.updatedGroupsAllowHistoricAccessOnInvite), + .feature(.updatedGroupsAllowDisplayPicture), + .feature(.updatedGroupsAllowDescriptionEditing), + .feature(.updatedGroupsAllowPromotions), + .feature(.updatedGroupsAllowInviteById), + .feature(.updatedGroupsDeleteBeforeNow), + .feature(.updatedGroupsDeleteAttachmentsBeforeNow) + ] + + static func initialState(using dependencies: Dependencies) -> State { + return State( + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], + updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], + updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] + ) + } + } + + let title: String = "Developer Group Settings" + + @Sendable private static func queryState( + previousState: State, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> State { + return State( + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], + updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], + updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] + ) + } + + private static func sections( + state: State, + previousState: State, + viewModel: DeveloperSettingsGroupsViewModel + ) -> [SectionModel] { + let general: SectionModel = SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .updatedGroupsDisableAutoApprove, + title: "Disable Auto Approve", + subtitle: """ + Prevents a group from automatically getting approved if the admin is already approved. + + Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. + """, + trailingAccessory: .toggle( + state.updatedGroupsDisableAutoApprove, + oldValue: previousState.updatedGroupsDisableAutoApprove + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDisableAutoApprove, + to: !state.updatedGroupsDisableAutoApprove + ) + } + ), + SessionCell.Info( + id: .updatedGroupsRemoveMessagesOnKick, + title: "Remove Messages on Kick", + subtitle: """ + Controls whether a group members messages should be removed when they are kicked from an updated group. + + Note: In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsRemoveMessagesOnKick, + oldValue: previousState.updatedGroupsRemoveMessagesOnKick + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsRemoveMessagesOnKick, + to: !state.updatedGroupsRemoveMessagesOnKick + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowHistoricAccessOnInvite, + title: "Allow Historic Message Access", + subtitle: """ + Controls whether members should be granted access to historic messages when invited to an updated group. + + Note: In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowHistoricAccessOnInvite, + oldValue: previousState.updatedGroupsAllowHistoricAccessOnInvite + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowHistoricAccessOnInvite, + to: !state.updatedGroupsAllowHistoricAccessOnInvite + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDisplayPicture, + title: "Custom Display Pictures", + subtitle: """ + Controls whether the UI allows group admins to set a custom display picture for a group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowDisplayPicture, + oldValue: previousState.updatedGroupsAllowDisplayPicture + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowDisplayPicture, + to: !state.updatedGroupsAllowDisplayPicture + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDescriptionEditing, + title: "Edit Group Descriptions", + subtitle: """ + Controls whether the UI allows group admins to modify the descriptions of updated groups. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowDescriptionEditing, + oldValue: previousState.updatedGroupsAllowDescriptionEditing + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowDescriptionEditing, + to: !state.updatedGroupsAllowDescriptionEditing + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowPromotions, + title: "Allow Group Promotions", + subtitle: """ + Controls whether the UI allows group admins to promote other group members to admin within an updated group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowPromotions, + oldValue: previousState.updatedGroupsAllowPromotions + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowPromotions, + to: !state.updatedGroupsAllowPromotions + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowInviteById, + title: "Allow Invite by ID", + subtitle: """ + Controls whether the UI allows group admins to invite other group members directly by their Account ID. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowInviteById, + oldValue: previousState.updatedGroupsAllowInviteById + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowInviteById, + to: !state.updatedGroupsAllowInviteById + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteBeforeNow, + title: "Show button to delete messages before now", + subtitle: """ + Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsDeleteBeforeNow, + oldValue: previousState.updatedGroupsDeleteBeforeNow + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDeleteBeforeNow, + to: !state.updatedGroupsDeleteBeforeNow + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteAttachmentsBeforeNow, + title: "Show button to delete attachments before now", + subtitle: """ + Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsDeleteAttachmentsBeforeNow, + oldValue: previousState.updatedGroupsDeleteAttachmentsBeforeNow + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDeleteAttachmentsBeforeNow, + to: !state.updatedGroupsDeleteAttachmentsBeforeNow + ) + } + ) + ] + ) + + return [general] + } + + // MARK: - Functions + + public static func disableDeveloperMode(using dependencies: Dependencies) { + let features: [FeatureConfig] = [ + .updatedGroupsDisableAutoApprove, + .updatedGroupsRemoveMessagesOnKick, + .updatedGroupsAllowHistoricAccessOnInvite, + .updatedGroupsAllowDisplayPicture, + .updatedGroupsAllowDescriptionEditing, + .updatedGroupsAllowPromotions, + .updatedGroupsAllowInviteById, + .updatedGroupsDeleteBeforeNow, + .updatedGroupsDeleteAttachmentsBeforeNow + ] + + features.forEach { feature in + guard dependencies.hasSet(feature: feature) else { return } + + dependencies.set(feature: feature, to: nil) + } + } +} diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift new file mode 100644 index 0000000000..0eb37764ab --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -0,0 +1,439 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import StoreKit +import Combine +import GRDB +import DifferenceKit +import SessionUIKit +import SessionNetworkingKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: State + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = State.initialState(using: dependencies) + + /// Bind the state + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(DeveloperSettingsProViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + // FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism + let oldState: State = self.internalState + self.internalState = updatedState + self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState)) + } + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case general + case subscriptions + case features + + var title: String? { + switch self { + case .general: return nil + case .subscriptions: return "Subscriptions" + case .features: return "Features" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .general: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case enableSessionPro + + case purchaseProSubscription + case manageProSubscriptions + case restoreProSubscription + + case proStatus + case proIncomingMessages + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .enableSessionPro: return "enableSessionPro" + + case .purchaseProSubscription: return "purchaseProSubscription" + case .manageProSubscriptions: return "manageProSubscriptions" + case .restoreProSubscription: return "restoreProSubscription" + + case .proStatus: return "proStatus" + case .proIncomingMessages: return "proIncomingMessages" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.enableSessionPro { + case .enableSessionPro: result.append(.enableSessionPro); fallthrough + + case .purchaseProSubscription: result.append(.purchaseProSubscription); fallthrough + case .manageProSubscriptions: result.append(.manageProSubscriptions); fallthrough + case .restoreProSubscription: result.append(.restoreProSubscription); fallthrough + + case .proStatus: result.append(.proStatus); fallthrough + case .proIncomingMessages: result.append(.proIncomingMessages) + } + + return result + } + } + + public enum DeveloperSettingsProEvent: Hashable { + case purchasedProduct([Product], Product?, String?, String?, UInt64?) + } + + // MARK: - Content + + public struct State: Equatable, ObservableKeyProvider { + let sessionProEnabled: Bool + + let products: [Product] + let purchasedProduct: Product? + let purchaseError: String? + let purchaseStatus: String? + let purchaseTransactionId: String? + + let mockCurrentUserSessionPro: Bool + let treatAllIncomingMessagesAsProMessages: Bool + + @MainActor public func sections(viewModel: DeveloperSettingsProViewModel, previousState: State) -> [SectionModel] { + DeveloperSettingsProViewModel.sections( + state: self, + previousState: previousState, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .feature(.sessionProEnabled), + .updateScreen(DeveloperSettingsProViewModel.self), + .feature(.mockCurrentUserSessionPro), + .feature(.treatAllIncomingMessagesAsProMessages) + ] + + static func initialState(using dependencies: Dependencies) -> State { + return State( + sessionProEnabled: dependencies[feature: .sessionProEnabled], + + products: [], + purchasedProduct: nil, + purchaseError: nil, + purchaseStatus: nil, + purchaseTransactionId: nil, + + mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], + treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages] + ) + } + } + + let title: String = "Developer Pro Settings" + + @Sendable private static func queryState( + previousState: State, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> State { + var products: [Product] = previousState.products + var purchasedProduct: Product? = previousState.purchasedProduct + var purchaseError: String? = previousState.purchaseError + var purchaseStatus: String? = previousState.purchaseStatus + var purchaseTransactionId: String? = previousState.purchaseTransactionId + + events.forEach { event in + guard let eventValue: DeveloperSettingsProEvent = event.value as? DeveloperSettingsProEvent else { return } + + switch eventValue { + case .purchasedProduct(let receivedProducts, let purchased, let error, let status, let id): + products = receivedProducts + purchasedProduct = purchased + purchaseError = error + purchaseStatus = status + purchaseTransactionId = id.map { "\($0)" } + } + } + + return State( + sessionProEnabled: dependencies[feature: .sessionProEnabled], + products: products, + purchasedProduct: purchasedProduct, + purchaseError: purchaseError, + purchaseStatus: purchaseStatus, + purchaseTransactionId: purchaseTransactionId, + mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], + treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages] + ) + } + + private static func sections( + state: State, + previousState: State, + viewModel: DeveloperSettingsProViewModel + ) -> [SectionModel] { + let general: SectionModel = SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .enableSessionPro, + title: "Enable Session Pro", + subtitle: """ + Enable Post Pro Release mode. + Turning on this Settings will show Pro badge and CTA if needed. + """, + trailingAccessory: .toggle( + state.sessionProEnabled, + oldValue: previousState.sessionProEnabled + ), + onTap: { [weak viewModel] in + viewModel?.updateSessionProEnabled(current: state.sessionProEnabled) + } + ) + ] + ) + + guard state.sessionProEnabled else { return [general] } + + let purchaseStatus: String = { + switch (state.purchaseError, state.purchaseStatus) { + case (.some(let error), _): return "\(error)" + case (_, .some(let status)): return "\(status)" + case (.none, .none): return "None" + } + }() + let productName: String = ( + state.purchasedProduct.map { "\($0.displayName)" } ?? + "N/A" + ) + let transactionId: String = ( + state.purchaseTransactionId.map { "\($0)" } ?? + "N/A" + ) + let subscriptions: SectionModel = SectionModel( + model: .subscriptions, + elements: [ + SessionCell.Info( + id: .purchaseProSubscription, + title: "Purchase Subscription", + subtitle: """ + Purchase Session Pro via the App Store. + + Status: \(purchaseStatus) + Product Name: \(productName) + TransactionId: \(transactionId) + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Purchase"), + onTap: { [weak viewModel] in + Task { await viewModel?.purchaseSubscription() } + } + ), + SessionCell.Info( + id: .manageProSubscriptions, + title: "Manage Subscriptions", + subtitle: """ + Manage subscriptions for Session Pro via the App Store. + + Note: You must purchase a Session Pro subscription before you can manage it. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Manage"), + onTap: { [weak viewModel] in + Task { await viewModel?.manageSubscriptions() } + } + ), + SessionCell.Info( + id: .restoreProSubscription, + title: "Restore Subscriptions", + subtitle: """ + Restore a Session Pro subscription via the App Store. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Restore"), + onTap: { [weak viewModel] in + Task { await viewModel?.restoreSubscriptions() } + } + ) + ] + ) + + let features: SectionModel = SectionModel( + model: .features, + elements: [ + SessionCell.Info( + id: .proStatus, + title: "Pro Status", + subtitle: """ + Mock current user a Session Pro user locally. + """, + trailingAccessory: .toggle( + state.mockCurrentUserSessionPro, + oldValue: previousState.mockCurrentUserSessionPro + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .mockCurrentUserSessionPro, + to: !state.mockCurrentUserSessionPro + ) + } + ), + SessionCell.Info( + id: .proIncomingMessages, + title: "All Pro Incoming Messages", + subtitle: """ + Treat all incoming messages as Pro messages. + """, + trailingAccessory: .toggle( + state.treatAllIncomingMessagesAsProMessages, + oldValue: previousState.treatAllIncomingMessagesAsProMessages + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .treatAllIncomingMessagesAsProMessages, + to: !state.treatAllIncomingMessagesAsProMessages + ) + } + ) + ] + ) + + return [general, subscriptions, features] + } + + // MARK: - Functions + + public static func disableDeveloperMode(using dependencies: Dependencies) { + let features: [FeatureConfig] = [ + .sessionProEnabled, + .mockCurrentUserSessionPro, + .treatAllIncomingMessagesAsProMessages + ] + + features.forEach { feature in + guard dependencies.hasSet(feature: feature) else { return } + + dependencies.set(feature: feature, to: nil) + } + } + + private func updateSessionProEnabled(current: Bool) { + dependencies.set(feature: .sessionProEnabled, to: !current) + + if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { + dependencies.set(feature: .mockCurrentUserSessionPro, to: nil) + } + + if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { + dependencies.set(feature: .treatAllIncomingMessagesAsProMessages, to: nil) + } + } + + private func purchaseSubscription() async { + do { + let products: [Product] = try await Product.products(for: ["com.getsession.org.pro_sub"]) + + guard let product: Product = products.first else { + Log.error("[DevSettings] Unable to purchase subscription due to error: No products found") + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct([], nil, "No products found", nil, nil) + ) + return + } + + let result = try await product.purchase() + switch result { + case .success(let verificationResult): + let transaction = try verificationResult.payloadValue + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "Successful", transaction.id) + ) + await transaction.finish() + + case .pending: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "Pending approval", nil) + ) + + case .userCancelled: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "User cancelled", nil) + ) + + @unknown default: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, "Unknown Error", nil, nil) + ) + } + + } + catch { + Log.error("[DevSettings] Unable to purchase subscription due to error: \(error)") + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct([], nil, "Failed: \(error)", nil, nil) + ) + } + } + + private func manageSubscriptions() async { + guard let scene: UIWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + return Log.error("[DevSettings] Unable to show manage subscriptions: Unable to get UIWindowScene") + } + + do { + try await AppStore.showManageSubscriptions(in: scene) + print("AS") + } + catch { + Log.error("[DevSettings] Unable to show manage subscriptions: \(error)") + } + } + + private func restoreSubscriptions() async { + do { + try await AppStore.sync() + } + catch { + Log.error("[DevSettings] Unable to show manage subscriptions: \(error)") + } + } +} diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift similarity index 100% rename from Session/Settings/DeveloperSettingsViewModel+Testing.swift rename to Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift similarity index 79% rename from Session/Settings/DeveloperSettingsViewModel.swift rename to Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift index 78ea7b150c..05b772057e 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift @@ -71,13 +71,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum TableItem: Hashable, Differentiable, CaseIterable { case developerMode - case enableSessionPro - case proStatus - case proIncomingMessages - - case versionBlindedID - case scheduleLocalNotification - case animationsEnabled case showStringKeys case truncatePubkeysInLogs @@ -99,15 +92,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case communityPollLimit - case updatedGroupsDisableAutoApprove - case updatedGroupsRemoveMessagesOnKick - case updatedGroupsAllowHistoricAccessOnInvite - case updatedGroupsAllowDisplayPicture - case updatedGroupsAllowDescriptionEditing - case updatedGroupsAllowPromotions - case updatedGroupsAllowInviteById - case updatedGroupsDeleteBeforeNow - case updatedGroupsDeleteAttachmentsBeforeNow + case groupConfig + case proConfig + + case versionBlindedID + case scheduleLocalNotification case createMockContacts case forceSlowDatabaseQueries @@ -142,22 +131,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .communityPollLimit: return "communityPollLimit" - case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" - case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" - case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" - case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture" - case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" - case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" - case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" - case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" - case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" + case .groupConfig: return "groupConfig" + case .proConfig: return "proConfig" case .versionBlindedID: return "versionBlindedID" case .scheduleLocalNotification: return "scheduleLocalNotification" - - case .enableSessionPro: return "enableSessionPro" - case .proStatus: return "proStatus" - case .proIncomingMessages: return "proIncomingMessages" case .createMockContacts: return "createMockContacts" case .forceSlowDatabaseQueries: return "forceSlowDatabaseQueries" @@ -195,24 +173,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .communityPollLimit: result.append(.communityPollLimit); fallthrough - case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough - case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough - case .updatedGroupsAllowHistoricAccessOnInvite: - result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough - case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough - case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough - case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough - case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough - case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough - case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow); fallthrough + case .groupConfig: result.append(.groupConfig); fallthrough + case .proConfig: result.append(.proConfig); fallthrough case .versionBlindedID: result.append(.versionBlindedID); fallthrough case .scheduleLocalNotification: result.append(.scheduleLocalNotification); fallthrough - case .enableSessionPro: result.append(.enableSessionPro); fallthrough - case .proStatus: result.append(.proStatus); fallthrough - case .proIncomingMessages: result.append(.proIncomingMessages); fallthrough - case .createMockContacts: result.append(.createMockContacts); fallthrough case .forceSlowDatabaseQueries: result.append(.forceSlowDatabaseQueries); fallthrough case .exportDatabase: result.append(.exportDatabase); fallthrough @@ -245,20 +211,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let communityPollLimit: Int - let updatedGroupsDisableAutoApprove: Bool - let updatedGroupsRemoveMessagesOnKick: Bool - let updatedGroupsAllowHistoricAccessOnInvite: Bool - let updatedGroupsAllowDisplayPicture: Bool - let updatedGroupsAllowDescriptionEditing: Bool - let updatedGroupsAllowPromotions: Bool - let updatedGroupsAllowInviteById: Bool - let updatedGroupsDeleteBeforeNow: Bool - let updatedGroupsDeleteAttachmentsBeforeNow: Bool - - let sessionProEnabled: Bool - let mockCurrentUserSessionPro: Bool - let treatAllIncomingMessagesAsProMessages: Bool - let forceSlowDatabaseQueries: Bool let updateSimulateAppReviewLimit: Bool @@ -302,20 +254,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, communityPollLimit: dependencies[feature: .communityPollLimit], - updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], - updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], - updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], - updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], - updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], - updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], - updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], - updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], - updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow], - - sessionProEnabled: dependencies[feature: .sessionProEnabled], - mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], - treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages], - forceSlowDatabaseQueries: dependencies[feature: .forceSlowDatabaseQueries], updateSimulateAppReviewLimit: dependencies[feature: .simulateAppReviewLimit] ) @@ -663,173 +601,39 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, model: .groups, elements: [ SessionCell.Info( - id: .updatedGroupsDisableAutoApprove, - title: "Disable Auto Approve", - subtitle: """ - Prevents a group from automatically getting approved if the admin is already approved. - - Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. - """, - trailingAccessory: .toggle( - current.updatedGroupsDisableAutoApprove, - oldValue: previous?.updatedGroupsDisableAutoApprove - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDisableAutoApprove, - to: !current.updatedGroupsDisableAutoApprove - ) - } - ), - SessionCell.Info( - id: .updatedGroupsRemoveMessagesOnKick, - title: "Remove Messages on Kick", - subtitle: """ - Controls whether a group members messages should be removed when they are kicked from an updated group. - - Note: In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsRemoveMessagesOnKick, - oldValue: previous?.updatedGroupsRemoveMessagesOnKick - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsRemoveMessagesOnKick, - to: !current.updatedGroupsRemoveMessagesOnKick - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowHistoricAccessOnInvite, - title: "Allow Historic Message Access", - subtitle: """ - Controls whether members should be granted access to historic messages when invited to an updated group. - - Note: In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowHistoricAccessOnInvite, - oldValue: previous?.updatedGroupsAllowHistoricAccessOnInvite - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowHistoricAccessOnInvite, - to: !current.updatedGroupsAllowHistoricAccessOnInvite - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowDisplayPicture, - title: "Custom Display Pictures", - subtitle: """ - Controls whether the UI allows group admins to set a custom display picture for a group. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowDisplayPicture, - oldValue: previous?.updatedGroupsAllowDisplayPicture - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowDisplayPicture, - to: !current.updatedGroupsAllowDisplayPicture - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowDescriptionEditing, - title: "Edit Group Descriptions", - subtitle: """ - Controls whether the UI allows group admins to modify the descriptions of updated groups. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowDescriptionEditing, - oldValue: previous?.updatedGroupsAllowDescriptionEditing - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowDescriptionEditing, - to: !current.updatedGroupsAllowDescriptionEditing - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowPromotions, - title: "Allow Group Promotions", - subtitle: """ - Controls whether the UI allows group admins to promote other group members to admin within an updated group. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowPromotions, - oldValue: previous?.updatedGroupsAllowPromotions - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowPromotions, - to: !current.updatedGroupsAllowPromotions - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowInviteById, - title: "Allow Invite by ID", - subtitle: """ - Controls whether the UI allows group admins to invite other group members directly by their Account ID. - - Note: In a future release we will offer this functionality but it's not included in the initial release. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowInviteById, - oldValue: previous?.updatedGroupsAllowInviteById - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowInviteById, - to: !current.updatedGroupsAllowInviteById - ) - } - ), - SessionCell.Info( - id: .updatedGroupsDeleteBeforeNow, - title: "Show button to delete messages before now", + id: .groupConfig, + title: "Group Configuration", subtitle: """ - Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. - - Note: In a future release we will offer this functionality but it's not included in the initial release. + Configure settings related to Groups. """, - trailingAccessory: .toggle( - current.updatedGroupsDeleteBeforeNow, - oldValue: previous?.updatedGroupsDeleteBeforeNow - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDeleteBeforeNow, - to: !current.updatedGroupsDeleteBeforeNow + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsGroupsViewModel(using: dependencies) + ) ) } - ), + ) + ] + ) + let sessionPro: SectionModel = SectionModel( + model: .sessionPro, + elements: [ SessionCell.Info( - id: .updatedGroupsDeleteAttachmentsBeforeNow, - title: "Show button to delete attachments before now", + id: .proConfig, + title: "Session Pro", subtitle: """ - Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. + Configure settings related to Session Pro. - Note: In a future release we will offer this functionality but it's not included in the initial release. + Session Pro: \(dependencies[feature: .sessionProEnabled] ? "Enabled" : "Disabled") """, - trailingAccessory: .toggle( - current.updatedGroupsDeleteAttachmentsBeforeNow, - oldValue: previous?.updatedGroupsDeleteAttachmentsBeforeNow - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDeleteAttachmentsBeforeNow, - to: !current.updatedGroupsDeleteAttachmentsBeforeNow + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsProViewModel(using: dependencies) + ) ) } ) @@ -898,63 +702,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) - let sessionPro: SectionModel = SectionModel( - model: .sessionPro, - elements: [ - SessionCell.Info( - id: .enableSessionPro, - title: "Enable Session Pro", - subtitle: """ - Enable Post Pro Release mode. - Turning on this Settings will show Pro badge and CTA if needed. - """, - trailingAccessory: .toggle( - current.sessionProEnabled, - oldValue: previous?.sessionProEnabled - ), - onTap: { [weak self] in - self?.updateSessionProEnabled(current: current.sessionProEnabled) - } - ) - ].appending( - contentsOf: current.sessionProEnabled ? [ - SessionCell.Info( - id: .proStatus, - title: "Pro Status", - subtitle: """ - Mock current user a Session Pro user locally. - """, - trailingAccessory: .toggle( - current.mockCurrentUserSessionPro, - oldValue: previous?.mockCurrentUserSessionPro - ), - onTap: { [weak self] in - self?.updateFlag( - for: .mockCurrentUserSessionPro, - to: !current.mockCurrentUserSessionPro - ) - } - ), - SessionCell.Info( - id: .proIncomingMessages, - title: "All Pro Incoming Messages", - subtitle: """ - Treat all incoming messages as Pro messages. - """, - trailingAccessory: .toggle( - current.treatAllIncomingMessagesAsProMessages, - oldValue: previous?.treatAllIncomingMessagesAsProMessages - ), - onTap: { [weak self] in - self?.updateFlag( - for: .treatAllIncomingMessagesAsProMessages, - to: !current.treatAllIncomingMessagesAsProMessages - ) - } - ) - ] : nil - ) - ) let sessionNetwork: SectionModel = SectionModel( model: .sessionNetwork, elements: [ @@ -1012,9 +759,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// then we will get a compile error if it doesn't get resetting instructions added) TableItem.allCases.forEach { item in switch item { - case .developerMode: break // Not a feature - case .versionBlindedID: break // Not a feature - case .scheduleLocalNotification: break // Not a feature + case .developerMode, .versionBlindedID, .scheduleLocalNotification, .copyDocumentsPath, + .copyAppGroupPath, .resetSnodeCache, .createMockContacts, .exportDatabase, + .importDatabase, .advancedLogging, .resetAppReviewPrompt: + break /// These are actions rather than values stored as "features" so no need to do anything case .animationsEnabled: guard dependencies.hasSet(feature: .animationsEnabled) else { return } @@ -1031,18 +779,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .truncatePubkeysInLogs, to: nil) - case .copyDocumentsPath: break // Not a feature - case .copyAppGroupPath: break // Not a feature - case .resetAppReviewPrompt: break case .simulateAppReviewLimit: guard dependencies.hasSet(feature: .simulateAppReviewLimit) else { return } updateFlag(for: .simulateAppReviewLimit, to: nil) - case .resetSnodeCache: break // Not a feature - case .createMockContacts: break // Not a feature - case .exportDatabase: break // Not a feature - case .importDatabase: break // Not a feature - case .advancedLogging: break // Not a feature case .defaultLogLevel: updateDefaulLogLevel(to: nil) // Always reset case .loggingCategory: resetLoggingCategories() // Always reset @@ -1073,71 +813,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.set(feature: .communityPollLimit, to: nil) forceRefresh(type: .databaseQuery) - case .updatedGroupsDisableAutoApprove: - guard dependencies.hasSet(feature: .updatedGroupsDisableAutoApprove) else { return } - - updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil) - - case .updatedGroupsRemoveMessagesOnKick: - guard dependencies.hasSet(feature: .updatedGroupsRemoveMessagesOnKick) else { return } - - updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil) - - case .updatedGroupsAllowHistoricAccessOnInvite: - guard dependencies.hasSet(feature: .updatedGroupsAllowHistoricAccessOnInvite) else { - return - } - - updateFlag(for: .updatedGroupsAllowHistoricAccessOnInvite, to: nil) - - case .updatedGroupsAllowDisplayPicture: - guard dependencies.hasSet(feature: .updatedGroupsAllowDisplayPicture) else { return } - - updateFlag(for: .updatedGroupsAllowDisplayPicture, to: nil) - - case .updatedGroupsAllowDescriptionEditing: - guard dependencies.hasSet(feature: .updatedGroupsAllowDescriptionEditing) else { return } - - updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) - - case .updatedGroupsAllowPromotions: - guard dependencies.hasSet(feature: .updatedGroupsAllowPromotions) else { return } - - updateFlag(for: .updatedGroupsAllowPromotions, to: nil) - - case .updatedGroupsAllowInviteById: - guard dependencies.hasSet(feature: .updatedGroupsAllowInviteById) else { return } - - updateFlag(for: .updatedGroupsAllowInviteById, to: nil) - - case .updatedGroupsDeleteBeforeNow: - guard dependencies.hasSet(feature: .updatedGroupsDeleteBeforeNow) else { return } - - updateFlag(for: .updatedGroupsDeleteBeforeNow, to: nil) - - case .updatedGroupsDeleteAttachmentsBeforeNow: - guard dependencies.hasSet(feature: .updatedGroupsDeleteAttachmentsBeforeNow) else { - return - } - - updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil) - - case .enableSessionPro: - guard dependencies.hasSet(feature: .sessionProEnabled) else { return } - - updateFlag(for: .sessionProEnabled, to: nil) - - case .proStatus: - guard dependencies.hasSet(feature: .mockCurrentUserSessionPro) else { return } - - updateFlag(for: .mockCurrentUserSessionPro, to: nil) - - case .proIncomingMessages: - guard dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) else { - return - } - - updateFlag(for: .treatAllIncomingMessagesAsProMessages, to: nil) + case .groupConfig: DeveloperSettingsGroupsViewModel.disableDeveloperMode(using: dependencies) + case .proConfig: DeveloperSettingsProViewModel.disableDeveloperMode(using: dependencies) case .forceSlowDatabaseQueries: guard dependencies.hasSet(feature: .forceSlowDatabaseQueries) else { return } @@ -1347,16 +1024,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } - private func updateSessionProEnabled(current: Bool) { - updateFlag(for: .sessionProEnabled, to: !current) - if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { - updateFlag(for: .mockCurrentUserSessionPro, to: nil) - } - if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { - updateFlag(for: .treatAllIncomingMessagesAsProMessages, to: nil) - } - } - private func updateForceOffline(current: Bool) { updateFlag(for: .forceOffline, to: !current) diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 47bdebb394..8a0a352752 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -575,19 +575,19 @@ public class SessionCell: UITableViewCell { titleTextField.accessibilityLabel = info.title?.accessibility?.label subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) subtitleLabel.font = info.subtitle?.font + subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.themeAttributedText = info.subtitle.map { subtitle -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: subtitle.text, font: subtitle.font) } - subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.textAlignment = (info.subtitle?.textAlignment ?? .left) subtitleLabel.accessibilityIdentifier = info.subtitle?.accessibility?.identifier subtitleLabel.accessibilityLabel = info.subtitle?.accessibility?.label subtitleLabel.isHidden = (info.subtitle == nil) expandableDescriptionLabel.font = info.description?.font ?? .systemFont(ofSize: 12) + expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.themeAttributedText = info.description.map { description -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: description.text, font: description.font) } - expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.textAlignment = (info.description?.textAlignment ?? .left) expandableDescriptionLabel.accessibilityIdentifier = info.description?.accessibility?.identifier expandableDescriptionLabel.accessibilityLabel = info.description?.accessibility?.label diff --git a/SessionUIKit/Utilities/Localization+Style.swift b/SessionUIKit/Utilities/Localization+Style.swift index 788e777c10..43dce7671e 100644 --- a/SessionUIKit/Utilities/Localization+Style.swift +++ b/SessionUIKit/Utilities/Localization+Style.swift @@ -20,6 +20,9 @@ public extension ThemedAttributedString { case strikethrough = "s" case primaryTheme = "span" case icon = "icon" + case warningTheme = "warn" + case dangerTheme = "error" + case disabledTheme = "disabled" // MARK: - Functions @@ -53,6 +56,9 @@ public extension ThemedAttributedString { case .strikethrough: return [.strikethroughStyle: NSUnderlineStyle.single.rawValue] case .primaryTheme: return [.themeForegroundColor: ThemeValue.sessionButton_text] case .icon: return Lucide.attributes(for: font) + case .warningTheme: return [.themeForegroundColor: ThemeValue.warning] + case .dangerTheme: return [.themeForegroundColor: ThemeValue.danger] + case .disabledTheme: return [.themeForegroundColor: ThemeValue.disabled] } } } @@ -184,6 +190,9 @@ private extension Collection where Element == ThemedAttributedString.HTMLTag { case .icon: result[.font] = fontWith(Lucide.font(ofSize: (font.pointSize + 1)), traits: []) result[.baselineOffset] = Lucide.defaultBaselineOffset + case .warningTheme: result[.themeForegroundColor] = ThemeValue.warning + case .dangerTheme: result[.themeForegroundColor] = ThemeValue.danger + case .disabledTheme: result[.themeForegroundColor] = ThemeValue.disabled } } } From a951f8de5959b2d556e436e8710cf6593aafaf1d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 09:05:32 +1000 Subject: [PATCH 214/244] Added a StoreKit config for testing, reordered some dev settings --- Session.xcodeproj/project.pbxproj | 8 ++ .../Session - Anonymous Messenger.storekit | 117 ++++++++++++++++++ .../DeveloperSettingsViewModel.swift | 115 ++++++++--------- 3 files changed, 184 insertions(+), 56 deletions(-) create mode 100644 Session/Meta/Session - Anonymous Messenger.storekit diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e6fc67b80f..2a07fbb52f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1043,6 +1043,8 @@ FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */; }; FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */; }; + FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */ = {isa = PBXBuildFile; fileRef = FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */; }; + FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; @@ -2311,6 +2313,8 @@ FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsGroupsViewModel.swift; sourceTree = ""; }; FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsProViewModel.swift; sourceTree = ""; }; + FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Session - Anonymous Messenger.storekit"; sourceTree = ""; }; + FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; @@ -2564,6 +2568,7 @@ A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */, FD6DA9CF2D015B440092085A /* Lucide in Frameworks */, A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */, + FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */, D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */, C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */, @@ -3853,6 +3858,7 @@ FDE125222A837E4E002DA685 /* MainAppContext.swift */, C3CA3AA0255CDA7000F4C6D4 /* Mnemonic */, FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */, + FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */, B67EBF5C19194AC60084CCFD /* Settings.bundle */, B657DDC91911A40500F45B0C /* Signal.entitlements */, FDF2220A2818F38D000A4995 /* SessionApp.swift */, @@ -3909,6 +3915,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */, FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */, 3496955F21A2FC8100DCFE74 /* CloudKit.framework */, 455A16DB1F1FEA0000F86704 /* Metal.framework */, @@ -5866,6 +5873,7 @@ C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */, FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */, B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */, + FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */, 45B74A7E2044AAB600CD42F8 /* complete.aifc in Resources */, 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */, B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */, diff --git a/Session/Meta/Session - Anonymous Messenger.storekit b/Session/Meta/Session - Anonymous Messenger.storekit new file mode 100644 index 0000000000..901eabe896 --- /dev/null +++ b/Session/Meta/Session - Anonymous Messenger.storekit @@ -0,0 +1,117 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "E83EE03B", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_applicationInternalID" : "1470168868", + "_developerTeamID" : "SUQ8J2PCT7", + "_failTransactionsEnabled" : false, + "_lastSynchronizedDate" : 779753823.36554396, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "21752814", + "localizations" : [ + + ], + "name" : "Session Pro", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "6749836944", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Test 1 Week Session Pro Subscription", + "displayName" : "Test Session Pro", + "locale" : "en_US" + } + ], + "productID" : "com.getsession.org.pro_sub", + "recurringSubscriptionPeriod" : "P1W", + "referenceName" : "Session Pro Subscription", + "subscriptionGroupID" : "21752814", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + } + ] + } + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift index 05b772057e..b32ecb10ad 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift @@ -35,27 +35,27 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum Section: SessionTableSection { case developerMode - case sessionPro case sessionNetwork + case sessionPro + case groups case general case logging case network case disappearingMessages case communities - case groups case database var title: String? { switch self { case .developerMode: return nil - case .sessionPro: return "Session Pro" case .sessionNetwork: return "Session Network" + case .sessionPro: return "Session Pro" + case .groups: return "Groups" case .general: return "General" case .logging: return "Logging" case .network: return "Network" case .disappearingMessages: return "Disappearing Messages" case .communities: return "Communities" - case .groups: return "Groups" case .database: return "Database" } } @@ -71,6 +71,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum TableItem: Hashable, Differentiable, CaseIterable { case developerMode + case proConfig + case groupConfig + case animationsEnabled case showStringKeys case truncatePubkeysInLogs @@ -92,8 +95,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case communityPollLimit - case groupConfig - case proConfig case versionBlindedID case scheduleLocalNotification @@ -110,6 +111,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public var differenceIdentifier: String { switch self { case .developerMode: return "developerMode" + + case .proConfig: return "proConfig" + case .groupConfig: return "groupConfig" + case .animationsEnabled: return "animationsEnabled" case .showStringKeys: return "showStringKeys" case .truncatePubkeysInLogs: return "truncatePubkeysInLogs" @@ -131,9 +136,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .communityPollLimit: return "communityPollLimit" - case .groupConfig: return "groupConfig" - case .proConfig: return "proConfig" - case .versionBlindedID: return "versionBlindedID" case .scheduleLocalNotification: return "scheduleLocalNotification" @@ -152,6 +154,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, var result: [TableItem] = [] switch TableItem.developerMode { case .developerMode: result.append(.developerMode); fallthrough + + case .proConfig: result.append(.proConfig); fallthrough + case .groupConfig: result.append(.groupConfig); fallthrough + case .animationsEnabled: result.append(.animationsEnabled); fallthrough case .showStringKeys: result.append(.showStringKeys); fallthrough case .truncatePubkeysInLogs: result.append(.truncatePubkeysInLogs); fallthrough @@ -173,9 +179,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .communityPollLimit: result.append(.communityPollLimit); fallthrough - case .groupConfig: result.append(.groupConfig); fallthrough - case .proConfig: result.append(.proConfig); fallthrough - case .versionBlindedID: result.append(.versionBlindedID); fallthrough case .scheduleLocalNotification: result.append(.scheduleLocalNotification); fallthrough @@ -286,6 +289,48 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) + let sessionPro: SectionModel = SectionModel( + model: .sessionPro, + elements: [ + SessionCell.Info( + id: .proConfig, + title: "Session Pro", + subtitle: """ + Configure settings related to Session Pro. + + Session Pro: \(dependencies[feature: .sessionProEnabled] ? "Enabled" : "Disabled") + """, + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsProViewModel(using: dependencies) + ) + ) + } + ) + ] + ) + let groups: SectionModel = SectionModel( + model: .groups, + elements: [ + SessionCell.Info( + id: .groupConfig, + title: "Group Configuration", + subtitle: """ + Configure settings related to Groups. + """, + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsGroupsViewModel(using: dependencies) + ) + ) + } + ) + ] + ) let general: SectionModel = SectionModel( model: .general, elements: [ @@ -597,48 +642,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) - let groups: SectionModel = SectionModel( - model: .groups, - elements: [ - SessionCell.Info( - id: .groupConfig, - title: "Group Configuration", - subtitle: """ - Configure settings related to Groups. - """, - trailingAccessory: .icon(.chevronRight), - onTap: { [weak self, dependencies] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: DeveloperSettingsGroupsViewModel(using: dependencies) - ) - ) - } - ) - ] - ) - let sessionPro: SectionModel = SectionModel( - model: .sessionPro, - elements: [ - SessionCell.Info( - id: .proConfig, - title: "Session Pro", - subtitle: """ - Configure settings related to Session Pro. - - Session Pro: \(dependencies[feature: .sessionProEnabled] ? "Enabled" : "Disabled") - """, - trailingAccessory: .icon(.chevronRight), - onTap: { [weak self, dependencies] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: DeveloperSettingsProViewModel(using: dependencies) - ) - ) - } - ) - ] - ) let database: SectionModel = SectionModel( model: .database, elements: [ @@ -740,13 +743,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, return [ developerMode, + sessionPro, + groups, general, logging, network, disappearingMessages, communities, - groups, - sessionPro, sessionNetwork, database ] From c1a195a976a36a261a3ee00c593456e54d40deec Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 09:21:49 +1000 Subject: [PATCH 215/244] Fixed an issue where UnsendRequests could be processed after their messages --- .../Errors/MessageReceiverError.swift | 4 +- .../Pollers/SwarmPoller.swift | 7 ++- .../Utilities/ExtensionHelper.swift | 59 ++++++++++++++----- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift index af8255fbce..15d5488927 100644 --- a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift +++ b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift @@ -26,13 +26,14 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case duplicatedCall case missingRequiredAdminPrivileges case deprecatedMessage + case failedToProcess public var isRetryable: Bool { switch self { case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .invalidSignature, .noData, .senderBlocked, .noThread, .selfSend, .decryptionFailed, .invalidConfigMessageHandling, .outdatedMessage, .ignorableMessage, .ignorableMessageRequestMessage, - .missingRequiredAdminPrivileges: + .missingRequiredAdminPrivileges, .failedToProcess: return false default: return true @@ -112,6 +113,7 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case .duplicatedCall: return "Duplicate call." case .missingRequiredAdminPrivileges: return "Handling this message requires admin privileges which the current user does not have." case .deprecatedMessage: return "This message type has been deprecated." + case .failedToProcess: return "Failed to process." } } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index cfb327b06e..ab0851838b 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -386,16 +386,17 @@ public class SwarmPoller: SwarmPollerType & PollerType { } } - /// Make sure to add any synchronously processed messages to the `finalProcessedMessages` - /// as otherwise they wouldn't be emitted by the `receivedPollResponseSubject` + /// Make sure to add any synchronously processed messages to the `finalProcessedMessages` as otherwise + /// they wouldn't be emitted by the `receivedPollResponseSubject`, also need to add the count to + /// `messageCount` to ensure it's not incorrect finalProcessedMessages += processedMessages + messageCount += processedMessages.count return nil } .flatMap { $0 } /// If we don't want to store the messages then no need to continue (don't want to create message receive jobs or mess with cached hashes) guard shouldStoreMessages && !forceSynchronousProcessing else { - messageCount += allProcessedMessages.count finalProcessedMessages += allProcessedMessages return ([], [], (finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) } diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index 99d5314260..0075b91d78 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -865,29 +865,58 @@ public class ExtensionHelper: ExtensionHelperType { } ) - allMessagePaths.forEach { path in - do { - let plaintext: Data = try this.read(from: path) - let message: SnodeReceivedMessage = try JSONDecoder(using: dependencies) - .decode(SnodeReceivedMessage.self, from: plaintext) - - SwarmPoller.processPollResponse( + let sortedMessages: [MessageData] = allMessagePaths + .reduce([Network.SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [Network.SnodeAPI.Namespace: [SnodeReceivedMessage]], path: String) in + do { + let plaintext: Data = try this.read(from: path) + let message: SnodeReceivedMessage = try JSONDecoder(using: dependencies) + .decode(SnodeReceivedMessage.self, from: plaintext) + + return result.appending(message, toArrayOn: message.namespace) + } + catch { + failureStandardCount += 1 + Log.error(.cat, "Discarding standard message due to error: \(error)") + return result + } + } + .map { namespace, messages -> MessageData in + /// We need to sort the messages as we don't know what order they were read from disk in and some + /// messages (eg. a `VisibleMessage` and it's corresponding `UnsendRequest`) need to be + /// processed in a particular order or they won't behave correctly, luckily the `SnodeReceivedMessage.timestampMs` + /// is the "network offset" timestamp when the message was sent to the storage server (rather than the + /// "sent timestamp" on the message, which for an `UnsendRequest` will match it's associate message) + /// so we can just sort by that + ( + namespace, + messages.sorted { $0.timestampMs < $1.timestampMs }, + nil + ) + } + .sorted { lhs, rhs in lhs.namespace.processingOrder < rhs.namespace.processingOrder } + + /// Process the message (inserting into the database if needed (messages are processed per conversaiton so + /// all have the same `swarmPublicKey`) + switch sortedMessages.first?.messages.first?.swarmPublicKey { + case .none: break + case .some(let swarmPublicKey): + let (_, _, result) = SwarmPoller.processPollResponse( db, cat: .cat, source: .pushNotification, - swarmPublicKey: message.swarmPublicKey, + swarmPublicKey: swarmPublicKey, shouldStoreMessages: true, ignoreDedupeFiles: true, forceSynchronousProcessing: true, - sortedMessages: [(message.namespace, [message], nil)], + sortedMessages: sortedMessages, using: dependencies ) - successStandardCount += 1 - } - catch { - failureStandardCount += 1 - Log.error(.cat, "Discarding standard message due to error: \(error)") - } + successStandardCount += result.validMessageCount + + if result.validMessageCount != result.rawMessageCount { + failureStandardCount += (result.rawMessageCount - result.validMessageCount) + Log.error(.cat, "Discarding some standard messages due to error: \(MessageReceiverError.failedToProcess)") + } } /// Remove the standard message files now that they are processed From 81536247d95bec8a2e04d48df7c8dee2e6f1863a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 13:19:21 +1000 Subject: [PATCH 216/244] Fixed merge conflict issues --- .../Message Cells/Content Views/DeletedMessageView.swift | 3 +-- SessionUIKit/Style Guide/ThemeManager.swift | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 0d552b34b4..baed864ba4 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -44,7 +44,6 @@ final class DeletedMessageView: UIView { // Body label let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) - titleLabel.preferredMaxLayoutWidth = maxWidth - 6 // `6` for the `stackView.layoutMargins` titleLabel.font = .italicSystemFont(ofSize: Values.mediumFontSize) titleLabel.text = { switch variant { @@ -78,6 +77,6 @@ final class DeletedMessageView: UIView { stackView.pin(.trailing, to: .trailing, of: self, withInset: -Self.horizontalInset) stackView.pin(.bottom, to: .bottom, of: self, withInset: -Self.verticalInset) - stackView.set(.height, to: calculatedSize.height) + stackView.set(.height, greaterThanOrEqualTo: calculatedSize.height) } } diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 7065d19d0f..e2dff643f2 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -63,7 +63,13 @@ public enum ThemeManager { // If the theme was changed then trigger a UI update and the callback for the theme settings // change (so it gets persisted) - guard themeChanged || matchSystemChanged else { return } + guard themeChanged || matchSystemChanged else { + if !hasSetInitialSystemTrait { + updateAllUI() + } + + return + } if !hasSetInitialSystemTrait || themeChanged { updateAllUI() From d4d518122c5b27e9bf7dcdf873130c613784609d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 13:20:48 +1000 Subject: [PATCH 217/244] Revert "Add other ways to dismiss keyboard on conversation screen" --- .../ConversationVC+Interaction.swift | 19 ++------- Session/Conversations/ConversationVC.swift | 16 +------ .../Conversations/Input View/InputView.swift | 15 ------- .../Message Cells/CallMessageCell.swift | 20 +++++---- .../Message Cells/InfoMessageCell.swift | 22 +++++----- .../Message Cells/MessageCell.swift | 42 +------------------ .../Message Cells/VisibleMessageCell.swift | 32 ++++++++------ 7 files changed, 48 insertions(+), 118 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 8da25fa33e..1330cba707 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -22,8 +22,7 @@ extension ConversationVC: ContextMenuActionDelegate, SendMediaNavDelegate, AttachmentApprovalViewControllerDelegate, - GifPickerViewControllerDelegate, - UIGestureRecognizerDelegate + GifPickerViewControllerDelegate { // MARK: - Open Settings @@ -34,11 +33,6 @@ extension ConversationVC: openSettingsFromTitleView() } - // Handle taps outside of tableview cell to dismiss keyboard - @MainActor @objc func dismissKeyboardOnTap() { - _ = self.snInputView.resignFirstResponder() - } - @MainActor func openSettingsFromTitleView() { // If we shouldn't be able to access settings then disable the title view shortcuts guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } @@ -260,11 +254,6 @@ extension ConversationVC: return true } - - // MARK: - UIGestureRecognizerDelegate - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } // MARK: - SendMediaNavDelegate @@ -1066,7 +1055,7 @@ extension ConversationVC: } // MARK: MessageCellDelegate - + func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the unblock modal if needed guard self.viewModel.threadData.threadIsBlocked != true else { @@ -2261,9 +2250,9 @@ extension ConversationVC: isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - if isShowingSearchUI == true { willManuallyCancelSearchUI() } - _ = snInputView.becomeFirstResponder() + if isShowingSearchUI { willManuallyCancelSearchUI() } + _ = snInputView.becomeFirstResponder() completion?() } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e111edb99b..ab8471b452 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -382,16 +382,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return result }() - - // Handle taps outside of tableview cell - private lazy var tableViewTapGesture: UITapGestureRecognizer = { - let result: UITapGestureRecognizer = UITapGestureRecognizer() - result.delegate = self - result.addTarget(self, action: #selector(dismissKeyboardOnTap)) - result.cancelsTouchesInView = false - - return result - }() // MARK: - Settings @@ -543,9 +533,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa object: nil ) } - - // Gesture - view.addGestureRecognizer(tableViewTapGesture) self.viewModel.navigatableState.setupBindings(viewController: self, disposables: &self.viewModel.disposables) @@ -1580,8 +1567,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // value will break things) let tableViewBottom: CGFloat = (tableView.contentSize.height - tableView.bounds.height + tableView.contentInset.bottom) - // Added `insetDifference > 0` to remove sudden table collapse and overscroll - if tableView.contentOffset.y < (tableViewBottom - 5) && insetDifference > 0 { + if tableView.contentOffset.y < (tableViewBottom - 5) { tableView.contentOffset.y += insetDifference } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index b75531a7e0..74316f9525 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -62,15 +62,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return result }() - private lazy var swipeGestureRecognizer: UISwipeGestureRecognizer = { - let result: UISwipeGestureRecognizer = UISwipeGestureRecognizer() - result.direction = .down - result.addTarget(self, action: #selector(didSwipeDown)) - result.cancelsTouchesInView = false - - return result - }() - private var bottomStackView: UIStackView? private lazy var attachmentsButton: ExpandingAttachmentsButton = { let result = ExpandingAttachmentsButton(delegate: delegate) @@ -236,7 +227,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M autoresizingMask = .flexibleHeight addGestureRecognizer(tapGestureRecognizer) - addGestureRecognizer(swipeGestureRecognizer) // Background & blur let backgroundView = UIView() @@ -464,7 +454,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self.accessibilityIdentifier = updatedInputState.accessibility?.identifier self.accessibilityLabel = updatedInputState.accessibility?.label tapGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes == .none) - inputState = updatedInputState disabledInputLabel.text = (updatedInputState.message ?? "") disabledInputLabel.accessibilityIdentifier = updatedInputState.messageAccessibility?.identifier @@ -641,10 +630,6 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M @objc private func characterLimitLabelTapped() { delegate?.handleCharacterLimitLabelTapped() } - - @objc private func didSwipeDown() { - inputTextView.resignFirstResponder() - } // MARK: - Convenience diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index a74406bcd7..305ea3a29c 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -16,13 +16,6 @@ final class CallMessageCell: MessageCell { override var contextSnapshotView: UIView? { return container } - override var allowedGestureRecognizers: Set { - return [ - .longPress, - .tap - ] - } - // MARK: - UI private lazy var topConstraint: NSLayoutConstraint = mainStackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) @@ -120,6 +113,15 @@ final class CallMessageCell: MessageCell { mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset) } + override func setUpGestureRecognizers() { + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressRecognizer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGestureRecognizer.numberOfTapsRequired = 1 + addGestureRecognizer(tapGestureRecognizer) + } + // MARK: - Updating override func update( @@ -203,7 +205,7 @@ final class CallMessageCell: MessageCell { // MARK: - Interaction - override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -214,7 +216,7 @@ final class CallMessageCell: MessageCell { isHandlingLongPress = true } - override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index a5196a0b88..9c626a90c5 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -13,13 +13,6 @@ final class InfoMessageCell: MessageCell { override var contextSnapshotView: UIView? { return label } - override var allowedGestureRecognizers: Set { - return [ - .longPress, - .tap - ] - } - // MARK: - UI private lazy var iconContainerViewWidthConstraint = iconContainerView.set(.width, to: InfoMessageCell.iconSize) @@ -84,6 +77,15 @@ final class InfoMessageCell: MessageCell { stackView.pin(.right, to: .right, of: self, withInset: -Values.massiveSpacing) stackView.pin(.bottom, to: .bottom, of: self, withInset: -InfoMessageCell.inset) } + + override func setUpGestureRecognizers() { + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressRecognizer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGestureRecognizer.numberOfTapsRequired = 1 + addGestureRecognizer(tapGestureRecognizer) + } // MARK: - Updating @@ -167,7 +169,7 @@ final class InfoMessageCell: MessageCell { // MARK: - Interaction - override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -178,9 +180,9 @@ final class InfoMessageCell: MessageCell { isHandlingLongPress = true } - override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + if cellViewModel.variant == .infoDisappearingMessagesUpdate && cellViewModel.canDoFollowingSetting() { delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index ec40f8e72e..39730a9d6c 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -10,16 +10,11 @@ public enum SwipeState { case cancelled } -public enum GestureRecognizerType { - case tap, longPress, doubleTap -} - public class MessageCell: UITableViewCell { var dependencies: Dependencies? var viewModel: MessageViewModel? weak var delegate: MessageCellDelegate? open var contextSnapshotView: UIView? { return nil } - open var allowedGestureRecognizers: Set { return [] } // Override to have gestures // MARK: - Lifecycle @@ -46,32 +41,7 @@ public class MessageCell: UITableViewCell { } func setUpGestureRecognizers() { - var tapGestureRecognizer: UITapGestureRecognizer? - var doubleTapGestureRecognizer: UITapGestureRecognizer? - - if allowedGestureRecognizers.contains(.tap) { - let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGesture.numberOfTapsRequired = 1 - addGestureRecognizer(tapGesture) - tapGestureRecognizer = tapGesture - } - - if allowedGestureRecognizers.contains(.doubleTap) { - let doubleTapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGesture.numberOfTapsRequired = 2 - addGestureRecognizer(doubleTapGesture) - doubleTapGestureRecognizer = doubleTapGesture - } - - if allowedGestureRecognizers.contains(.longPress) { - let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressGesture) - } - - // If we have both tap and double tap gestures then the single tap should fail if a double tap occurs - if let tapGesture: UITapGestureRecognizer = tapGestureRecognizer, let doubleTapGesture: UITapGestureRecognizer = doubleTapGestureRecognizer { - tapGesture.require(toFail: doubleTapGesture) - } + // To be overridden by subclasses } // MARK: - Updating @@ -123,16 +93,6 @@ public class MessageCell: UITableViewCell { return CallMessageCell.self } } - - // MARK: - Gesture events - @objc - func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {} - - @objc - func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {} - - @objc - func handleDoubleTap() {} } // MARK: - MessageCellDelegate diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0dbde5e460..21a9517955 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -25,14 +25,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { override var contextSnapshotView: UIView? { return snContentView } - override var allowedGestureRecognizers: Set { - return [ - .tap, - .longPress, - .doubleTap - ] - } - // Constraints internal lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) @@ -278,7 +270,21 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) } - + + override func setUpGestureRecognizers() { + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressRecognizer) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGestureRecognizer.numberOfTapsRequired = 1 + addGestureRecognizer(tapGestureRecognizer) + + let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGestureRecognizer.numberOfTapsRequired = 2 + addGestureRecognizer(doubleTapGestureRecognizer) + tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) + } + // MARK: - Updating override func update( @@ -962,7 +968,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -988,9 +994,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { isHandlingLongPress = true } - override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + let location = gestureRecognizer.location(in: self) if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile { @@ -1056,7 +1062,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - override func handleDoubleTap() { + @objc private func handleDoubleTap() { guard let cellViewModel: MessageViewModel = self.viewModel else { return } delegate?.handleItemDoubleTapped(cellViewModel) From 3fe262ac5846bb4e6714b535580952e4e397de17 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 13:22:19 +1000 Subject: [PATCH 218/244] Revert "Added skip authentication flags for some group api calls" --- .../Jobs/DisplayPictureDownloadJob.swift | 18 ++-- .../RetrieveDefaultOpenGroupRoomsJob.swift | 4 +- .../Open Groups/OpenGroupAPI.swift | 34 +++---- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 9 +- .../Open Groups/OpenGroupAPISpec.swift | 97 ------------------- 5 files changed, 23 insertions(+), 139 deletions(-) diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index b6323ce744..ec9ee87bc3 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -48,7 +48,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { using: dependencies ) - case .community(let fileId, let roomToken, let server, let skipAuthentication): + case .community(let fileId, let roomToken, let server): guard let info: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) @@ -58,7 +58,6 @@ public enum DisplayPictureDownloadJob: JobExecutor { fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), - skipAuthentication: skipAuthentication, using: dependencies ) } @@ -207,7 +206,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { ) db.addConversationEvent(id: id, type: .updated(.displayPictureUrl(url))) - case .community(_, let roomToken, let server, _): + case .community(_, let roomToken, let server): _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: server)) .updateAllAndConfig( @@ -229,7 +228,7 @@ extension DisplayPictureDownloadJob { public enum Target: Codable, Hashable, CustomStringConvertible { case profile(id: String, url: String, encryptionKey: Data) case group(id: String, url: String, encryptionKey: Data) - case community(imageId: String, roomToken: String, server: String, skipAuthentication: Bool = false) + case community(imageId: String, roomToken: String, server: String) var isValid: Bool { switch self { @@ -240,7 +239,7 @@ extension DisplayPictureDownloadJob { encryptionKey.count == DisplayPictureManager.aes256KeyByteLength ) - case .community(let imageId, _, _, _): return !imageId.isEmpty + case .community(let imageId, _, _): return !imageId.isEmpty } } @@ -250,7 +249,7 @@ extension DisplayPictureDownloadJob { switch self { case .profile(let id, _, _): return "profile: \(id)" case .group(let id, _, _): return "group: \(id)" - case .community(_, let roomToken, let server, _): return "room: \(roomToken) on server: \(server)" + case .community(_, let roomToken, let server): return "room: \(roomToken) on server: \(server)" } } } @@ -275,12 +274,11 @@ extension DisplayPictureDownloadJob { self.target = { switch target { - case .community(let imageId, let roomToken, let server, let skipAuthentication): + case .community(let imageId, let roomToken, let server): return .community( imageId: imageId, roomToken: roomToken, - server: server.lowercased(), // Always in lowercase on `OpenGroup` - skipAuthentication: skipAuthentication + server: server.lowercased() // Always in lowercase on `OpenGroup` ) default: return target @@ -360,7 +358,7 @@ extension DisplayPictureDownloadJob { return (url == latestDisplayPictureUrl) - case .community(let imageId, let roomToken, let server, _): + case .community(let imageId, let roomToken, let server): guard let latestImageId: String = try? OpenGroup .select(.imageId) diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index 66252af6f5..a864b6c2fe 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -72,7 +72,6 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in try OpenGroupAPI.preparedCapabilitiesAndRooms( authMethod: authMethod, - skipAuthentication: true, using: dependencies ).send(using: dependencies) } @@ -158,8 +157,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { target: .community( imageId: imageId, roomToken: room.token, - server: OpenGroupAPI.defaultServer, - skipAuthentication: true + server: OpenGroupAPI.defaultServer ), timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index ff04d26785..3d0bc3ccb2 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -165,10 +165,9 @@ public enum OpenGroupAPI { private static func preparedSequence( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, - skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest> { - let preparedRequest = try Network.PreparedRequest( + return try Network.PreparedRequest( request: Request( method: .post, endpoint: Endpoint.sequence, @@ -179,7 +178,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Capabilities @@ -193,10 +192,9 @@ public enum OpenGroupAPI { /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` public static func preparedCapabilities( authMethod: AuthenticationMethod, - skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - let preparedRequest = try Network.PreparedRequest( + return try Network.PreparedRequest( request: Request( endpoint: .capabilities, authMethod: authMethod @@ -205,7 +203,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Room @@ -215,10 +213,9 @@ public enum OpenGroupAPI { /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included public static func preparedRooms( authMethod: AuthenticationMethod, - skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[Room]> { - let preparedRequest = try Network.PreparedRequest( + return try Network.PreparedRequest( request: Request( endpoint: .rooms, authMethod: authMethod @@ -227,7 +224,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: OpenGroupAPI.signRequest, using: dependencies) } /// Returns the details of a single room @@ -329,25 +326,20 @@ public enum OpenGroupAPI { /// methods for the documented behaviour of each method public static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, - skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - let preparedRequest = try OpenGroupAPI + return try OpenGroupAPI .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the // cached ones are stale) - preparedCapabilities(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies), - preparedRooms(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies) + preparedCapabilities(authMethod: authMethod, using: dependencies), + preparedRooms(authMethod: authMethod, using: dependencies) ], authMethod: authMethod, - skipAuthentication: skipAuthentication, using: dependencies ) - - let finalRequest = skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) - - return finalRequest + .signed(with: OpenGroupAPI.signRequest, using: dependencies) .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data @@ -850,10 +842,9 @@ public enum OpenGroupAPI { fileId: String, roomToken: String, authMethod: AuthenticationMethod, - skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - let preparedRequest = try Network.PreparedRequest( + return try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), authMethod: authMethod @@ -863,8 +854,7 @@ public enum OpenGroupAPI { requestTimeout: Network.fileDownloadTimeout, using: dependencies ) - - return skipAuthentication ? preparedRequest : try preparedRequest.signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: OpenGroupAPI.signRequest, using: dependencies) } // MARK: - Inbox/Outbox (Message Requests) diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 7a046e4266..39d2bff89d 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -263,7 +263,6 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ), forceBlinded: false ), - skipAuthentication: true, using: dependencies ) } @@ -285,8 +284,6 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) - - expect(expectedRequest?.headers).to(beEmpty()) } // MARK: -- will retry 8 times before it fails @@ -441,8 +438,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer, - skipAuthentication: true + server: OpenGroupAPI.defaultServer ), timestamp: 1234567890 ) @@ -489,8 +485,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer, - skipAuthentication: true + server: OpenGroupAPI.defaultServer ), timestamp: 1234567890 ) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index a4ffc9f523..fa13eeea02 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -770,44 +770,6 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) - - expect(preparedRequest?.headers).toNot(beEmpty()) - expect(preparedRequest?.headers).to(equal([ - HTTPHeader.sogsNonce: "pK6YRtQApl4NhECGizF0Cg==", - HTTPHeader.sogsTimestamp: "1234567890", - HTTPHeader.sogsSignature: "VGVzdFNvZ3NTaWduYXR1cmU=", - HTTPHeader.sogsPubKey: "1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" - ])) - } - - // MARK: ---- generates the request correctly and skips adding request headers - it("generates the request correctly and skips adding request headers") { - expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( - authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), - forceBlinded: false - ), - skipAuthentication: true, - using: dependencies - ) - }.toNot(throwError()) - - expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) - .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) - .to(equal(.rooms)) - - expect(preparedRequest?.path).to(equal("/sequence")) - expect(preparedRequest?.method.rawValue).to(equal("POST")) - - expect(preparedRequest?.headers).to(beEmpty()) } // MARK: ---- processes a valid response correctly @@ -1664,31 +1626,6 @@ class OpenGroupAPISpec: QuickSpec { ])) } - // MARK: ---- generates the download destination correctly when given an id and skips adding request headers - it("generates the download destination correctly when given an id and skips adding request headers") { - expect { - preparedRequest = try OpenGroupAPI.preparedDownload( - fileId: "1", - roomToken: "roomToken", - authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), - forceBlinded: false - ), - skipAuthentication: true, - using: dependencies - ) - }.toNot(throwError()) - - expect(preparedRequest?.path).to(equal("/room/roomToken/file/1")) - expect(preparedRequest?.method.rawValue).to(equal("GET")) - expect(preparedRequest?.headers).to(beEmpty()) - } - // MARK: ---- generates the download request correctly when given a URL it("generates the download request correctly when given a URL") { expect { @@ -2337,40 +2274,6 @@ class OpenGroupAPISpec: QuickSpec { .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - - expect(preparedRequest?.headers).toNot(beEmpty()) - - expect(response).toNot(beNil()) - expect(error).to(beNil()) - } - - // MARK: ---- triggers sending correctly without headers - it("triggers sending correctly without headers") { - var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? - - expect { - preparedRequest = try OpenGroupAPI.preparedRooms( - authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), - forceBlinded: false - ), - skipAuthentication: true, - using: dependencies - ) - }.toNot(throwError()) - - preparedRequest? - .send(using: dependencies) - .handleEvents(receiveOutput: { result in response = result }) - .mapError { error.setting(to: $0) } - .sinkAndStore(in: &disposables) - - expect(preparedRequest?.headers).to(beEmpty()) expect(response).toNot(beNil()) expect(error).to(beNil()) From a42a6ca5195cc4378015a3cc3733865ae9d46df4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 13:22:57 +1000 Subject: [PATCH 219/244] Revert "Fix emoji category title not displayed properly" --- Scripts/EmojiGenerator.swift | 11 +- .../EmojiPickerCollectionView.swift | 7 +- Session/Emoji/Emoji+Category.swift | 85 +----- Session/Emoji/Emoji+Name.swift | 37 +-- Session/Emoji/Emoji+SkinTones.swift | 149 +--------- Session/Emoji/Emoji.swift | 35 +-- Session/Emoji/EmojiWithSkinTones+String.swift | 264 +----------------- 7 files changed, 32 insertions(+), 556 deletions(-) diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 1c2102c81f..519ce65220 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -53,7 +53,7 @@ enum RemoteModel { case flags = "Flags" case components = "Component" - var localizedKey: String { + var localizedKey: String = { switch self { case .smileys: return "Smileys" @@ -77,8 +77,8 @@ enum RemoteModel { return "Flags" case .components: return "Component" - } - } + } + }() } static func fetchEmojiData() throws -> Data { @@ -569,9 +569,8 @@ extension EmojiGenerator { fileHandle.indent { let stringKey = "emojiCategory\(category.localizedKey)" let stringComment = "The name for the emoji category '\(category.rawValue)'" - - fileHandle.writeLine("// \(stringComment)") - fileHandle.writeLine("return \"\(stringKey)\".localized()") + + fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")") } } fileHandle.writeLine("}") diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 289a6bb8e2..b366b5e2a8 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -322,12 +322,7 @@ private class EmojiSectionHeader: UICollectionReusableView { label.font = .systemFont(ofSize: Values.smallFontSize) label.themeTextColor = .textPrimary addSubview(label) - - label.pin(.top, to: .top, of: self) - label.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) - label.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) - label.pin(.bottom, to: .bottom, of: self) - + label.pin(to: self) label.setCompressionResistance(to: .required) } diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 996f7d83a4..343d9a0e87 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -20,29 +20,21 @@ extension Emoji { var localizedName: String { switch self { case .smileysAndPeople: - // The name for the emoji category 'Smileys & People' - return "emojiCategorySmileys".localized() + return NSLocalizedString("emojiCategorySmileys", comment: "The name for the emoji category 'Smileys & People'") case .animals: - // The name for the emoji category 'Animals & Nature' - return "emojiCategoryAnimals".localized() + return NSLocalizedString("emojiCategoryAnimals", comment: "The name for the emoji category 'Animals & Nature'") case .food: - // The name for the emoji category 'Food & Drink' - return "emojiCategoryFood".localized() + return NSLocalizedString("emojiCategoryFood", comment: "The name for the emoji category 'Food & Drink'") case .activities: - // The name for the emoji category 'Activities' - return "emojiCategoryActivities".localized() + return NSLocalizedString("emojiCategoryActivities", comment: "The name for the emoji category 'Activities'") case .travel: - // The name for the emoji category 'Travel & Places' - return "emojiCategoryTravel".localized() + return NSLocalizedString("emojiCategoryTravel", comment: "The name for the emoji category 'Travel & Places'") case .objects: - // The name for the emoji category 'Objects' - return "emojiCategoryObjects".localized() + return NSLocalizedString("emojiCategoryObjects", comment: "The name for the emoji category 'Objects'") case .symbols: - // The name for the emoji category 'Symbols' - return "emojiCategorySymbols".localized() + return NSLocalizedString("emojiCategorySymbols", comment: "The name for the emoji category 'Symbols'") case .flags: - // The name for the emoji category 'Flags' - return "emojiCategoryFlags".localized() + return NSLocalizedString("emojiCategoryFlags", comment: "The name for the emoji category 'Flags'") } } @@ -100,8 +92,6 @@ extension Emoji { .faceExhaling, .lyingFace, .shakingFace, - .headShakingHorizontally, - .headShakingVertically, .relieved, .pensive, .sleepy, @@ -460,42 +450,24 @@ extension Emoji { .walking, .manWalking, .womanWalking, - .personWalkingFacingRight, - .womanWalkingFacingRight, - .manWalkingFacingRight, .standingPerson, .manStanding, .womanStanding, .kneelingPerson, .manKneeling, .womanKneeling, - .personKneelingFacingRight, - .womanKneelingFacingRight, - .manKneelingFacingRight, .personWithProbingCane, - .personWithWhiteCaneFacingRight, .manWithProbingCane, - .manWithWhiteCaneFacingRight, .womanWithProbingCane, - .womanWithWhiteCaneFacingRight, .personInMotorizedWheelchair, - .personInMotorizedWheelchairFacingRight, .manInMotorizedWheelchair, - .manInMotorizedWheelchairFacingRight, .womanInMotorizedWheelchair, - .womanInMotorizedWheelchairFacingRight, .personInManualWheelchair, - .personInManualWheelchairFacingRight, .manInManualWheelchair, - .manInManualWheelchairFacingRight, .womanInManualWheelchair, - .womanInManualWheelchairFacingRight, .runner, .manRunning, .womanRunning, - .personRunningFacingRight, - .womanRunningFacingRight, - .manRunningFacingRight, .dancer, .manDancing, .manInBusinessSuitLevitating, @@ -568,6 +540,7 @@ extension Emoji { .womanHeartMan, .manHeartMan, .womanHeartWoman, + .family, .manWomanBoy, .manWomanGirl, .manWomanGirlBoy, @@ -597,11 +570,6 @@ extension Emoji { .bustInSilhouette, .bustsInSilhouette, .peopleHugging, - .family, - .familyAdultAdultChild, - .familyAdultAdultChildChild, - .familyAdultChild, - .familyAdultChildChild, .footprints, ] case .animals: @@ -693,7 +661,6 @@ extension Emoji { .wing, .blackBird, .goose, - .phoenix, .frog, .crocodile, .turtle, @@ -767,7 +734,6 @@ extension Emoji { .watermelon, .tangerine, .lemon, - .lime, .banana, .pineapple, .mango, @@ -799,7 +765,6 @@ extension Emoji { .chestnut, .gingerRoot, .peaPod, - .brownMushroom, .bread, .croissant, .baguetteBread, @@ -1417,7 +1382,6 @@ extension Emoji { .scales, .probingCane, .link, - .brokenChain, .chains, .hook, .toolbox, @@ -2025,8 +1989,6 @@ extension Emoji { case .faceExhaling: return .smileysAndPeople case .lyingFace: return .smileysAndPeople case .shakingFace: return .smileysAndPeople - case .headShakingHorizontally: return .smileysAndPeople - case .headShakingVertically: return .smileysAndPeople case .relieved: return .smileysAndPeople case .pensive: return .smileysAndPeople case .sleepy: return .smileysAndPeople @@ -2385,42 +2347,24 @@ extension Emoji { case .walking: return .smileysAndPeople case .manWalking: return .smileysAndPeople case .womanWalking: return .smileysAndPeople - case .personWalkingFacingRight: return .smileysAndPeople - case .womanWalkingFacingRight: return .smileysAndPeople - case .manWalkingFacingRight: return .smileysAndPeople case .standingPerson: return .smileysAndPeople case .manStanding: return .smileysAndPeople case .womanStanding: return .smileysAndPeople case .kneelingPerson: return .smileysAndPeople case .manKneeling: return .smileysAndPeople case .womanKneeling: return .smileysAndPeople - case .personKneelingFacingRight: return .smileysAndPeople - case .womanKneelingFacingRight: return .smileysAndPeople - case .manKneelingFacingRight: return .smileysAndPeople case .personWithProbingCane: return .smileysAndPeople - case .personWithWhiteCaneFacingRight: return .smileysAndPeople case .manWithProbingCane: return .smileysAndPeople - case .manWithWhiteCaneFacingRight: return .smileysAndPeople case .womanWithProbingCane: return .smileysAndPeople - case .womanWithWhiteCaneFacingRight: return .smileysAndPeople case .personInMotorizedWheelchair: return .smileysAndPeople - case .personInMotorizedWheelchairFacingRight: return .smileysAndPeople case .manInMotorizedWheelchair: return .smileysAndPeople - case .manInMotorizedWheelchairFacingRight: return .smileysAndPeople case .womanInMotorizedWheelchair: return .smileysAndPeople - case .womanInMotorizedWheelchairFacingRight: return .smileysAndPeople case .personInManualWheelchair: return .smileysAndPeople - case .personInManualWheelchairFacingRight: return .smileysAndPeople case .manInManualWheelchair: return .smileysAndPeople - case .manInManualWheelchairFacingRight: return .smileysAndPeople case .womanInManualWheelchair: return .smileysAndPeople - case .womanInManualWheelchairFacingRight: return .smileysAndPeople case .runner: return .smileysAndPeople case .manRunning: return .smileysAndPeople case .womanRunning: return .smileysAndPeople - case .personRunningFacingRight: return .smileysAndPeople - case .womanRunningFacingRight: return .smileysAndPeople - case .manRunningFacingRight: return .smileysAndPeople case .dancer: return .smileysAndPeople case .manDancing: return .smileysAndPeople case .manInBusinessSuitLevitating: return .smileysAndPeople @@ -2493,6 +2437,7 @@ extension Emoji { case .womanHeartMan: return .smileysAndPeople case .manHeartMan: return .smileysAndPeople case .womanHeartWoman: return .smileysAndPeople + case .family: return .smileysAndPeople case .manWomanBoy: return .smileysAndPeople case .manWomanGirl: return .smileysAndPeople case .manWomanGirlBoy: return .smileysAndPeople @@ -2522,11 +2467,6 @@ extension Emoji { case .bustInSilhouette: return .smileysAndPeople case .bustsInSilhouette: return .smileysAndPeople case .peopleHugging: return .smileysAndPeople - case .family: return .smileysAndPeople - case .familyAdultAdultChild: return .smileysAndPeople - case .familyAdultAdultChildChild: return .smileysAndPeople - case .familyAdultChild: return .smileysAndPeople - case .familyAdultChildChild: return .smileysAndPeople case .footprints: return .smileysAndPeople case .monkeyFace: return .animals case .monkey: return .animals @@ -2615,7 +2555,6 @@ extension Emoji { case .wing: return .animals case .blackBird: return .animals case .goose: return .animals - case .phoenix: return .animals case .frog: return .animals case .crocodile: return .animals case .turtle: return .animals @@ -2686,7 +2625,6 @@ extension Emoji { case .watermelon: return .food case .tangerine: return .food case .lemon: return .food - case .lime: return .food case .banana: return .food case .pineapple: return .food case .mango: return .food @@ -2718,7 +2656,6 @@ extension Emoji { case .chestnut: return .food case .gingerRoot: return .food case .peaPod: return .food - case .brownMushroom: return .food case .bread: return .food case .croissant: return .food case .baguetteBread: return .food @@ -3327,7 +3264,6 @@ extension Emoji { case .scales: return .objects case .probingCane: return .objects case .link: return .objects - case .brokenChain: return .objects case .chains: return .objects case .hook: return .objects case .toolbox: return .objects @@ -3885,3 +3821,4 @@ extension Emoji { } } } +// swiftlint:disable all diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift index 2b6abe71f9..2ddb050adb 100644 --- a/Session/Emoji/Emoji+Name.swift +++ b/Session/Emoji/Emoji+Name.swift @@ -1,11 +1,9 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. -// + // swiftlint:disable all // stringlint:disable -import Foundation - extension Emoji { var name: String { switch self { @@ -59,8 +57,6 @@ extension Emoji { case .faceExhaling: return "face exhaling, face_exhaling, faceexhaling" case .lyingFace: return "lying face, lying_face, lyingface" case .shakingFace: return "shaking face, shaking_face, shakingface" - case .headShakingHorizontally: return "head shaking horizontally, head_shaking_horizontally, headshakinghorizontally" - case .headShakingVertically: return "head shaking vertically, head_shaking_vertically, headshakingvertically" case .relieved: return "relieved, relieved face" case .pensive: return "pensive, pensive face" case .sleepy: return "sleepy, sleepy face" @@ -419,42 +415,24 @@ extension Emoji { case .walking: return "pedestrian, walking" case .manWalking: return "man walking, man-walking, manwalking" case .womanWalking: return "woman walking, woman-walking, womanwalking" - case .personWalkingFacingRight: return "person walking facing right, person_walking_facing_right, personwalkingfacingright" - case .womanWalkingFacingRight: return "woman walking facing right, woman_walking_facing_right, womanwalkingfacingright" - case .manWalkingFacingRight: return "man walking facing right, man_walking_facing_right, manwalkingfacingright" case .standingPerson: return "standing person, standing_person, standingperson" case .manStanding: return "man standing, man_standing, manstanding" case .womanStanding: return "woman standing, woman_standing, womanstanding" case .kneelingPerson: return "kneeling person, kneeling_person, kneelingperson" case .manKneeling: return "man kneeling, man_kneeling, mankneeling" case .womanKneeling: return "woman kneeling, woman_kneeling, womankneeling" - case .personKneelingFacingRight: return "person kneeling facing right, person_kneeling_facing_right, personkneelingfacingright" - case .womanKneelingFacingRight: return "woman kneeling facing right, woman_kneeling_facing_right, womankneelingfacingright" - case .manKneelingFacingRight: return "man kneeling facing right, man_kneeling_facing_right, mankneelingfacingright" case .personWithProbingCane: return "person with white cane, person_with_probing_cane, personwithprobingcane" - case .personWithWhiteCaneFacingRight: return "person with white cane facing right, person_with_white_cane_facing_right, personwithwhitecanefacingright" case .manWithProbingCane: return "man with white cane, man_with_probing_cane, manwithprobingcane" - case .manWithWhiteCaneFacingRight: return "man with white cane facing right, man_with_white_cane_facing_right, manwithwhitecanefacingright" case .womanWithProbingCane: return "woman with white cane, woman_with_probing_cane, womanwithprobingcane" - case .womanWithWhiteCaneFacingRight: return "woman with white cane facing right, woman_with_white_cane_facing_right, womanwithwhitecanefacingright" case .personInMotorizedWheelchair: return "person in motorized wheelchair, person_in_motorized_wheelchair, personinmotorizedwheelchair" - case .personInMotorizedWheelchairFacingRight: return "person in motorized wheelchair facing right, person_in_motorized_wheelchair_facing_right, personinmotorizedwheelchairfacingright" case .manInMotorizedWheelchair: return "man in motorized wheelchair, man_in_motorized_wheelchair, maninmotorizedwheelchair" - case .manInMotorizedWheelchairFacingRight: return "man in motorized wheelchair facing right, man_in_motorized_wheelchair_facing_right, maninmotorizedwheelchairfacingright" case .womanInMotorizedWheelchair: return "woman in motorized wheelchair, woman_in_motorized_wheelchair, womaninmotorizedwheelchair" - case .womanInMotorizedWheelchairFacingRight: return "woman in motorized wheelchair facing right, woman_in_motorized_wheelchair_facing_right, womaninmotorizedwheelchairfacingright" case .personInManualWheelchair: return "person in manual wheelchair, person_in_manual_wheelchair, personinmanualwheelchair" - case .personInManualWheelchairFacingRight: return "person in manual wheelchair facing right, person_in_manual_wheelchair_facing_right, personinmanualwheelchairfacingright" case .manInManualWheelchair: return "man in manual wheelchair, man_in_manual_wheelchair, maninmanualwheelchair" - case .manInManualWheelchairFacingRight: return "man in manual wheelchair facing right, man_in_manual_wheelchair_facing_right, maninmanualwheelchairfacingright" case .womanInManualWheelchair: return "woman in manual wheelchair, woman_in_manual_wheelchair, womaninmanualwheelchair" - case .womanInManualWheelchairFacingRight: return "woman in manual wheelchair facing right, woman_in_manual_wheelchair_facing_right, womaninmanualwheelchairfacingright" case .runner: return "runner, running" case .manRunning: return "man running, man-running, manrunning" case .womanRunning: return "woman running, woman-running, womanrunning" - case .personRunningFacingRight: return "person running facing right, person_running_facing_right, personrunningfacingright" - case .womanRunningFacingRight: return "woman running facing right, woman_running_facing_right, womanrunningfacingright" - case .manRunningFacingRight: return "man running facing right, man_running_facing_right, manrunningfacingright" case .dancer: return "dancer" case .manDancing: return "man dancing, man_dancing, mandancing" case .manInBusinessSuitLevitating: return "man_in_business_suit_levitating, maninbusinesssuitlevitating, person in suit levitating" @@ -527,6 +505,7 @@ extension Emoji { case .womanHeartMan: return "couple with heart: woman, man, woman-heart-man, womanheartman" case .manHeartMan: return "couple with heart: man, man, man-heart-man, manheartman" case .womanHeartWoman: return "couple with heart: woman, woman, woman-heart-woman, womanheartwoman" + case .family: return "family" case .manWomanBoy: return "family: man, woman, boy, man-woman-boy, manwomanboy" case .manWomanGirl: return "family: man, woman, girl, man-woman-girl, manwomangirl" case .manWomanGirlBoy: return "family: man, woman, girl, boy, man-woman-girl-boy, manwomangirlboy" @@ -556,11 +535,6 @@ extension Emoji { case .bustInSilhouette: return "bust in silhouette, bust_in_silhouette, bustinsilhouette" case .bustsInSilhouette: return "busts in silhouette, busts_in_silhouette, bustsinsilhouette" case .peopleHugging: return "people hugging, people_hugging, peoplehugging" - case .family: return "family" - case .familyAdultAdultChild: return "family: adult, adult, child, family_adult_adult_child, familyadultadultchild" - case .familyAdultAdultChildChild: return "family: adult, adult, child, child, family_adult_adult_child_child, familyadultadultchildchild" - case .familyAdultChild: return "family: adult, child, family_adult_child, familyadultchild" - case .familyAdultChildChild: return "family: adult, child, child, family_adult_child_child, familyadultchildchild" case .footprints: return "footprints" case .skinTone2: return "emoji modifier fitzpatrick type-1-2, skin-tone-2, skintone2" case .skinTone3: return "emoji modifier fitzpatrick type-3, skin-tone-3, skintone3" @@ -654,7 +628,6 @@ extension Emoji { case .wing: return "wing" case .blackBird: return "black bird, black_bird, blackbird" case .goose: return "goose" - case .phoenix: return "phoenix" case .frog: return "frog, frog face" case .crocodile: return "crocodile" case .turtle: return "turtle" @@ -725,7 +698,6 @@ extension Emoji { case .watermelon: return "watermelon" case .tangerine: return "tangerine" case .lemon: return "lemon" - case .lime: return "lime" case .banana: return "banana" case .pineapple: return "pineapple" case .mango: return "mango" @@ -757,7 +729,6 @@ extension Emoji { case .chestnut: return "chestnut" case .gingerRoot: return "ginger root, ginger_root, gingerroot" case .peaPod: return "pea pod, pea_pod, peapod" - case .brownMushroom: return "brown mushroom, brown_mushroom, brownmushroom" case .bread: return "bread" case .croissant: return "croissant" case .baguetteBread: return "baguette bread, baguette_bread, baguettebread" @@ -1366,7 +1337,6 @@ extension Emoji { case .scales: return "balance scale, scales" case .probingCane: return "probing cane, probing_cane, probingcane" case .link: return "link, link symbol" - case .brokenChain: return "broken chain, broken_chain, brokenchain" case .chains: return "chains" case .hook: return "hook" case .toolbox: return "toolbox" @@ -1882,7 +1852,7 @@ extension Emoji { case .flagTm: return "flag-tm, flagtm, turkmenistan flag" case .flagTn: return "flag-tn, flagtn, tunisia flag" case .flagTo: return "flag-to, flagto, tonga flag" - case .flagTr: return "flag-tr, flagtr, türkiye flag" + case .flagTr: return "flag-tr, flagtr, turkey flag" case .flagTt: return "flag-tt, flagtt, trinidad & tobago flag" case .flagTv: return "flag-tv, flagtv, tuvalu flag" case .flagTw: return "flag-tw, flagtw, taiwan flag" @@ -1915,3 +1885,4 @@ extension Emoji { } } } +// swiftlint:disable all diff --git a/Session/Emoji/Emoji+SkinTones.swift b/Session/Emoji/Emoji+SkinTones.swift index 9f34de6151..f1fb18434f 100644 --- a/Session/Emoji/Emoji+SkinTones.swift +++ b/Session/Emoji/Emoji+SkinTones.swift @@ -1,11 +1,9 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. -// + // swiftlint:disable all // stringlint:disable -import Foundation - extension Emoji { enum SkinTone: String, CaseIterable, Equatable { case light = "🏻" @@ -1843,30 +1841,6 @@ extension Emoji { [.mediumDark]: "🚶🏾‍♀️", [.dark]: "🚶🏿‍♀️", ] - case .personWalkingFacingRight: - return [ - [.light]: "🚶🏻‍➡️", - [.mediumLight]: "🚶🏼‍➡️", - [.medium]: "🚶🏽‍➡️", - [.mediumDark]: "🚶🏾‍➡️", - [.dark]: "🚶🏿‍➡️", - ] - case .womanWalkingFacingRight: - return [ - [.light]: "🚶🏻‍♀️‍➡️", - [.mediumLight]: "🚶🏼‍♀️‍➡️", - [.medium]: "🚶🏽‍♀️‍➡️", - [.mediumDark]: "🚶🏾‍♀️‍➡️", - [.dark]: "🚶🏿‍♀️‍➡️", - ] - case .manWalkingFacingRight: - return [ - [.light]: "🚶🏻‍♂️‍➡️", - [.mediumLight]: "🚶🏼‍♂️‍➡️", - [.medium]: "🚶🏽‍♂️‍➡️", - [.mediumDark]: "🚶🏾‍♂️‍➡️", - [.dark]: "🚶🏿‍♂️‍➡️", - ] case .standingPerson: return [ [.light]: "🧍🏻", @@ -1915,30 +1889,6 @@ extension Emoji { [.mediumDark]: "🧎🏾‍♀️", [.dark]: "🧎🏿‍♀️", ] - case .personKneelingFacingRight: - return [ - [.light]: "🧎🏻‍➡️", - [.mediumLight]: "🧎🏼‍➡️", - [.medium]: "🧎🏽‍➡️", - [.mediumDark]: "🧎🏾‍➡️", - [.dark]: "🧎🏿‍➡️", - ] - case .womanKneelingFacingRight: - return [ - [.light]: "🧎🏻‍♀️‍➡️", - [.mediumLight]: "🧎🏼‍♀️‍➡️", - [.medium]: "🧎🏽‍♀️‍➡️", - [.mediumDark]: "🧎🏾‍♀️‍➡️", - [.dark]: "🧎🏿‍♀️‍➡️", - ] - case .manKneelingFacingRight: - return [ - [.light]: "🧎🏻‍♂️‍➡️", - [.mediumLight]: "🧎🏼‍♂️‍➡️", - [.medium]: "🧎🏽‍♂️‍➡️", - [.mediumDark]: "🧎🏾‍♂️‍➡️", - [.dark]: "🧎🏿‍♂️‍➡️", - ] case .personWithProbingCane: return [ [.light]: "🧑🏻‍🦯", @@ -1947,14 +1897,6 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦯", [.dark]: "🧑🏿‍🦯", ] - case .personWithWhiteCaneFacingRight: - return [ - [.light]: "🧑🏻‍🦯‍➡️", - [.mediumLight]: "🧑🏼‍🦯‍➡️", - [.medium]: "🧑🏽‍🦯‍➡️", - [.mediumDark]: "🧑🏾‍🦯‍➡️", - [.dark]: "🧑🏿‍🦯‍➡️", - ] case .manWithProbingCane: return [ [.light]: "👨🏻‍🦯", @@ -1963,14 +1905,6 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦯", [.dark]: "👨🏿‍🦯", ] - case .manWithWhiteCaneFacingRight: - return [ - [.light]: "👨🏻‍🦯‍➡️", - [.mediumLight]: "👨🏼‍🦯‍➡️", - [.medium]: "👨🏽‍🦯‍➡️", - [.mediumDark]: "👨🏾‍🦯‍➡️", - [.dark]: "👨🏿‍🦯‍➡️", - ] case .womanWithProbingCane: return [ [.light]: "👩🏻‍🦯", @@ -1979,14 +1913,6 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦯", [.dark]: "👩🏿‍🦯", ] - case .womanWithWhiteCaneFacingRight: - return [ - [.light]: "👩🏻‍🦯‍➡️", - [.mediumLight]: "👩🏼‍🦯‍➡️", - [.medium]: "👩🏽‍🦯‍➡️", - [.mediumDark]: "👩🏾‍🦯‍➡️", - [.dark]: "👩🏿‍🦯‍➡️", - ] case .personInMotorizedWheelchair: return [ [.light]: "🧑🏻‍🦼", @@ -1995,14 +1921,6 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦼", [.dark]: "🧑🏿‍🦼", ] - case .personInMotorizedWheelchairFacingRight: - return [ - [.light]: "🧑🏻‍🦼‍➡️", - [.mediumLight]: "🧑🏼‍🦼‍➡️", - [.medium]: "🧑🏽‍🦼‍➡️", - [.mediumDark]: "🧑🏾‍🦼‍➡️", - [.dark]: "🧑🏿‍🦼‍➡️", - ] case .manInMotorizedWheelchair: return [ [.light]: "👨🏻‍🦼", @@ -2011,14 +1929,6 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦼", [.dark]: "👨🏿‍🦼", ] - case .manInMotorizedWheelchairFacingRight: - return [ - [.light]: "👨🏻‍🦼‍➡️", - [.mediumLight]: "👨🏼‍🦼‍➡️", - [.medium]: "👨🏽‍🦼‍➡️", - [.mediumDark]: "👨🏾‍🦼‍➡️", - [.dark]: "👨🏿‍🦼‍➡️", - ] case .womanInMotorizedWheelchair: return [ [.light]: "👩🏻‍🦼", @@ -2027,14 +1937,6 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦼", [.dark]: "👩🏿‍🦼", ] - case .womanInMotorizedWheelchairFacingRight: - return [ - [.light]: "👩🏻‍🦼‍➡️", - [.mediumLight]: "👩🏼‍🦼‍➡️", - [.medium]: "👩🏽‍🦼‍➡️", - [.mediumDark]: "👩🏾‍🦼‍➡️", - [.dark]: "👩🏿‍🦼‍➡️", - ] case .personInManualWheelchair: return [ [.light]: "🧑🏻‍🦽", @@ -2043,14 +1945,6 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦽", [.dark]: "🧑🏿‍🦽", ] - case .personInManualWheelchairFacingRight: - return [ - [.light]: "🧑🏻‍🦽‍➡️", - [.mediumLight]: "🧑🏼‍🦽‍➡️", - [.medium]: "🧑🏽‍🦽‍➡️", - [.mediumDark]: "🧑🏾‍🦽‍➡️", - [.dark]: "🧑🏿‍🦽‍➡️", - ] case .manInManualWheelchair: return [ [.light]: "👨🏻‍🦽", @@ -2059,14 +1953,6 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦽", [.dark]: "👨🏿‍🦽", ] - case .manInManualWheelchairFacingRight: - return [ - [.light]: "👨🏻‍🦽‍➡️", - [.mediumLight]: "👨🏼‍🦽‍➡️", - [.medium]: "👨🏽‍🦽‍➡️", - [.mediumDark]: "👨🏾‍🦽‍➡️", - [.dark]: "👨🏿‍🦽‍➡️", - ] case .womanInManualWheelchair: return [ [.light]: "👩🏻‍🦽", @@ -2075,14 +1961,6 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦽", [.dark]: "👩🏿‍🦽", ] - case .womanInManualWheelchairFacingRight: - return [ - [.light]: "👩🏻‍🦽‍➡️", - [.mediumLight]: "👩🏼‍🦽‍➡️", - [.medium]: "👩🏽‍🦽‍➡️", - [.mediumDark]: "👩🏾‍🦽‍➡️", - [.dark]: "👩🏿‍🦽‍➡️", - ] case .runner: return [ [.light]: "🏃🏻", @@ -2107,30 +1985,6 @@ extension Emoji { [.mediumDark]: "🏃🏾‍♀️", [.dark]: "🏃🏿‍♀️", ] - case .personRunningFacingRight: - return [ - [.light]: "🏃🏻‍➡️", - [.mediumLight]: "🏃🏼‍➡️", - [.medium]: "🏃🏽‍➡️", - [.mediumDark]: "🏃🏾‍➡️", - [.dark]: "🏃🏿‍➡️", - ] - case .womanRunningFacingRight: - return [ - [.light]: "🏃🏻‍♀️‍➡️", - [.mediumLight]: "🏃🏼‍♀️‍➡️", - [.medium]: "🏃🏽‍♀️‍➡️", - [.mediumDark]: "🏃🏾‍♀️‍➡️", - [.dark]: "🏃🏿‍♀️‍➡️", - ] - case .manRunningFacingRight: - return [ - [.light]: "🏃🏻‍♂️‍➡️", - [.mediumLight]: "🏃🏼‍♂️‍➡️", - [.medium]: "🏃🏽‍♂️‍➡️", - [.mediumDark]: "🏃🏾‍♂️‍➡️", - [.dark]: "🏃🏿‍♂️‍➡️", - ] case .dancer: return [ [.light]: "💃🏻", @@ -2887,3 +2741,4 @@ extension Emoji { } } } +// swiftlint:disable all diff --git a/Session/Emoji/Emoji.swift b/Session/Emoji/Emoji.swift index a315b53273..aa8352ca24 100644 --- a/Session/Emoji/Emoji.swift +++ b/Session/Emoji/Emoji.swift @@ -1,11 +1,9 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. -// + // swiftlint:disable all // stringlint:disable -import Foundation - /// A sorted representation of all available emoji enum Emoji: String, CaseIterable, Equatable { case grinning = "😀" @@ -58,8 +56,6 @@ enum Emoji: String, CaseIterable, Equatable { case faceExhaling = "😮‍💨" case lyingFace = "🤥" case shakingFace = "🫨" - case headShakingHorizontally = "🙂‍↔️" - case headShakingVertically = "🙂‍↕️" case relieved = "😌" case pensive = "😔" case sleepy = "😪" @@ -418,42 +414,24 @@ enum Emoji: String, CaseIterable, Equatable { case walking = "🚶" case manWalking = "🚶‍♂️" case womanWalking = "🚶‍♀️" - case personWalkingFacingRight = "🚶‍➡️" - case womanWalkingFacingRight = "🚶‍♀️‍➡️" - case manWalkingFacingRight = "🚶‍♂️‍➡️" case standingPerson = "🧍" case manStanding = "🧍‍♂️" case womanStanding = "🧍‍♀️" case kneelingPerson = "🧎" case manKneeling = "🧎‍♂️" case womanKneeling = "🧎‍♀️" - case personKneelingFacingRight = "🧎‍➡️" - case womanKneelingFacingRight = "🧎‍♀️‍➡️" - case manKneelingFacingRight = "🧎‍♂️‍➡️" case personWithProbingCane = "🧑‍🦯" - case personWithWhiteCaneFacingRight = "🧑‍🦯‍➡️" case manWithProbingCane = "👨‍🦯" - case manWithWhiteCaneFacingRight = "👨‍🦯‍➡️" case womanWithProbingCane = "👩‍🦯" - case womanWithWhiteCaneFacingRight = "👩‍🦯‍➡️" case personInMotorizedWheelchair = "🧑‍🦼" - case personInMotorizedWheelchairFacingRight = "🧑‍🦼‍➡️" case manInMotorizedWheelchair = "👨‍🦼" - case manInMotorizedWheelchairFacingRight = "👨‍🦼‍➡️" case womanInMotorizedWheelchair = "👩‍🦼" - case womanInMotorizedWheelchairFacingRight = "👩‍🦼‍➡️" case personInManualWheelchair = "🧑‍🦽" - case personInManualWheelchairFacingRight = "🧑‍🦽‍➡️" case manInManualWheelchair = "👨‍🦽" - case manInManualWheelchairFacingRight = "👨‍🦽‍➡️" case womanInManualWheelchair = "👩‍🦽" - case womanInManualWheelchairFacingRight = "👩‍🦽‍➡️" case runner = "🏃" case manRunning = "🏃‍♂️" case womanRunning = "🏃‍♀️" - case personRunningFacingRight = "🏃‍➡️" - case womanRunningFacingRight = "🏃‍♀️‍➡️" - case manRunningFacingRight = "🏃‍♂️‍➡️" case dancer = "💃" case manDancing = "🕺" case manInBusinessSuitLevitating = "🕴️" @@ -526,6 +504,7 @@ enum Emoji: String, CaseIterable, Equatable { case womanHeartMan = "👩‍❤️‍👨" case manHeartMan = "👨‍❤️‍👨" case womanHeartWoman = "👩‍❤️‍👩" + case family = "👪" case manWomanBoy = "👨‍👩‍👦" case manWomanGirl = "👨‍👩‍👧" case manWomanGirlBoy = "👨‍👩‍👧‍👦" @@ -555,11 +534,6 @@ enum Emoji: String, CaseIterable, Equatable { case bustInSilhouette = "👤" case bustsInSilhouette = "👥" case peopleHugging = "🫂" - case family = "👪" - case familyAdultAdultChild = "🧑‍🧑‍🧒" - case familyAdultAdultChildChild = "🧑‍🧑‍🧒‍🧒" - case familyAdultChild = "🧑‍🧒" - case familyAdultChildChild = "🧑‍🧒‍🧒" case footprints = "👣" case skinTone2 = "🏻" case skinTone3 = "🏼" @@ -653,7 +627,6 @@ enum Emoji: String, CaseIterable, Equatable { case wing = "🪽" case blackBird = "🐦‍⬛" case goose = "🪿" - case phoenix = "🐦‍🔥" case frog = "🐸" case crocodile = "🐊" case turtle = "🐢" @@ -724,7 +697,6 @@ enum Emoji: String, CaseIterable, Equatable { case watermelon = "🍉" case tangerine = "🍊" case lemon = "🍋" - case lime = "🍋‍🟩" case banana = "🍌" case pineapple = "🍍" case mango = "🥭" @@ -756,7 +728,6 @@ enum Emoji: String, CaseIterable, Equatable { case chestnut = "🌰" case gingerRoot = "🫚" case peaPod = "🫛" - case brownMushroom = "🍄‍🟫" case bread = "🍞" case croissant = "🥐" case baguetteBread = "🥖" @@ -1365,7 +1336,6 @@ enum Emoji: String, CaseIterable, Equatable { case scales = "⚖️" case probingCane = "🦯" case link = "🔗" - case brokenChain = "⛓️‍💥" case chains = "⛓️" case hook = "🪝" case toolbox = "🧰" @@ -1912,3 +1882,4 @@ enum Emoji: String, CaseIterable, Equatable { case flagScotland = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" case flagWales = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" } +// swiftlint:disable all diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift index 3572ff1249..e1b1c84daa 100644 --- a/Session/Emoji/EmojiWithSkinTones+String.swift +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -1,11 +1,9 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. -// + // swiftlint:disable all // stringlint:disable -import Foundation - extension EmojiWithSkinTones { init?(rawValue: String) { guard rawValue.isSingleEmoji else { return nil } @@ -77,19 +75,16 @@ extension EmojiWithSkinTones { case 1934: self = EmojiWithSkinTones.emojiFrom1934(rawValue) case 1935: self = EmojiWithSkinTones.emojiFrom1935(rawValue) case 1937: self = EmojiWithSkinTones.emojiFrom1937(rawValue) - case 2104: self = EmojiWithSkinTones.emojiFrom2104(rawValue) case 2109: self = EmojiWithSkinTones.emojiFrom2109(rawValue) case 2111: self = EmojiWithSkinTones.emojiFrom2111(rawValue) case 2112: self = EmojiWithSkinTones.emojiFrom2112(rawValue) case 2113: self = EmojiWithSkinTones.emojiFrom2113(rawValue) case 2116: self = EmojiWithSkinTones.emojiFrom2116(rawValue) case 2117: self = EmojiWithSkinTones.emojiFrom2117(rawValue) - case 2120: self = EmojiWithSkinTones.emojiFrom2120(rawValue) case 2123: self = EmojiWithSkinTones.emojiFrom2123(rawValue) case 2125: self = EmojiWithSkinTones.emojiFrom2125(rawValue) case 2126: self = EmojiWithSkinTones.emojiFrom2126(rawValue) case 2127: self = EmojiWithSkinTones.emojiFrom2127(rawValue) - case 2128: self = EmojiWithSkinTones.emojiFrom2128(rawValue) case 2129: self = EmojiWithSkinTones.emojiFrom2129(rawValue) case 2210: self = EmojiWithSkinTones.emojiFrom2210(rawValue) case 2549: self = EmojiWithSkinTones.emojiFrom2549(rawValue) @@ -110,10 +105,8 @@ extension EmojiWithSkinTones { case 2641: self = EmojiWithSkinTones.emojiFrom2641(rawValue) case 2642: self = EmojiWithSkinTones.emojiFrom2642(rawValue) case 2644: self = EmojiWithSkinTones.emojiFrom2644(rawValue) - case 2645: self = EmojiWithSkinTones.emojiFrom2645(rawValue) case 2646: self = EmojiWithSkinTones.emojiFrom2646(rawValue) case 2649: self = EmojiWithSkinTones.emojiFrom2649(rawValue) - case 2650: self = EmojiWithSkinTones.emojiFrom2650(rawValue) case 2655: self = EmojiWithSkinTones.emojiFrom2655(rawValue) case 2656: self = EmojiWithSkinTones.emojiFrom2656(rawValue) case 2657: self = EmojiWithSkinTones.emojiFrom2657(rawValue) @@ -124,9 +117,6 @@ extension EmojiWithSkinTones { case 2760: self = EmojiWithSkinTones.emojiFrom2760(rawValue) case 2761: self = EmojiWithSkinTones.emojiFrom2761(rawValue) case 2764: self = EmojiWithSkinTones.emojiFrom2764(rawValue) - case 2943: self = EmojiWithSkinTones.emojiFrom2943(rawValue) - case 2951: self = EmojiWithSkinTones.emojiFrom2951(rawValue) - case 2959: self = EmojiWithSkinTones.emojiFrom2959(rawValue) case 3289: self = EmojiWithSkinTones.emojiFrom3289(rawValue) case 3295: self = EmojiWithSkinTones.emojiFrom3295(rawValue) case 3389: self = EmojiWithSkinTones.emojiFrom3389(rawValue) @@ -136,16 +126,12 @@ extension EmojiWithSkinTones { case 3394: self = EmojiWithSkinTones.emojiFrom3394(rawValue) case 3396: self = EmojiWithSkinTones.emojiFrom3396(rawValue) case 3397: self = EmojiWithSkinTones.emojiFrom3397(rawValue) - case 3400: self = EmojiWithSkinTones.emojiFrom3400(rawValue) case 3403: self = EmojiWithSkinTones.emojiFrom3403(rawValue) case 3404: self = EmojiWithSkinTones.emojiFrom3404(rawValue) case 3405: self = EmojiWithSkinTones.emojiFrom3405(rawValue) case 3406: self = EmojiWithSkinTones.emojiFrom3406(rawValue) case 3407: self = EmojiWithSkinTones.emojiFrom3407(rawValue) - case 3408: self = EmojiWithSkinTones.emojiFrom3408(rawValue) case 3477: self = EmojiWithSkinTones.emojiFrom3477(rawValue) - case 3491: self = EmojiWithSkinTones.emojiFrom3491(rawValue) - case 3505: self = EmojiWithSkinTones.emojiFrom3505(rawValue) case 3921: self = EmojiWithSkinTones.emojiFrom3921(rawValue) case 3922: self = EmojiWithSkinTones.emojiFrom3922(rawValue) case 3924: self = EmojiWithSkinTones.emojiFrom3924(rawValue) @@ -163,16 +149,9 @@ extension EmojiWithSkinTones { case 3951: self = EmojiWithSkinTones.emojiFrom3951(rawValue) case 4007: self = EmojiWithSkinTones.emojiFrom4007(rawValue) case 4046: self = EmojiWithSkinTones.emojiFrom4046(rawValue) - case 4048: self = EmojiWithSkinTones.emojiFrom4048(rawValue) - case 4223: self = EmojiWithSkinTones.emojiFrom4223(rawValue) - case 4231: self = EmojiWithSkinTones.emojiFrom4231(rawValue) - case 4239: self = EmojiWithSkinTones.emojiFrom4239(rawValue) - case 4771: self = EmojiWithSkinTones.emojiFrom4771(rawValue) - case 4785: self = EmojiWithSkinTones.emojiFrom4785(rawValue) case 4840: self = EmojiWithSkinTones.emojiFrom4840(rawValue) case 5237: self = EmojiWithSkinTones.emojiFrom5237(rawValue) case 5370: self = EmojiWithSkinTones.emojiFrom5370(rawValue) - case 5425: self = EmojiWithSkinTones.emojiFrom5425(rawValue) case 6037: self = EmojiWithSkinTones.emojiFrom6037(rawValue) case 6065: self = EmojiWithSkinTones.emojiFrom6065(rawValue) case 6579: self = EmojiWithSkinTones.emojiFrom6579(rawValue) @@ -982,9 +961,9 @@ extension EmojiWithSkinTones { "👬": EmojiWithSkinTones(baseEmoji: .twoMenHoldingHands, skinTones: nil), "💏": EmojiWithSkinTones(baseEmoji: .personKissPerson, skinTones: nil), "💑": EmojiWithSkinTones(baseEmoji: .personHeartPerson, skinTones: nil), + "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "👤": EmojiWithSkinTones(baseEmoji: .bustInSilhouette, skinTones: nil), "👥": EmojiWithSkinTones(baseEmoji: .bustsInSilhouette, skinTones: nil), - "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "💐": EmojiWithSkinTones(baseEmoji: .bouquet, skinTones: nil), "💮": EmojiWithSkinTones(baseEmoji: .whiteFlower, skinTones: nil), "💒": EmojiWithSkinTones(baseEmoji: .wedding, skinTones: nil), @@ -2014,14 +1993,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2104(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🙂‍↔️": EmojiWithSkinTones(baseEmoji: .headShakingHorizontally, skinTones: nil), - "🙂‍↕️": EmojiWithSkinTones(baseEmoji: .headShakingVertically, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom2109(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏃‍♂️": EmojiWithSkinTones(baseEmoji: .manRunning, skinTones: nil), @@ -2075,9 +2046,7 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: nil), "👩‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: nil), - "🏃‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: nil), - "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil), - "⛓️‍💥": EmojiWithSkinTones(baseEmoji: .brokenChain, skinTones: nil) + "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -2115,13 +2084,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2120(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🚶‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom2123(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: nil), @@ -2197,13 +2159,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2128(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧎‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom2129(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "❤️‍🩹": EmojiWithSkinTones(baseEmoji: .mendingHeart, skinTones: nil) @@ -3242,13 +3197,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2645(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🐦‍🔥": EmojiWithSkinTones(baseEmoji: .phoenix, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom2646(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍🔧": EmojiWithSkinTones(baseEmoji: .maleMechanic, skinTones: nil), @@ -3271,14 +3219,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2650(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🍋‍🟩": EmojiWithSkinTones(baseEmoji: .lime, skinTones: nil), - "🍄‍🟫": EmojiWithSkinTones(baseEmoji: .brownMushroom, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom2655(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🧑‍🎓": EmojiWithSkinTones(baseEmoji: .student, skinTones: nil), @@ -3353,8 +3293,7 @@ extension EmojiWithSkinTones { "🧑‍🦲": EmojiWithSkinTones(baseEmoji: .baldPerson, skinTones: nil), "🧑‍🦯": EmojiWithSkinTones(baseEmoji: .personWithProbingCane, skinTones: nil), "🧑‍🦼": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchair, skinTones: nil), - "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil), - "🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChild, skinTones: nil) + "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3384,30 +3323,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom2943(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🏃‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: nil), - "🏃‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom2951(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🚶‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: nil), - "🚶‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom2959(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧎‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: nil), - "🧎‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom3289(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏳️‍🌈": EmojiWithSkinTones(baseEmoji: .rainbowFlag, skinTones: nil) @@ -3604,19 +3519,14 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.light]), "👩🏻‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.light]), - "🏃🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.light]), "👨🏼‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumLight]), "👩🏼‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumLight]), - "🏃🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumLight]), "👨🏽‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.medium]), "👩🏽‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.medium]), - "🏃🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.medium]), "👨🏾‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumDark]), "👩🏾‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumDark]), - "🏃🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumDark]), "👨🏿‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.dark]), - "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]), - "🏃🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.dark]) + "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3749,17 +3659,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom3400(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🚶🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.light]), - "🚶🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumLight]), - "🚶🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.medium]), - "🚶🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumDark]), - "🚶🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom3403(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦🏻‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: [.light]), @@ -4015,17 +3914,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom3408(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧎🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.light]), - "🧎🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumLight]), - "🧎🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.medium]), - "🧎🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumDark]), - "🧎🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom3477(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍👨": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: nil), @@ -4035,27 +3923,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom3491(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "👨‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: nil), - "👩‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: nil), - "👨‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: nil), - "👩‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: nil), - "👨‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: nil), - "👩‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom3505(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧑‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: nil), - "🧑‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: nil), - "🧑‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom3921(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍🎓": EmojiWithSkinTones(baseEmoji: .maleStudent, skinTones: [.light]), @@ -4492,119 +4359,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom4048(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧑‍🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChild, skinTones: nil), - "🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChildChild, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom4223(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🏃🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.light]), - "🏃🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.light]), - "🏃🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumLight]), - "🏃🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumLight]), - "🏃🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.medium]), - "🏃🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.medium]), - "🏃🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumDark]), - "🏃🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumDark]), - "🏃🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.dark]), - "🏃🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom4231(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🚶🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.light]), - "🚶🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.light]), - "🚶🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumLight]), - "🚶🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumLight]), - "🚶🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.medium]), - "🚶🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.medium]), - "🚶🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumDark]), - "🚶🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumDark]), - "🚶🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.dark]), - "🚶🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom4239(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧎🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.light]), - "🧎🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.light]), - "🧎🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumLight]), - "🧎🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumLight]), - "🧎🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.medium]), - "🧎🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.medium]), - "🧎🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumDark]), - "🧎🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumDark]), - "🧎🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.dark]), - "🧎🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom4771(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "👨🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.light]), - "👩🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.light]), - "👨🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.light]), - "👩🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.light]), - "👨🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.light]), - "👩🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.light]), - "👨🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumLight]), - "👩🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumLight]), - "👨🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), - "👩🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), - "👨🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumLight]), - "👩🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumLight]), - "👨🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.medium]), - "👩🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.medium]), - "👨🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.medium]), - "👩🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.medium]), - "👨🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.medium]), - "👩🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.medium]), - "👨🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumDark]), - "👩🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumDark]), - "👨🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), - "👩🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), - "👨🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumDark]), - "👩🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumDark]), - "👨🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.dark]), - "👩🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.dark]), - "👨🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.dark]), - "👩🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.dark]), - "👨🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.dark]), - "👩🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - - private static func emojiFrom4785(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧑🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.light]), - "🧑🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.light]), - "🧑🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.light]), - "🧑🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumLight]), - "🧑🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), - "🧑🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumLight]), - "🧑🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.medium]), - "🧑🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.medium]), - "🧑🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.medium]), - "🧑🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumDark]), - "🧑🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), - "🧑🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumDark]), - "🧑🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.dark]), - "🧑🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.dark]), - "🧑🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.dark]) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom4840(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍💋‍👨": EmojiWithSkinTones(baseEmoji: .womanKissMan, skinTones: nil), @@ -4655,13 +4409,6 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } - private static func emojiFrom5425(_ rawValue: String) -> EmojiWithSkinTones { - let lookup: [String: EmojiWithSkinTones] = [ - "🧑‍🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChildChild, skinTones: nil) - ] - return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) - } - private static func emojiFrom6037(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩🏻‍❤️‍👨🏻": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: [.light]), @@ -4982,3 +4729,4 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } } +// swiftlint:disable all From e45ee5adb03f6ec92ad505f8ca7f8ca0e50fad55 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 14:13:27 +1000 Subject: [PATCH 220/244] Re-apply 'fix/SES-4551/emoji_category_section_title' changes --- Scripts/EmojiGenerator.swift | 11 +- .../EmojiPickerCollectionView.swift | 7 +- Session/Emoji/Emoji+Category.swift | 85 +++++- Session/Emoji/Emoji+Name.swift | 37 ++- Session/Emoji/Emoji+SkinTones.swift | 149 +++++++++- Session/Emoji/Emoji.swift | 35 ++- Session/Emoji/EmojiWithSkinTones+String.swift | 264 +++++++++++++++++- 7 files changed, 556 insertions(+), 32 deletions(-) diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 519ce65220..1c2102c81f 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -53,7 +53,7 @@ enum RemoteModel { case flags = "Flags" case components = "Component" - var localizedKey: String = { + var localizedKey: String { switch self { case .smileys: return "Smileys" @@ -77,8 +77,8 @@ enum RemoteModel { return "Flags" case .components: return "Component" - } - }() + } + } } static func fetchEmojiData() throws -> Data { @@ -569,8 +569,9 @@ extension EmojiGenerator { fileHandle.indent { let stringKey = "emojiCategory\(category.localizedKey)" let stringComment = "The name for the emoji category '\(category.rawValue)'" - - fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")") + + fileHandle.writeLine("// \(stringComment)") + fileHandle.writeLine("return \"\(stringKey)\".localized()") } } fileHandle.writeLine("}") diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index b366b5e2a8..289a6bb8e2 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -322,7 +322,12 @@ private class EmojiSectionHeader: UICollectionReusableView { label.font = .systemFont(ofSize: Values.smallFontSize) label.themeTextColor = .textPrimary addSubview(label) - label.pin(to: self) + + label.pin(.top, to: .top, of: self) + label.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + label.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) + label.pin(.bottom, to: .bottom, of: self) + label.setCompressionResistance(to: .required) } diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 343d9a0e87..996f7d83a4 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -20,21 +20,29 @@ extension Emoji { var localizedName: String { switch self { case .smileysAndPeople: - return NSLocalizedString("emojiCategorySmileys", comment: "The name for the emoji category 'Smileys & People'") + // The name for the emoji category 'Smileys & People' + return "emojiCategorySmileys".localized() case .animals: - return NSLocalizedString("emojiCategoryAnimals", comment: "The name for the emoji category 'Animals & Nature'") + // The name for the emoji category 'Animals & Nature' + return "emojiCategoryAnimals".localized() case .food: - return NSLocalizedString("emojiCategoryFood", comment: "The name for the emoji category 'Food & Drink'") + // The name for the emoji category 'Food & Drink' + return "emojiCategoryFood".localized() case .activities: - return NSLocalizedString("emojiCategoryActivities", comment: "The name for the emoji category 'Activities'") + // The name for the emoji category 'Activities' + return "emojiCategoryActivities".localized() case .travel: - return NSLocalizedString("emojiCategoryTravel", comment: "The name for the emoji category 'Travel & Places'") + // The name for the emoji category 'Travel & Places' + return "emojiCategoryTravel".localized() case .objects: - return NSLocalizedString("emojiCategoryObjects", comment: "The name for the emoji category 'Objects'") + // The name for the emoji category 'Objects' + return "emojiCategoryObjects".localized() case .symbols: - return NSLocalizedString("emojiCategorySymbols", comment: "The name for the emoji category 'Symbols'") + // The name for the emoji category 'Symbols' + return "emojiCategorySymbols".localized() case .flags: - return NSLocalizedString("emojiCategoryFlags", comment: "The name for the emoji category 'Flags'") + // The name for the emoji category 'Flags' + return "emojiCategoryFlags".localized() } } @@ -92,6 +100,8 @@ extension Emoji { .faceExhaling, .lyingFace, .shakingFace, + .headShakingHorizontally, + .headShakingVertically, .relieved, .pensive, .sleepy, @@ -450,24 +460,42 @@ extension Emoji { .walking, .manWalking, .womanWalking, + .personWalkingFacingRight, + .womanWalkingFacingRight, + .manWalkingFacingRight, .standingPerson, .manStanding, .womanStanding, .kneelingPerson, .manKneeling, .womanKneeling, + .personKneelingFacingRight, + .womanKneelingFacingRight, + .manKneelingFacingRight, .personWithProbingCane, + .personWithWhiteCaneFacingRight, .manWithProbingCane, + .manWithWhiteCaneFacingRight, .womanWithProbingCane, + .womanWithWhiteCaneFacingRight, .personInMotorizedWheelchair, + .personInMotorizedWheelchairFacingRight, .manInMotorizedWheelchair, + .manInMotorizedWheelchairFacingRight, .womanInMotorizedWheelchair, + .womanInMotorizedWheelchairFacingRight, .personInManualWheelchair, + .personInManualWheelchairFacingRight, .manInManualWheelchair, + .manInManualWheelchairFacingRight, .womanInManualWheelchair, + .womanInManualWheelchairFacingRight, .runner, .manRunning, .womanRunning, + .personRunningFacingRight, + .womanRunningFacingRight, + .manRunningFacingRight, .dancer, .manDancing, .manInBusinessSuitLevitating, @@ -540,7 +568,6 @@ extension Emoji { .womanHeartMan, .manHeartMan, .womanHeartWoman, - .family, .manWomanBoy, .manWomanGirl, .manWomanGirlBoy, @@ -570,6 +597,11 @@ extension Emoji { .bustInSilhouette, .bustsInSilhouette, .peopleHugging, + .family, + .familyAdultAdultChild, + .familyAdultAdultChildChild, + .familyAdultChild, + .familyAdultChildChild, .footprints, ] case .animals: @@ -661,6 +693,7 @@ extension Emoji { .wing, .blackBird, .goose, + .phoenix, .frog, .crocodile, .turtle, @@ -734,6 +767,7 @@ extension Emoji { .watermelon, .tangerine, .lemon, + .lime, .banana, .pineapple, .mango, @@ -765,6 +799,7 @@ extension Emoji { .chestnut, .gingerRoot, .peaPod, + .brownMushroom, .bread, .croissant, .baguetteBread, @@ -1382,6 +1417,7 @@ extension Emoji { .scales, .probingCane, .link, + .brokenChain, .chains, .hook, .toolbox, @@ -1989,6 +2025,8 @@ extension Emoji { case .faceExhaling: return .smileysAndPeople case .lyingFace: return .smileysAndPeople case .shakingFace: return .smileysAndPeople + case .headShakingHorizontally: return .smileysAndPeople + case .headShakingVertically: return .smileysAndPeople case .relieved: return .smileysAndPeople case .pensive: return .smileysAndPeople case .sleepy: return .smileysAndPeople @@ -2347,24 +2385,42 @@ extension Emoji { case .walking: return .smileysAndPeople case .manWalking: return .smileysAndPeople case .womanWalking: return .smileysAndPeople + case .personWalkingFacingRight: return .smileysAndPeople + case .womanWalkingFacingRight: return .smileysAndPeople + case .manWalkingFacingRight: return .smileysAndPeople case .standingPerson: return .smileysAndPeople case .manStanding: return .smileysAndPeople case .womanStanding: return .smileysAndPeople case .kneelingPerson: return .smileysAndPeople case .manKneeling: return .smileysAndPeople case .womanKneeling: return .smileysAndPeople + case .personKneelingFacingRight: return .smileysAndPeople + case .womanKneelingFacingRight: return .smileysAndPeople + case .manKneelingFacingRight: return .smileysAndPeople case .personWithProbingCane: return .smileysAndPeople + case .personWithWhiteCaneFacingRight: return .smileysAndPeople case .manWithProbingCane: return .smileysAndPeople + case .manWithWhiteCaneFacingRight: return .smileysAndPeople case .womanWithProbingCane: return .smileysAndPeople + case .womanWithWhiteCaneFacingRight: return .smileysAndPeople case .personInMotorizedWheelchair: return .smileysAndPeople + case .personInMotorizedWheelchairFacingRight: return .smileysAndPeople case .manInMotorizedWheelchair: return .smileysAndPeople + case .manInMotorizedWheelchairFacingRight: return .smileysAndPeople case .womanInMotorizedWheelchair: return .smileysAndPeople + case .womanInMotorizedWheelchairFacingRight: return .smileysAndPeople case .personInManualWheelchair: return .smileysAndPeople + case .personInManualWheelchairFacingRight: return .smileysAndPeople case .manInManualWheelchair: return .smileysAndPeople + case .manInManualWheelchairFacingRight: return .smileysAndPeople case .womanInManualWheelchair: return .smileysAndPeople + case .womanInManualWheelchairFacingRight: return .smileysAndPeople case .runner: return .smileysAndPeople case .manRunning: return .smileysAndPeople case .womanRunning: return .smileysAndPeople + case .personRunningFacingRight: return .smileysAndPeople + case .womanRunningFacingRight: return .smileysAndPeople + case .manRunningFacingRight: return .smileysAndPeople case .dancer: return .smileysAndPeople case .manDancing: return .smileysAndPeople case .manInBusinessSuitLevitating: return .smileysAndPeople @@ -2437,7 +2493,6 @@ extension Emoji { case .womanHeartMan: return .smileysAndPeople case .manHeartMan: return .smileysAndPeople case .womanHeartWoman: return .smileysAndPeople - case .family: return .smileysAndPeople case .manWomanBoy: return .smileysAndPeople case .manWomanGirl: return .smileysAndPeople case .manWomanGirlBoy: return .smileysAndPeople @@ -2467,6 +2522,11 @@ extension Emoji { case .bustInSilhouette: return .smileysAndPeople case .bustsInSilhouette: return .smileysAndPeople case .peopleHugging: return .smileysAndPeople + case .family: return .smileysAndPeople + case .familyAdultAdultChild: return .smileysAndPeople + case .familyAdultAdultChildChild: return .smileysAndPeople + case .familyAdultChild: return .smileysAndPeople + case .familyAdultChildChild: return .smileysAndPeople case .footprints: return .smileysAndPeople case .monkeyFace: return .animals case .monkey: return .animals @@ -2555,6 +2615,7 @@ extension Emoji { case .wing: return .animals case .blackBird: return .animals case .goose: return .animals + case .phoenix: return .animals case .frog: return .animals case .crocodile: return .animals case .turtle: return .animals @@ -2625,6 +2686,7 @@ extension Emoji { case .watermelon: return .food case .tangerine: return .food case .lemon: return .food + case .lime: return .food case .banana: return .food case .pineapple: return .food case .mango: return .food @@ -2656,6 +2718,7 @@ extension Emoji { case .chestnut: return .food case .gingerRoot: return .food case .peaPod: return .food + case .brownMushroom: return .food case .bread: return .food case .croissant: return .food case .baguetteBread: return .food @@ -3264,6 +3327,7 @@ extension Emoji { case .scales: return .objects case .probingCane: return .objects case .link: return .objects + case .brokenChain: return .objects case .chains: return .objects case .hook: return .objects case .toolbox: return .objects @@ -3821,4 +3885,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift index 2ddb050adb..2b6abe71f9 100644 --- a/Session/Emoji/Emoji+Name.swift +++ b/Session/Emoji/Emoji+Name.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { var name: String { switch self { @@ -57,6 +59,8 @@ extension Emoji { case .faceExhaling: return "face exhaling, face_exhaling, faceexhaling" case .lyingFace: return "lying face, lying_face, lyingface" case .shakingFace: return "shaking face, shaking_face, shakingface" + case .headShakingHorizontally: return "head shaking horizontally, head_shaking_horizontally, headshakinghorizontally" + case .headShakingVertically: return "head shaking vertically, head_shaking_vertically, headshakingvertically" case .relieved: return "relieved, relieved face" case .pensive: return "pensive, pensive face" case .sleepy: return "sleepy, sleepy face" @@ -415,24 +419,42 @@ extension Emoji { case .walking: return "pedestrian, walking" case .manWalking: return "man walking, man-walking, manwalking" case .womanWalking: return "woman walking, woman-walking, womanwalking" + case .personWalkingFacingRight: return "person walking facing right, person_walking_facing_right, personwalkingfacingright" + case .womanWalkingFacingRight: return "woman walking facing right, woman_walking_facing_right, womanwalkingfacingright" + case .manWalkingFacingRight: return "man walking facing right, man_walking_facing_right, manwalkingfacingright" case .standingPerson: return "standing person, standing_person, standingperson" case .manStanding: return "man standing, man_standing, manstanding" case .womanStanding: return "woman standing, woman_standing, womanstanding" case .kneelingPerson: return "kneeling person, kneeling_person, kneelingperson" case .manKneeling: return "man kneeling, man_kneeling, mankneeling" case .womanKneeling: return "woman kneeling, woman_kneeling, womankneeling" + case .personKneelingFacingRight: return "person kneeling facing right, person_kneeling_facing_right, personkneelingfacingright" + case .womanKneelingFacingRight: return "woman kneeling facing right, woman_kneeling_facing_right, womankneelingfacingright" + case .manKneelingFacingRight: return "man kneeling facing right, man_kneeling_facing_right, mankneelingfacingright" case .personWithProbingCane: return "person with white cane, person_with_probing_cane, personwithprobingcane" + case .personWithWhiteCaneFacingRight: return "person with white cane facing right, person_with_white_cane_facing_right, personwithwhitecanefacingright" case .manWithProbingCane: return "man with white cane, man_with_probing_cane, manwithprobingcane" + case .manWithWhiteCaneFacingRight: return "man with white cane facing right, man_with_white_cane_facing_right, manwithwhitecanefacingright" case .womanWithProbingCane: return "woman with white cane, woman_with_probing_cane, womanwithprobingcane" + case .womanWithWhiteCaneFacingRight: return "woman with white cane facing right, woman_with_white_cane_facing_right, womanwithwhitecanefacingright" case .personInMotorizedWheelchair: return "person in motorized wheelchair, person_in_motorized_wheelchair, personinmotorizedwheelchair" + case .personInMotorizedWheelchairFacingRight: return "person in motorized wheelchair facing right, person_in_motorized_wheelchair_facing_right, personinmotorizedwheelchairfacingright" case .manInMotorizedWheelchair: return "man in motorized wheelchair, man_in_motorized_wheelchair, maninmotorizedwheelchair" + case .manInMotorizedWheelchairFacingRight: return "man in motorized wheelchair facing right, man_in_motorized_wheelchair_facing_right, maninmotorizedwheelchairfacingright" case .womanInMotorizedWheelchair: return "woman in motorized wheelchair, woman_in_motorized_wheelchair, womaninmotorizedwheelchair" + case .womanInMotorizedWheelchairFacingRight: return "woman in motorized wheelchair facing right, woman_in_motorized_wheelchair_facing_right, womaninmotorizedwheelchairfacingright" case .personInManualWheelchair: return "person in manual wheelchair, person_in_manual_wheelchair, personinmanualwheelchair" + case .personInManualWheelchairFacingRight: return "person in manual wheelchair facing right, person_in_manual_wheelchair_facing_right, personinmanualwheelchairfacingright" case .manInManualWheelchair: return "man in manual wheelchair, man_in_manual_wheelchair, maninmanualwheelchair" + case .manInManualWheelchairFacingRight: return "man in manual wheelchair facing right, man_in_manual_wheelchair_facing_right, maninmanualwheelchairfacingright" case .womanInManualWheelchair: return "woman in manual wheelchair, woman_in_manual_wheelchair, womaninmanualwheelchair" + case .womanInManualWheelchairFacingRight: return "woman in manual wheelchair facing right, woman_in_manual_wheelchair_facing_right, womaninmanualwheelchairfacingright" case .runner: return "runner, running" case .manRunning: return "man running, man-running, manrunning" case .womanRunning: return "woman running, woman-running, womanrunning" + case .personRunningFacingRight: return "person running facing right, person_running_facing_right, personrunningfacingright" + case .womanRunningFacingRight: return "woman running facing right, woman_running_facing_right, womanrunningfacingright" + case .manRunningFacingRight: return "man running facing right, man_running_facing_right, manrunningfacingright" case .dancer: return "dancer" case .manDancing: return "man dancing, man_dancing, mandancing" case .manInBusinessSuitLevitating: return "man_in_business_suit_levitating, maninbusinesssuitlevitating, person in suit levitating" @@ -505,7 +527,6 @@ extension Emoji { case .womanHeartMan: return "couple with heart: woman, man, woman-heart-man, womanheartman" case .manHeartMan: return "couple with heart: man, man, man-heart-man, manheartman" case .womanHeartWoman: return "couple with heart: woman, woman, woman-heart-woman, womanheartwoman" - case .family: return "family" case .manWomanBoy: return "family: man, woman, boy, man-woman-boy, manwomanboy" case .manWomanGirl: return "family: man, woman, girl, man-woman-girl, manwomangirl" case .manWomanGirlBoy: return "family: man, woman, girl, boy, man-woman-girl-boy, manwomangirlboy" @@ -535,6 +556,11 @@ extension Emoji { case .bustInSilhouette: return "bust in silhouette, bust_in_silhouette, bustinsilhouette" case .bustsInSilhouette: return "busts in silhouette, busts_in_silhouette, bustsinsilhouette" case .peopleHugging: return "people hugging, people_hugging, peoplehugging" + case .family: return "family" + case .familyAdultAdultChild: return "family: adult, adult, child, family_adult_adult_child, familyadultadultchild" + case .familyAdultAdultChildChild: return "family: adult, adult, child, child, family_adult_adult_child_child, familyadultadultchildchild" + case .familyAdultChild: return "family: adult, child, family_adult_child, familyadultchild" + case .familyAdultChildChild: return "family: adult, child, child, family_adult_child_child, familyadultchildchild" case .footprints: return "footprints" case .skinTone2: return "emoji modifier fitzpatrick type-1-2, skin-tone-2, skintone2" case .skinTone3: return "emoji modifier fitzpatrick type-3, skin-tone-3, skintone3" @@ -628,6 +654,7 @@ extension Emoji { case .wing: return "wing" case .blackBird: return "black bird, black_bird, blackbird" case .goose: return "goose" + case .phoenix: return "phoenix" case .frog: return "frog, frog face" case .crocodile: return "crocodile" case .turtle: return "turtle" @@ -698,6 +725,7 @@ extension Emoji { case .watermelon: return "watermelon" case .tangerine: return "tangerine" case .lemon: return "lemon" + case .lime: return "lime" case .banana: return "banana" case .pineapple: return "pineapple" case .mango: return "mango" @@ -729,6 +757,7 @@ extension Emoji { case .chestnut: return "chestnut" case .gingerRoot: return "ginger root, ginger_root, gingerroot" case .peaPod: return "pea pod, pea_pod, peapod" + case .brownMushroom: return "brown mushroom, brown_mushroom, brownmushroom" case .bread: return "bread" case .croissant: return "croissant" case .baguetteBread: return "baguette bread, baguette_bread, baguettebread" @@ -1337,6 +1366,7 @@ extension Emoji { case .scales: return "balance scale, scales" case .probingCane: return "probing cane, probing_cane, probingcane" case .link: return "link, link symbol" + case .brokenChain: return "broken chain, broken_chain, brokenchain" case .chains: return "chains" case .hook: return "hook" case .toolbox: return "toolbox" @@ -1852,7 +1882,7 @@ extension Emoji { case .flagTm: return "flag-tm, flagtm, turkmenistan flag" case .flagTn: return "flag-tn, flagtn, tunisia flag" case .flagTo: return "flag-to, flagto, tonga flag" - case .flagTr: return "flag-tr, flagtr, turkey flag" + case .flagTr: return "flag-tr, flagtr, türkiye flag" case .flagTt: return "flag-tt, flagtt, trinidad & tobago flag" case .flagTv: return "flag-tv, flagtv, tuvalu flag" case .flagTw: return "flag-tw, flagtw, taiwan flag" @@ -1885,4 +1915,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+SkinTones.swift b/Session/Emoji/Emoji+SkinTones.swift index f1fb18434f..9f34de6151 100644 --- a/Session/Emoji/Emoji+SkinTones.swift +++ b/Session/Emoji/Emoji+SkinTones.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { enum SkinTone: String, CaseIterable, Equatable { case light = "🏻" @@ -1841,6 +1843,30 @@ extension Emoji { [.mediumDark]: "🚶🏾‍♀️", [.dark]: "🚶🏿‍♀️", ] + case .personWalkingFacingRight: + return [ + [.light]: "🚶🏻‍➡️", + [.mediumLight]: "🚶🏼‍➡️", + [.medium]: "🚶🏽‍➡️", + [.mediumDark]: "🚶🏾‍➡️", + [.dark]: "🚶🏿‍➡️", + ] + case .womanWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♀️‍➡️", + [.mediumLight]: "🚶🏼‍♀️‍➡️", + [.medium]: "🚶🏽‍♀️‍➡️", + [.mediumDark]: "🚶🏾‍♀️‍➡️", + [.dark]: "🚶🏿‍♀️‍➡️", + ] + case .manWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♂️‍➡️", + [.mediumLight]: "🚶🏼‍♂️‍➡️", + [.medium]: "🚶🏽‍♂️‍➡️", + [.mediumDark]: "🚶🏾‍♂️‍➡️", + [.dark]: "🚶🏿‍♂️‍➡️", + ] case .standingPerson: return [ [.light]: "🧍🏻", @@ -1889,6 +1915,30 @@ extension Emoji { [.mediumDark]: "🧎🏾‍♀️", [.dark]: "🧎🏿‍♀️", ] + case .personKneelingFacingRight: + return [ + [.light]: "🧎🏻‍➡️", + [.mediumLight]: "🧎🏼‍➡️", + [.medium]: "🧎🏽‍➡️", + [.mediumDark]: "🧎🏾‍➡️", + [.dark]: "🧎🏿‍➡️", + ] + case .womanKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♀️‍➡️", + [.mediumLight]: "🧎🏼‍♀️‍➡️", + [.medium]: "🧎🏽‍♀️‍➡️", + [.mediumDark]: "🧎🏾‍♀️‍➡️", + [.dark]: "🧎🏿‍♀️‍➡️", + ] + case .manKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♂️‍➡️", + [.mediumLight]: "🧎🏼‍♂️‍➡️", + [.medium]: "🧎🏽‍♂️‍➡️", + [.mediumDark]: "🧎🏾‍♂️‍➡️", + [.dark]: "🧎🏿‍♂️‍➡️", + ] case .personWithProbingCane: return [ [.light]: "🧑🏻‍🦯", @@ -1897,6 +1947,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦯", [.dark]: "🧑🏿‍🦯", ] + case .personWithWhiteCaneFacingRight: + return [ + [.light]: "🧑🏻‍🦯‍➡️", + [.mediumLight]: "🧑🏼‍🦯‍➡️", + [.medium]: "🧑🏽‍🦯‍➡️", + [.mediumDark]: "🧑🏾‍🦯‍➡️", + [.dark]: "🧑🏿‍🦯‍➡️", + ] case .manWithProbingCane: return [ [.light]: "👨🏻‍🦯", @@ -1905,6 +1963,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦯", [.dark]: "👨🏿‍🦯", ] + case .manWithWhiteCaneFacingRight: + return [ + [.light]: "👨🏻‍🦯‍➡️", + [.mediumLight]: "👨🏼‍🦯‍➡️", + [.medium]: "👨🏽‍🦯‍➡️", + [.mediumDark]: "👨🏾‍🦯‍➡️", + [.dark]: "👨🏿‍🦯‍➡️", + ] case .womanWithProbingCane: return [ [.light]: "👩🏻‍🦯", @@ -1913,6 +1979,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦯", [.dark]: "👩🏿‍🦯", ] + case .womanWithWhiteCaneFacingRight: + return [ + [.light]: "👩🏻‍🦯‍➡️", + [.mediumLight]: "👩🏼‍🦯‍➡️", + [.medium]: "👩🏽‍🦯‍➡️", + [.mediumDark]: "👩🏾‍🦯‍➡️", + [.dark]: "👩🏿‍🦯‍➡️", + ] case .personInMotorizedWheelchair: return [ [.light]: "🧑🏻‍🦼", @@ -1921,6 +1995,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦼", [.dark]: "🧑🏿‍🦼", ] + case .personInMotorizedWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦼‍➡️", + [.mediumLight]: "🧑🏼‍🦼‍➡️", + [.medium]: "🧑🏽‍🦼‍➡️", + [.mediumDark]: "🧑🏾‍🦼‍➡️", + [.dark]: "🧑🏿‍🦼‍➡️", + ] case .manInMotorizedWheelchair: return [ [.light]: "👨🏻‍🦼", @@ -1929,6 +2011,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦼", [.dark]: "👨🏿‍🦼", ] + case .manInMotorizedWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦼‍➡️", + [.mediumLight]: "👨🏼‍🦼‍➡️", + [.medium]: "👨🏽‍🦼‍➡️", + [.mediumDark]: "👨🏾‍🦼‍➡️", + [.dark]: "👨🏿‍🦼‍➡️", + ] case .womanInMotorizedWheelchair: return [ [.light]: "👩🏻‍🦼", @@ -1937,6 +2027,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦼", [.dark]: "👩🏿‍🦼", ] + case .womanInMotorizedWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦼‍➡️", + [.mediumLight]: "👩🏼‍🦼‍➡️", + [.medium]: "👩🏽‍🦼‍➡️", + [.mediumDark]: "👩🏾‍🦼‍➡️", + [.dark]: "👩🏿‍🦼‍➡️", + ] case .personInManualWheelchair: return [ [.light]: "🧑🏻‍🦽", @@ -1945,6 +2043,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦽", [.dark]: "🧑🏿‍🦽", ] + case .personInManualWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦽‍➡️", + [.mediumLight]: "🧑🏼‍🦽‍➡️", + [.medium]: "🧑🏽‍🦽‍➡️", + [.mediumDark]: "🧑🏾‍🦽‍➡️", + [.dark]: "🧑🏿‍🦽‍➡️", + ] case .manInManualWheelchair: return [ [.light]: "👨🏻‍🦽", @@ -1953,6 +2059,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦽", [.dark]: "👨🏿‍🦽", ] + case .manInManualWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦽‍➡️", + [.mediumLight]: "👨🏼‍🦽‍➡️", + [.medium]: "👨🏽‍🦽‍➡️", + [.mediumDark]: "👨🏾‍🦽‍➡️", + [.dark]: "👨🏿‍🦽‍➡️", + ] case .womanInManualWheelchair: return [ [.light]: "👩🏻‍🦽", @@ -1961,6 +2075,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦽", [.dark]: "👩🏿‍🦽", ] + case .womanInManualWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦽‍➡️", + [.mediumLight]: "👩🏼‍🦽‍➡️", + [.medium]: "👩🏽‍🦽‍➡️", + [.mediumDark]: "👩🏾‍🦽‍➡️", + [.dark]: "👩🏿‍🦽‍➡️", + ] case .runner: return [ [.light]: "🏃🏻", @@ -1985,6 +2107,30 @@ extension Emoji { [.mediumDark]: "🏃🏾‍♀️", [.dark]: "🏃🏿‍♀️", ] + case .personRunningFacingRight: + return [ + [.light]: "🏃🏻‍➡️", + [.mediumLight]: "🏃🏼‍➡️", + [.medium]: "🏃🏽‍➡️", + [.mediumDark]: "🏃🏾‍➡️", + [.dark]: "🏃🏿‍➡️", + ] + case .womanRunningFacingRight: + return [ + [.light]: "🏃🏻‍♀️‍➡️", + [.mediumLight]: "🏃🏼‍♀️‍➡️", + [.medium]: "🏃🏽‍♀️‍➡️", + [.mediumDark]: "🏃🏾‍♀️‍➡️", + [.dark]: "🏃🏿‍♀️‍➡️", + ] + case .manRunningFacingRight: + return [ + [.light]: "🏃🏻‍♂️‍➡️", + [.mediumLight]: "🏃🏼‍♂️‍➡️", + [.medium]: "🏃🏽‍♂️‍➡️", + [.mediumDark]: "🏃🏾‍♂️‍➡️", + [.dark]: "🏃🏿‍♂️‍➡️", + ] case .dancer: return [ [.light]: "💃🏻", @@ -2741,4 +2887,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji.swift b/Session/Emoji/Emoji.swift index aa8352ca24..a315b53273 100644 --- a/Session/Emoji/Emoji.swift +++ b/Session/Emoji/Emoji.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + /// A sorted representation of all available emoji enum Emoji: String, CaseIterable, Equatable { case grinning = "😀" @@ -56,6 +58,8 @@ enum Emoji: String, CaseIterable, Equatable { case faceExhaling = "😮‍💨" case lyingFace = "🤥" case shakingFace = "🫨" + case headShakingHorizontally = "🙂‍↔️" + case headShakingVertically = "🙂‍↕️" case relieved = "😌" case pensive = "😔" case sleepy = "😪" @@ -414,24 +418,42 @@ enum Emoji: String, CaseIterable, Equatable { case walking = "🚶" case manWalking = "🚶‍♂️" case womanWalking = "🚶‍♀️" + case personWalkingFacingRight = "🚶‍➡️" + case womanWalkingFacingRight = "🚶‍♀️‍➡️" + case manWalkingFacingRight = "🚶‍♂️‍➡️" case standingPerson = "🧍" case manStanding = "🧍‍♂️" case womanStanding = "🧍‍♀️" case kneelingPerson = "🧎" case manKneeling = "🧎‍♂️" case womanKneeling = "🧎‍♀️" + case personKneelingFacingRight = "🧎‍➡️" + case womanKneelingFacingRight = "🧎‍♀️‍➡️" + case manKneelingFacingRight = "🧎‍♂️‍➡️" case personWithProbingCane = "🧑‍🦯" + case personWithWhiteCaneFacingRight = "🧑‍🦯‍➡️" case manWithProbingCane = "👨‍🦯" + case manWithWhiteCaneFacingRight = "👨‍🦯‍➡️" case womanWithProbingCane = "👩‍🦯" + case womanWithWhiteCaneFacingRight = "👩‍🦯‍➡️" case personInMotorizedWheelchair = "🧑‍🦼" + case personInMotorizedWheelchairFacingRight = "🧑‍🦼‍➡️" case manInMotorizedWheelchair = "👨‍🦼" + case manInMotorizedWheelchairFacingRight = "👨‍🦼‍➡️" case womanInMotorizedWheelchair = "👩‍🦼" + case womanInMotorizedWheelchairFacingRight = "👩‍🦼‍➡️" case personInManualWheelchair = "🧑‍🦽" + case personInManualWheelchairFacingRight = "🧑‍🦽‍➡️" case manInManualWheelchair = "👨‍🦽" + case manInManualWheelchairFacingRight = "👨‍🦽‍➡️" case womanInManualWheelchair = "👩‍🦽" + case womanInManualWheelchairFacingRight = "👩‍🦽‍➡️" case runner = "🏃" case manRunning = "🏃‍♂️" case womanRunning = "🏃‍♀️" + case personRunningFacingRight = "🏃‍➡️" + case womanRunningFacingRight = "🏃‍♀️‍➡️" + case manRunningFacingRight = "🏃‍♂️‍➡️" case dancer = "💃" case manDancing = "🕺" case manInBusinessSuitLevitating = "🕴️" @@ -504,7 +526,6 @@ enum Emoji: String, CaseIterable, Equatable { case womanHeartMan = "👩‍❤️‍👨" case manHeartMan = "👨‍❤️‍👨" case womanHeartWoman = "👩‍❤️‍👩" - case family = "👪" case manWomanBoy = "👨‍👩‍👦" case manWomanGirl = "👨‍👩‍👧" case manWomanGirlBoy = "👨‍👩‍👧‍👦" @@ -534,6 +555,11 @@ enum Emoji: String, CaseIterable, Equatable { case bustInSilhouette = "👤" case bustsInSilhouette = "👥" case peopleHugging = "🫂" + case family = "👪" + case familyAdultAdultChild = "🧑‍🧑‍🧒" + case familyAdultAdultChildChild = "🧑‍🧑‍🧒‍🧒" + case familyAdultChild = "🧑‍🧒" + case familyAdultChildChild = "🧑‍🧒‍🧒" case footprints = "👣" case skinTone2 = "🏻" case skinTone3 = "🏼" @@ -627,6 +653,7 @@ enum Emoji: String, CaseIterable, Equatable { case wing = "🪽" case blackBird = "🐦‍⬛" case goose = "🪿" + case phoenix = "🐦‍🔥" case frog = "🐸" case crocodile = "🐊" case turtle = "🐢" @@ -697,6 +724,7 @@ enum Emoji: String, CaseIterable, Equatable { case watermelon = "🍉" case tangerine = "🍊" case lemon = "🍋" + case lime = "🍋‍🟩" case banana = "🍌" case pineapple = "🍍" case mango = "🥭" @@ -728,6 +756,7 @@ enum Emoji: String, CaseIterable, Equatable { case chestnut = "🌰" case gingerRoot = "🫚" case peaPod = "🫛" + case brownMushroom = "🍄‍🟫" case bread = "🍞" case croissant = "🥐" case baguetteBread = "🥖" @@ -1336,6 +1365,7 @@ enum Emoji: String, CaseIterable, Equatable { case scales = "⚖️" case probingCane = "🦯" case link = "🔗" + case brokenChain = "⛓️‍💥" case chains = "⛓️" case hook = "🪝" case toolbox = "🧰" @@ -1882,4 +1912,3 @@ enum Emoji: String, CaseIterable, Equatable { case flagScotland = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" case flagWales = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" } -// swiftlint:disable all diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift index e1b1c84daa..3572ff1249 100644 --- a/Session/Emoji/EmojiWithSkinTones+String.swift +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension EmojiWithSkinTones { init?(rawValue: String) { guard rawValue.isSingleEmoji else { return nil } @@ -75,16 +77,19 @@ extension EmojiWithSkinTones { case 1934: self = EmojiWithSkinTones.emojiFrom1934(rawValue) case 1935: self = EmojiWithSkinTones.emojiFrom1935(rawValue) case 1937: self = EmojiWithSkinTones.emojiFrom1937(rawValue) + case 2104: self = EmojiWithSkinTones.emojiFrom2104(rawValue) case 2109: self = EmojiWithSkinTones.emojiFrom2109(rawValue) case 2111: self = EmojiWithSkinTones.emojiFrom2111(rawValue) case 2112: self = EmojiWithSkinTones.emojiFrom2112(rawValue) case 2113: self = EmojiWithSkinTones.emojiFrom2113(rawValue) case 2116: self = EmojiWithSkinTones.emojiFrom2116(rawValue) case 2117: self = EmojiWithSkinTones.emojiFrom2117(rawValue) + case 2120: self = EmojiWithSkinTones.emojiFrom2120(rawValue) case 2123: self = EmojiWithSkinTones.emojiFrom2123(rawValue) case 2125: self = EmojiWithSkinTones.emojiFrom2125(rawValue) case 2126: self = EmojiWithSkinTones.emojiFrom2126(rawValue) case 2127: self = EmojiWithSkinTones.emojiFrom2127(rawValue) + case 2128: self = EmojiWithSkinTones.emojiFrom2128(rawValue) case 2129: self = EmojiWithSkinTones.emojiFrom2129(rawValue) case 2210: self = EmojiWithSkinTones.emojiFrom2210(rawValue) case 2549: self = EmojiWithSkinTones.emojiFrom2549(rawValue) @@ -105,8 +110,10 @@ extension EmojiWithSkinTones { case 2641: self = EmojiWithSkinTones.emojiFrom2641(rawValue) case 2642: self = EmojiWithSkinTones.emojiFrom2642(rawValue) case 2644: self = EmojiWithSkinTones.emojiFrom2644(rawValue) + case 2645: self = EmojiWithSkinTones.emojiFrom2645(rawValue) case 2646: self = EmojiWithSkinTones.emojiFrom2646(rawValue) case 2649: self = EmojiWithSkinTones.emojiFrom2649(rawValue) + case 2650: self = EmojiWithSkinTones.emojiFrom2650(rawValue) case 2655: self = EmojiWithSkinTones.emojiFrom2655(rawValue) case 2656: self = EmojiWithSkinTones.emojiFrom2656(rawValue) case 2657: self = EmojiWithSkinTones.emojiFrom2657(rawValue) @@ -117,6 +124,9 @@ extension EmojiWithSkinTones { case 2760: self = EmojiWithSkinTones.emojiFrom2760(rawValue) case 2761: self = EmojiWithSkinTones.emojiFrom2761(rawValue) case 2764: self = EmojiWithSkinTones.emojiFrom2764(rawValue) + case 2943: self = EmojiWithSkinTones.emojiFrom2943(rawValue) + case 2951: self = EmojiWithSkinTones.emojiFrom2951(rawValue) + case 2959: self = EmojiWithSkinTones.emojiFrom2959(rawValue) case 3289: self = EmojiWithSkinTones.emojiFrom3289(rawValue) case 3295: self = EmojiWithSkinTones.emojiFrom3295(rawValue) case 3389: self = EmojiWithSkinTones.emojiFrom3389(rawValue) @@ -126,12 +136,16 @@ extension EmojiWithSkinTones { case 3394: self = EmojiWithSkinTones.emojiFrom3394(rawValue) case 3396: self = EmojiWithSkinTones.emojiFrom3396(rawValue) case 3397: self = EmojiWithSkinTones.emojiFrom3397(rawValue) + case 3400: self = EmojiWithSkinTones.emojiFrom3400(rawValue) case 3403: self = EmojiWithSkinTones.emojiFrom3403(rawValue) case 3404: self = EmojiWithSkinTones.emojiFrom3404(rawValue) case 3405: self = EmojiWithSkinTones.emojiFrom3405(rawValue) case 3406: self = EmojiWithSkinTones.emojiFrom3406(rawValue) case 3407: self = EmojiWithSkinTones.emojiFrom3407(rawValue) + case 3408: self = EmojiWithSkinTones.emojiFrom3408(rawValue) case 3477: self = EmojiWithSkinTones.emojiFrom3477(rawValue) + case 3491: self = EmojiWithSkinTones.emojiFrom3491(rawValue) + case 3505: self = EmojiWithSkinTones.emojiFrom3505(rawValue) case 3921: self = EmojiWithSkinTones.emojiFrom3921(rawValue) case 3922: self = EmojiWithSkinTones.emojiFrom3922(rawValue) case 3924: self = EmojiWithSkinTones.emojiFrom3924(rawValue) @@ -149,9 +163,16 @@ extension EmojiWithSkinTones { case 3951: self = EmojiWithSkinTones.emojiFrom3951(rawValue) case 4007: self = EmojiWithSkinTones.emojiFrom4007(rawValue) case 4046: self = EmojiWithSkinTones.emojiFrom4046(rawValue) + case 4048: self = EmojiWithSkinTones.emojiFrom4048(rawValue) + case 4223: self = EmojiWithSkinTones.emojiFrom4223(rawValue) + case 4231: self = EmojiWithSkinTones.emojiFrom4231(rawValue) + case 4239: self = EmojiWithSkinTones.emojiFrom4239(rawValue) + case 4771: self = EmojiWithSkinTones.emojiFrom4771(rawValue) + case 4785: self = EmojiWithSkinTones.emojiFrom4785(rawValue) case 4840: self = EmojiWithSkinTones.emojiFrom4840(rawValue) case 5237: self = EmojiWithSkinTones.emojiFrom5237(rawValue) case 5370: self = EmojiWithSkinTones.emojiFrom5370(rawValue) + case 5425: self = EmojiWithSkinTones.emojiFrom5425(rawValue) case 6037: self = EmojiWithSkinTones.emojiFrom6037(rawValue) case 6065: self = EmojiWithSkinTones.emojiFrom6065(rawValue) case 6579: self = EmojiWithSkinTones.emojiFrom6579(rawValue) @@ -961,9 +982,9 @@ extension EmojiWithSkinTones { "👬": EmojiWithSkinTones(baseEmoji: .twoMenHoldingHands, skinTones: nil), "💏": EmojiWithSkinTones(baseEmoji: .personKissPerson, skinTones: nil), "💑": EmojiWithSkinTones(baseEmoji: .personHeartPerson, skinTones: nil), - "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "👤": EmojiWithSkinTones(baseEmoji: .bustInSilhouette, skinTones: nil), "👥": EmojiWithSkinTones(baseEmoji: .bustsInSilhouette, skinTones: nil), + "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "💐": EmojiWithSkinTones(baseEmoji: .bouquet, skinTones: nil), "💮": EmojiWithSkinTones(baseEmoji: .whiteFlower, skinTones: nil), "💒": EmojiWithSkinTones(baseEmoji: .wedding, skinTones: nil), @@ -1993,6 +2014,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2104(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🙂‍↔️": EmojiWithSkinTones(baseEmoji: .headShakingHorizontally, skinTones: nil), + "🙂‍↕️": EmojiWithSkinTones(baseEmoji: .headShakingVertically, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2109(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏃‍♂️": EmojiWithSkinTones(baseEmoji: .manRunning, skinTones: nil), @@ -2046,7 +2075,9 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: nil), "👩‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: nil), - "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil) + "🏃‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: nil), + "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil), + "⛓️‍💥": EmojiWithSkinTones(baseEmoji: .brokenChain, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -2084,6 +2115,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2120(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2123(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: nil), @@ -2159,6 +2197,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2128(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2129(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "❤️‍🩹": EmojiWithSkinTones(baseEmoji: .mendingHeart, skinTones: nil) @@ -3197,6 +3242,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2645(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🐦‍🔥": EmojiWithSkinTones(baseEmoji: .phoenix, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2646(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍🔧": EmojiWithSkinTones(baseEmoji: .maleMechanic, skinTones: nil), @@ -3219,6 +3271,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2650(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🍋‍🟩": EmojiWithSkinTones(baseEmoji: .lime, skinTones: nil), + "🍄‍🟫": EmojiWithSkinTones(baseEmoji: .brownMushroom, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2655(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🧑‍🎓": EmojiWithSkinTones(baseEmoji: .student, skinTones: nil), @@ -3293,7 +3353,8 @@ extension EmojiWithSkinTones { "🧑‍🦲": EmojiWithSkinTones(baseEmoji: .baldPerson, skinTones: nil), "🧑‍🦯": EmojiWithSkinTones(baseEmoji: .personWithProbingCane, skinTones: nil), "🧑‍🦼": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchair, skinTones: nil), - "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil) + "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil), + "🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChild, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3323,6 +3384,30 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2943(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: nil), + "🏃‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2951(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: nil), + "🚶‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2959(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: nil), + "🧎‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3289(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏳️‍🌈": EmojiWithSkinTones(baseEmoji: .rainbowFlag, skinTones: nil) @@ -3519,14 +3604,19 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.light]), "👩🏻‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.light]), + "🏃🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.light]), "👨🏼‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumLight]), "👩🏼‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumLight]), + "🏃🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumLight]), "👨🏽‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.medium]), "👩🏽‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.medium]), + "🏃🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.medium]), "👨🏾‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumDark]), "👩🏾‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumDark]), + "🏃🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumDark]), "👨🏿‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.dark]), - "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]) + "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]), + "🏃🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.dark]) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3659,6 +3749,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3400(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3403(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦🏻‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: [.light]), @@ -3914,6 +4015,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3408(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3477(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍👨": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: nil), @@ -3923,6 +4035,27 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3491(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: nil), + "👩‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: nil), + "👨‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: nil), + "👩‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: nil), + "👨‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: nil), + "👩‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom3505(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: nil), + "🧑‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: nil), + "🧑‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3921(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍🎓": EmojiWithSkinTones(baseEmoji: .maleStudent, skinTones: [.light]), @@ -4359,6 +4492,119 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom4048(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChild, skinTones: nil), + "🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4223(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.light]), + "🏃🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.light]), + "🏃🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.medium]), + "🏃🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.medium]), + "🏃🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.dark]), + "🏃🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4231(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.light]), + "🚶🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.medium]), + "🚶🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.dark]), + "🚶🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4239(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.light]), + "🧎🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.medium]), + "🧎🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.dark]), + "🧎🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4771(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.light]), + "👩🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.light]), + "👨🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👨🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.light]), + "👨🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.medium]), + "👩🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.medium]), + "👨🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👨🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.medium]), + "👨🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.dark]), + "👩🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.dark]), + "👨🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👨🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4785(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.light]), + "🧑🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.light]), + "🧑🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.light]), + "🧑🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.medium]), + "🧑🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "🧑🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.medium]), + "🧑🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.dark]), + "🧑🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "🧑🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom4840(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍💋‍👨": EmojiWithSkinTones(baseEmoji: .womanKissMan, skinTones: nil), @@ -4409,6 +4655,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom5425(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom6037(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩🏻‍❤️‍👨🏻": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: [.light]), @@ -4729,4 +4982,3 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } } -// swiftlint:disable all From 567bade1615ea781e2f202abe6a3b0e66d5200a6 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 14:14:30 +1000 Subject: [PATCH 221/244] Re-apply 'fix/SES-4573/dimiss_keyboard_on_tap_out' changes --- .../ConversationVC+Interaction.swift | 15 ++++++- Session/Conversations/ConversationVC.swift | 16 ++++++- .../Conversations/Input View/InputView.swift | 15 +++++++ .../Message Cells/CallMessageCell.swift | 20 ++++----- .../Message Cells/InfoMessageCell.swift | 22 +++++----- .../Message Cells/MessageCell.swift | 42 ++++++++++++++++++- .../Message Cells/VisibleMessageCell.swift | 32 ++++++-------- 7 files changed, 116 insertions(+), 46 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index da4ac06010..ad0541a19d 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -22,7 +22,8 @@ extension ConversationVC: ContextMenuActionDelegate, SendMediaNavDelegate, AttachmentApprovalViewControllerDelegate, - GifPickerViewControllerDelegate + GifPickerViewControllerDelegate, + UIGestureRecognizerDelegate { // MARK: - Open Settings @@ -33,6 +34,11 @@ extension ConversationVC: openSettingsFromTitleView() } + // Handle taps outside of tableview cell to dismiss keyboard + @MainActor @objc func dismissKeyboardOnTap() { + _ = self.snInputView.resignFirstResponder() + } + @MainActor func openSettingsFromTitleView() { // If we shouldn't be able to access settings then disable the title view shortcuts guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } @@ -254,6 +260,11 @@ extension ConversationVC: return true } + + // MARK: - UIGestureRecognizerDelegate + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } // MARK: - SendMediaNavDelegate @@ -1063,7 +1074,7 @@ extension ConversationVC: } // MARK: MessageCellDelegate - + func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the unblock modal if needed guard self.viewModel.threadData.threadIsBlocked != true else { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index fd52a451dc..0b13d35bbd 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -386,6 +386,16 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return result }() + + // Handle taps outside of tableview cell + private lazy var tableViewTapGesture: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer() + result.delegate = self + result.addTarget(self, action: #selector(dismissKeyboardOnTap)) + result.cancelsTouchesInView = false + + return result + }() // MARK: - Settings @@ -537,6 +547,9 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa object: nil ) } + + // Gesture + view.addGestureRecognizer(tableViewTapGesture) self.viewModel.navigatableState.setupBindings(viewController: self, disposables: &self.viewModel.disposables) @@ -1580,7 +1593,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // value will break things) let tableViewBottom: CGFloat = (tableView.contentSize.height - tableView.bounds.height + tableView.contentInset.bottom) - if tableView.contentOffset.y < (tableViewBottom - 5) { + // Added `insetDifference > 0` to remove sudden table collapse and overscroll + if tableView.contentOffset.y < (tableViewBottom - 5) && insetDifference > 0 { tableView.contentOffset.y += insetDifference } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 74316f9525..b75531a7e0 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -62,6 +62,15 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return result }() + private lazy var swipeGestureRecognizer: UISwipeGestureRecognizer = { + let result: UISwipeGestureRecognizer = UISwipeGestureRecognizer() + result.direction = .down + result.addTarget(self, action: #selector(didSwipeDown)) + result.cancelsTouchesInView = false + + return result + }() + private var bottomStackView: UIStackView? private lazy var attachmentsButton: ExpandingAttachmentsButton = { let result = ExpandingAttachmentsButton(delegate: delegate) @@ -227,6 +236,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M autoresizingMask = .flexibleHeight addGestureRecognizer(tapGestureRecognizer) + addGestureRecognizer(swipeGestureRecognizer) // Background & blur let backgroundView = UIView() @@ -454,6 +464,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self.accessibilityIdentifier = updatedInputState.accessibility?.identifier self.accessibilityLabel = updatedInputState.accessibility?.label tapGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes == .none) + inputState = updatedInputState disabledInputLabel.text = (updatedInputState.message ?? "") disabledInputLabel.accessibilityIdentifier = updatedInputState.messageAccessibility?.identifier @@ -630,6 +641,10 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M @objc private func characterLimitLabelTapped() { delegate?.handleCharacterLimitLabelTapped() } + + @objc private func didSwipeDown() { + inputTextView.resignFirstResponder() + } // MARK: - Convenience diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index de80fef7c1..47dca8d33b 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -18,6 +18,13 @@ final class CallMessageCell: MessageCell { override var contextSnapshotView: UIView? { return container } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var topConstraint: NSLayoutConstraint = mainStackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) @@ -115,15 +122,6 @@ final class CallMessageCell: MessageCell { mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset) } - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } - // MARK: - Updating override func update( @@ -207,7 +205,7 @@ final class CallMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -218,7 +216,7 @@ final class CallMessageCell: MessageCell { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 9c626a90c5..a5196a0b88 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -13,6 +13,13 @@ final class InfoMessageCell: MessageCell { override var contextSnapshotView: UIView? { return label } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var iconContainerViewWidthConstraint = iconContainerView.set(.width, to: InfoMessageCell.iconSize) @@ -77,15 +84,6 @@ final class InfoMessageCell: MessageCell { stackView.pin(.right, to: .right, of: self, withInset: -Values.massiveSpacing) stackView.pin(.bottom, to: .bottom, of: self, withInset: -InfoMessageCell.inset) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } // MARK: - Updating @@ -169,7 +167,7 @@ final class InfoMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -180,9 +178,9 @@ final class InfoMessageCell: MessageCell { isHandlingLongPress = true } - @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + if cellViewModel.variant == .infoDisappearingMessagesUpdate && cellViewModel.canDoFollowingSetting() { delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 39730a9d6c..ec40f8e72e 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -10,11 +10,16 @@ public enum SwipeState { case cancelled } +public enum GestureRecognizerType { + case tap, longPress, doubleTap +} + public class MessageCell: UITableViewCell { var dependencies: Dependencies? var viewModel: MessageViewModel? weak var delegate: MessageCellDelegate? open var contextSnapshotView: UIView? { return nil } + open var allowedGestureRecognizers: Set { return [] } // Override to have gestures // MARK: - Lifecycle @@ -41,7 +46,32 @@ public class MessageCell: UITableViewCell { } func setUpGestureRecognizers() { - // To be overridden by subclasses + var tapGestureRecognizer: UITapGestureRecognizer? + var doubleTapGestureRecognizer: UITapGestureRecognizer? + + if allowedGestureRecognizers.contains(.tap) { + let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) + tapGestureRecognizer = tapGesture + } + + if allowedGestureRecognizers.contains(.doubleTap) { + let doubleTapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGesture.numberOfTapsRequired = 2 + addGestureRecognizer(doubleTapGesture) + doubleTapGestureRecognizer = doubleTapGesture + } + + if allowedGestureRecognizers.contains(.longPress) { + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressGesture) + } + + // If we have both tap and double tap gestures then the single tap should fail if a double tap occurs + if let tapGesture: UITapGestureRecognizer = tapGestureRecognizer, let doubleTapGesture: UITapGestureRecognizer = doubleTapGestureRecognizer { + tapGesture.require(toFail: doubleTapGesture) + } } // MARK: - Updating @@ -93,6 +123,16 @@ public class MessageCell: UITableViewCell { return CallMessageCell.self } } + + // MARK: - Gesture events + @objc + func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {} + + @objc + func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {} + + @objc + func handleDoubleTap() {} } // MARK: - MessageCellDelegate diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 21a9517955..0dbde5e460 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -25,6 +25,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { override var contextSnapshotView: UIView? { return snContentView } + override var allowedGestureRecognizers: Set { + return [ + .tap, + .longPress, + .doubleTap + ] + } + // Constraints internal lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) @@ -270,21 +278,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - - let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGestureRecognizer.numberOfTapsRequired = 2 - addGestureRecognizer(doubleTapGestureRecognizer) - tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) - } - + // MARK: - Updating override func update( @@ -968,7 +962,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -994,9 +988,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + let location = gestureRecognizer.location(in: self) if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile { @@ -1062,7 +1056,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc private func handleDoubleTap() { + override func handleDoubleTap() { guard let cellViewModel: MessageViewModel = self.viewModel else { return } delegate?.handleItemDoubleTapped(cellViewModel) From 6385830eefa593066accfcc0325502cad168bbdf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 14:17:26 +1000 Subject: [PATCH 222/244] Re-apply 'feat/SES-3085/remove_authentication_on_default_rooms' changes --- .../Jobs/DisplayPictureDownloadJob.swift | 18 ++-- .../RetrieveDefaultOpenGroupRoomsJob.swift | 1 + ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 3 + SessionNetworkingKit/SOGS/SOGSAPI.swift | 42 +++++--- .../SOGS/SOGSAPISpec.swift | 97 +++++++++++++++++++ 5 files changed, 142 insertions(+), 19 deletions(-) diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index dac761d872..edae3c0108 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -48,7 +48,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { using: dependencies ) - case .community(let fileId, let roomToken, let server): + case .community(let fileId, let roomToken, let server, let skipAuthentication): guard let info: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) @@ -58,6 +58,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), + skipAuthentication: skipAuthentication, using: dependencies ) } @@ -206,7 +207,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { ) db.addConversationEvent(id: id, type: .updated(.displayPictureUrl(url))) - case .community(_, let roomToken, let server): + case .community(_, let roomToken, let server, _): _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: server)) .updateAllAndConfig( @@ -228,7 +229,7 @@ extension DisplayPictureDownloadJob { public enum Target: Codable, Hashable, CustomStringConvertible { case profile(id: String, url: String, encryptionKey: Data) case group(id: String, url: String, encryptionKey: Data) - case community(imageId: String, roomToken: String, server: String) + case community(imageId: String, roomToken: String, server: String, skipAuthentication: Bool = false) var isValid: Bool { switch self { @@ -239,7 +240,7 @@ extension DisplayPictureDownloadJob { encryptionKey.count == DisplayPictureManager.aes256KeyByteLength ) - case .community(let imageId, _, _): return !imageId.isEmpty + case .community(let imageId, _, _, _): return !imageId.isEmpty } } @@ -249,7 +250,7 @@ extension DisplayPictureDownloadJob { switch self { case .profile(let id, _, _): return "profile: \(id)" case .group(let id, _, _): return "group: \(id)" - case .community(_, let roomToken, let server): return "room: \(roomToken) on server: \(server)" + case .community(_, let roomToken, let server, _): return "room: \(roomToken) on server: \(server)" } } } @@ -274,11 +275,12 @@ extension DisplayPictureDownloadJob { self.target = { switch target { - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, let skipAuthentication): return .community( imageId: imageId, roomToken: roomToken, - server: server.lowercased() // Always in lowercase on `OpenGroup` + server: server.lowercased(), // Always in lowercase on `OpenGroup` + skipAuthentication: skipAuthentication ) default: return target @@ -358,7 +360,7 @@ extension DisplayPictureDownloadJob { return (url == latestDisplayPictureUrl) - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, _): guard let latestImageId: String = try? OpenGroup .select(.imageId) diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index c5dbbd97ef..e34c48694b 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -72,6 +72,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, Network.SOGS.CapabilitiesAndRoomsResponse), Error> in try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: authMethod, + skipAuthentication: true, using: dependencies ).send(using: dependencies) } diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 3ab3ae81bf..42ff8e59cc 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -265,6 +265,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ), forceBlinded: false ), + skipAuthentication: true, using: dependencies ) } @@ -286,6 +287,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) + + expect(expectedRequest?.headers).to(beEmpty()) } // MARK: -- will retry 8 times before it fails diff --git a/SessionNetworkingKit/SOGS/SOGSAPI.swift b/SessionNetworkingKit/SOGS/SOGSAPI.swift index 4898a6202c..981058ae32 100644 --- a/SessionNetworkingKit/SOGS/SOGSAPI.swift +++ b/SessionNetworkingKit/SOGS/SOGSAPI.swift @@ -156,9 +156,10 @@ public extension Network.SOGS { private static func preparedSequence( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( method: .post, endpoint: Endpoint.sequence, @@ -170,7 +171,10 @@ public extension Network.SOGS { using: dependencies ) - return (skipAuthentication ? preparedRequest : try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies)) + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Capabilities @@ -184,6 +188,7 @@ public extension Network.SOGS { /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` static func preparedCapabilities( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { let preparedRequest = try Network.PreparedRequest( @@ -196,7 +201,10 @@ public extension Network.SOGS { using: dependencies ) - return (skipAuthentication ? preparedRequest : try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies)) + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Room @@ -206,9 +214,10 @@ public extension Network.SOGS { /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included static func preparedRooms( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[Room]> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .rooms, authMethod: authMethod @@ -218,7 +227,10 @@ public extension Network.SOGS { using: dependencies ) - return (skipAuthentication ? preparedRequest : try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies)) + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } /// Returns the details of a single room @@ -320,6 +332,7 @@ public extension Network.SOGS { /// methods for the documented behaviour of each method static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { let preparedRequest = try Network.SOGS @@ -327,14 +340,17 @@ public extension Network.SOGS { requests: [ // Get the latest capabilities for the server (in case it's a new server or the // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies), - preparedRooms(authMethod: authMethod, using: dependencies) + preparedCapabilities(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies), + preparedRooms(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies) ], authMethod: authMethod, + skipAuthentication: skipAuthentication, using: dependencies ) - - let finalRequest = (skipAuthentication ? preparedRequest : try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies)) + let finalRequest = (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) return finalRequest .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in @@ -841,9 +857,10 @@ public extension Network.SOGS { fileId: String, roomToken: String, authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), authMethod: authMethod @@ -854,7 +871,10 @@ public extension Network.SOGS { using: dependencies ) - return (skipAuthentication ? preparedRequest : try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies)) + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Inbox/Outbox (Message Requests) diff --git a/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift index 877b6f31da..627199f818 100644 --- a/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift +++ b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift @@ -751,6 +751,44 @@ class SOGSAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).toNot(beEmpty()) + expect(preparedRequest?.headers).to(equal([ + HTTPHeader.sogsNonce: "pK6YRtQApl4NhECGizF0Cg==", + HTTPHeader.sogsTimestamp: "1234567890", + HTTPHeader.sogsSignature: "VGVzdFNvZ3NTaWduYXR1cmU=", + HTTPHeader.sogsPubKey: "1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" + ])) + } + + // MARK: ---- generates the request correctly and skips adding request headers + it("generates the request correctly and skips adding request headers") { + expect { + preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.batchEndpoints.count).to(equal(2)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + .to(equal(.capabilities)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + .to(equal(.rooms)) + + expect(preparedRequest?.path).to(equal("/sequence")) + expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).to(beEmpty()) } // MARK: ---- processes a valid response correctly @@ -1578,6 +1616,31 @@ class SOGSAPISpec: QuickSpec { ])) } + // MARK: ---- generates the download destination correctly when given an id and skips adding request headers + it("generates the download destination correctly when given an id and skips adding request headers") { + expect { + preparedRequest = try OpenGroupAPI.preparedDownload( + fileId: "1", + roomToken: "roomToken", + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.path).to(equal("/room/roomToken/file/1")) + expect(preparedRequest?.method.rawValue).to(equal("GET")) + expect(preparedRequest?.headers).to(beEmpty()) + } + // MARK: ---- generates the download request correctly when given a URL it("generates the download request correctly when given a URL") { expect { @@ -2203,6 +2266,40 @@ class SOGSAPISpec: QuickSpec { .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).toNot(beEmpty()) + + expect(response).toNot(beNil()) + expect(error).to(beNil()) + } + + // MARK: ---- triggers sending correctly without headers + it("triggers sending correctly without headers") { + var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? + + expect { + preparedRequest = try OpenGroupAPI.preparedRooms( + authMethod: Authentication.community( + info: LibSession.OpenGroupCapabilityInfo( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + capabilities: [] + ), + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + preparedRequest? + .send(using: dependencies) + .handleEvents(receiveOutput: { result in response = result }) + .mapError { error.setting(to: $0) } + .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).to(beEmpty()) expect(response).toNot(beNil()) expect(error).to(beNil()) From a7e137535e6dff3922b3c1558798e4ff81352cd9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Sep 2025 14:22:07 +1000 Subject: [PATCH 223/244] Fixed broken tests --- .../SOGS/SOGSAPISpec.swift | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift index 627199f818..342be00b57 100644 --- a/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift +++ b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift @@ -764,14 +764,13 @@ class SOGSAPISpec: QuickSpec { // MARK: ---- generates the request correctly and skips adding request headers it("generates the request correctly and skips adding request headers") { expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), skipAuthentication: true, @@ -780,9 +779,9 @@ class SOGSAPISpec: QuickSpec { }.toNot(throwError()) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.rooms)) expect(preparedRequest?.path).to(equal("/sequence")) @@ -1619,16 +1618,15 @@ class SOGSAPISpec: QuickSpec { // MARK: ---- generates the download destination correctly when given an id and skips adding request headers it("generates the download destination correctly when given an id and skips adding request headers") { expect { - preparedRequest = try OpenGroupAPI.preparedDownload( + preparedRequest = try Network.SOGS.preparedDownload( fileId: "1", roomToken: "roomToken", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), skipAuthentication: true, @@ -2275,17 +2273,16 @@ class SOGSAPISpec: QuickSpec { // MARK: ---- triggers sending correctly without headers it("triggers sending correctly without headers") { - var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? + var response: (info: ResponseInfoType, data: [Network.SOGS.Room])? expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), skipAuthentication: true, From da9d9254bccdfc652673ac343d0554d2e34e1c40 Mon Sep 17 00:00:00 2001 From: Bilb <1544279+Bilb@users.noreply.github.com> Date: Thu, 18 Sep 2025 08:09:33 +0000 Subject: [PATCH 224/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 244 +++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 31da116616..e1b245a269 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -84364,6 +84364,39 @@ } } }, + "checkingProStatus" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking {pro} Status" + } + } + } + }, + "checkingProStatusDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking your {pro} details. Some information on this page may be inaccurate until this check is complete." + } + } + } + }, + "checkingProStatusUpgradeDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking your {pro} status. You'll be able to upgrade to {pro} once this check is complete." + } + } + } + }, "clear" : { "extractionState" : "manual", "localizations" : { @@ -189073,6 +189106,17 @@ } } }, + "errorCheckingProStatus" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error checking {pro} status." + } + } + } + }, "errorConnection" : { "extractionState" : "manual", "localizations" : { @@ -190671,6 +190715,17 @@ } } }, + "errorLoadingProPlan" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error loading {pro} plan." + } + } + } + }, "errorUnknown" : { "extractionState" : "manual", "localizations" : { @@ -354919,7 +354974,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Hazırkı planınızda artıq\r\ntam {app_pro} qiymətinin {percent}% endirimi mövcuddur." + "value" : "Hazırkı planınızda artıq tam {app_pro} qiymətinin {percent}% endirimi mövcuddur." } }, "cs" : { @@ -354948,6 +355003,17 @@ } } }, + "proErrorRefreshingStatus" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error refreshing {pro} status" + } + } + } + }, "proExpired" : { "extractionState" : "manual", "localizations" : { @@ -362239,6 +362305,17 @@ } } }, + "proPlanError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Error" + } + } + } + }, "proPlanExpireDate" : { "extractionState" : "manual", "localizations" : { @@ -362274,6 +362351,50 @@ } } }, + "proPlanLoading" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Loading" + } + } + } + }, + "proPlanLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Information about your {pro} plan is still being loaded. You cannot update your plan until this process is complete." + } + } + } + }, + "proPlanLoadingEllipsis" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} plan loading..." + } + } + } + }, + "proPlanNetworkLoadError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to load your current plan. Updating your plan via {app_name} will be disabled until connectivity is restored.

    Please check your network connection and retry." + } + } + } + }, "proPlanNotFound" : { "extractionState" : "manual", "localizations" : { @@ -363248,6 +363369,28 @@ } } }, + "proStatsLoading" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Stats Loading" + } + } + } + }, + "proStatsLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} stats are loading, please wait." + } + } + } + }, "proStatsTooltip" : { "extractionState" : "manual", "localizations" : { @@ -363283,6 +363426,83 @@ } } }, + "proStatusError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Status Error" + } + } + } + }, + "proStatusInfoInaccurateNetworkError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to check your {pro} status. Information displayed on this page may be inaccurate until connectivity is restored.

    Please check your network connection and retry." + } + } + } + }, + "proStatusLoading" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Status Loading" + } + } + } + }, + "proStatusLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} information is being loaded. Some actions on this page may be unavailable until loading is complete." + } + } + } + }, + "proStatusLoadingSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} status loading" + } + } + } + }, + "proStatusNetworkErrorDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to check your {pro} status. You cannot upgrade to {pro} until connectivity is restored.

    Please check your network connection and retry." + } + } + } + }, + "proStatusRefreshNetworkError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to refresh your {pro} status. Some actions on this page will be disabled until connectivity is restored.

    Please check your network connection and retry." + } + } + } + }, "proSupportDescription" : { "extractionState" : "manual", "localizations" : { @@ -415000,6 +415220,17 @@ } } }, + "unsupportedCpu" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unsupported CPU" + } + } + } + }, "updateApp" : { "extractionState" : "manual", "localizations" : { @@ -428707,6 +428938,17 @@ } } }, + "yourCpuIsUnsupportedSSE42" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your CPU does not support SSE 4.2, which is required for Session to run on Linux x64. Please upgrade your CPU or use a different operating system." + } + } + } + }, "yourRecoveryPassword" : { "extractionState" : "manual", "localizations" : { From 6ed6aec702fe7ccf9b938be5bbc347b90d303945 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Sep 2025 10:58:13 +1000 Subject: [PATCH 225/244] Fixed a couple of layout issues found by the automated tests --- Session/Settings/Views/NewTagView.swift | 2 +- Session/Shared/Views/SessionCell+AccessoryView.swift | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Session/Settings/Views/NewTagView.swift b/Session/Settings/Views/NewTagView.swift index 5156a4885a..416f0ae49b 100644 --- a/Session/Settings/Views/NewTagView.swift +++ b/Session/Settings/Views/NewTagView.swift @@ -34,7 +34,7 @@ final class NewTagView: UIView { private func setupUI() { addSubview(newTagLabel) - newTagLabel.pin(.leading, to: .leading, of: self, withInset: -(Values.mediumSpacing + Values.verySmallSpacing)) + newTagLabel.pin(.leading, to: .leading, of: self, withInset: -Values.mediumSmallSpacing) newTagLabel.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.trailing ], to: self) } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 3b09865e9f..ef3e66cf9e 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -782,10 +782,7 @@ extension SessionCell { minWidthConstraint.isActive = true } - view.pin(.top, to: .top, of: self) - view.pin(.bottom, to: .bottom, of: self) - view.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) - view.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) + view.pin(to: self) } private func configureCustomView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.AnyCustom) { From 6895c147cab8e75be910bf7e8c6a1c8d51b249a3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Sep 2025 11:03:03 +1000 Subject: [PATCH 226/244] Bumped build number --- Session.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4982853375..2ab6a4f486 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1040,11 +1040,11 @@ FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */; }; FDE521A62E0E6C8C00061B8E /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */; }; FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */; }; FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */ = {isa = PBXBuildFile; fileRef = FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */; }; FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */; }; - FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; @@ -2310,11 +2310,11 @@ FDE5219F2E0D22FD00061B8E /* ObservationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationManager.swift; sourceTree = ""; }; FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionMessagingKit.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsGroupsViewModel.swift; sourceTree = ""; }; FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsProViewModel.swift; sourceTree = ""; }; FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Session - Anonymous Messenger.storekit"; sourceTree = ""; }; FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; @@ -8320,7 +8320,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 632; + CURRENT_PROJECT_VERSION = 633; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8401,7 +8401,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 632; + CURRENT_PROJECT_VERSION = 633; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8882,7 +8882,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 632; + CURRENT_PROJECT_VERSION = 633; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9469,7 +9469,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 632; + CURRENT_PROJECT_VERSION = 633; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From fc7fc4789ae65f3f0788daf10239a2a46f7b17f7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Sep 2025 11:11:26 +1000 Subject: [PATCH 227/244] Fixed a bug where deleting a conversation would delete the contact --- Session/Utilities/UIContextualAction+Utilities.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 7e30326c7e..c0edbc3b20 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -692,7 +692,11 @@ public extension UIContextualAction { return .deleteGroupAndContent case (.group, _, _): return .leaveGroupAsync - case (.contact, _, _): return .deleteContactConversationAndContact + case (.contact, true, _): + return .deleteContactConversationAndContact + + case (.contact, false, _): + return .deleteContactConversationAndMarkHidden } }() From 0ca839995e79bbce32d60909f7c8f555f7dea434 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Sep 2025 16:47:13 +1000 Subject: [PATCH 228/244] Updated "profileLastUpdated" logic to be consistent with other platforms --- .../Database/Models/Profile.swift | 2 +- .../Jobs/DisplayPictureDownloadJob.swift | 17 +- .../Config Handling/LibSession+Shared.swift | 2 +- .../VisibleMessage+Profile.swift | 20 +- .../Protos/Generated/SNProto.swift | 16 +- .../Protos/Generated/SessionProtos.pb.swift | 512 ++++++++++++++---- .../Generated/WebSocketResources.pb.swift | 48 +- .../Protos/SessionProtos.proto | 2 +- .../MessageReceiver+Groups.swift | 9 +- .../MessageReceiver+MessageRequests.swift | 5 +- .../MessageReceiver+VisibleMessages.swift | 3 +- .../Sending & Receiving/MessageSender.swift | 6 +- .../Utilities/Profile+Updating.swift | 40 +- 13 files changed, 528 insertions(+), 154 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 87824331db..d9dd41f57e 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -169,7 +169,7 @@ public extension Profile { } if let profileLastUpdated: TimeInterval = profileLastUpdated { - profileProto.setProfileUpdateTimestamp(UInt64(profileLastUpdated)) + profileProto.setLastUpdateSeconds(UInt64(profileLastUpdated)) } do { diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index bc3674d515..b687c07f66 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -264,7 +264,7 @@ extension DisplayPictureDownloadJob { public struct Details: Codable, Hashable { public let target: Target - public let timestamp: TimeInterval + public let timestamp: TimeInterval? // MARK: - Hashable @@ -277,7 +277,7 @@ extension DisplayPictureDownloadJob { // MARK: - Initialization - public init?(target: Target, timestamp: TimeInterval) { + public init?(target: Target, timestamp: TimeInterval?) { guard target.isValid else { return nil } self.target = { @@ -348,11 +348,16 @@ extension DisplayPictureDownloadJob { case .profile(let id, let url, let encryptionKey): guard let latestProfile: Profile = try? Profile.fetchOne(db, id: id) else { return false } + /// If the data matches what is stored in the database then we should be fine to consider it valid (it may be that + /// we are re-downloading a profile due to some invalid state) + let dataMatches: Bool = ( + encryptionKey == latestProfile.displayPictureEncryptionKey && + url == latestProfile.displayPictureUrl + ) + return ( - timestamp >= (latestProfile.profileLastUpdated ?? 0) || ( - encryptionKey == latestProfile.displayPictureEncryptionKey && - url == latestProfile.displayPictureUrl - ) + Profile.shouldUpdateProfile(timestamp, profile: latestProfile, using: dependencies) || + dataMatches ) case .group(let id, let url,_): diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index d192c833aa..2d69301415 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -761,7 +761,7 @@ public extension LibSession.Cache { let displayNameInMessage: String? = (visibleMessage?.sender != contactId ? nil : visibleMessage?.profile?.displayName?.nullIfEmpty ) - let profileLastUpdatedInMessage: TimeInterval? = visibleMessage?.profile?.updateTimestampMs.map { TimeInterval($0 / 1000) } + let profileLastUpdatedInMessage: TimeInterval? = visibleMessage?.profile?.updateTimestampSeconds let fallbackProfile: Profile? = displayNameInMessage.map { Profile(id: contactId, name: $0) } guard var cContactId: [CChar] = contactId.cString(using: .utf8) else { diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index f7a521c937..d3eccefb6f 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -10,7 +10,7 @@ public extension VisibleMessage { public let displayName: String? public let profileKey: Data? public let profilePictureUrl: String? - public let updateTimestampMs: UInt64? + public let updateTimestampSeconds: TimeInterval? public let blocksCommunityMessageRequests: Bool? public let sessionProProof: String? @@ -20,7 +20,7 @@ public extension VisibleMessage { displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil, - updateTimestampMs: UInt64? = nil, + updateTimestampSeconds: TimeInterval? = nil, blocksCommunityMessageRequests: Bool? = nil, sessionProProof: String? = nil ) { @@ -29,7 +29,7 @@ public extension VisibleMessage { self.displayName = displayName self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) - self.updateTimestampMs = updateTimestampMs + self.updateTimestampSeconds = updateTimestampSeconds self.blocksCommunityMessageRequests = blocksCommunityMessageRequests self.sessionProProof = sessionProProof } @@ -46,7 +46,7 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, - updateTimestampMs: profileProto.profileUpdateTimestamp, + updateTimestampSeconds: TimeInterval(profileProto.lastUpdateSeconds), blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) @@ -68,8 +68,8 @@ public extension VisibleMessage { profileProto.setProfilePicture(profilePictureUrl) } - if let updateTimestampMs: UInt64 = updateTimestampMs { - profileProto.setProfileUpdateTimestamp(updateTimestampMs) + if let updateTimestampSeconds: TimeInterval = updateTimestampSeconds { + profileProto.setLastUpdateSeconds(UInt64(updateTimestampSeconds)) } dataMessageProto.setProfile(try profileProto.build()) @@ -100,7 +100,7 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, - updateTimestampMs: profileProto.profileUpdateTimestamp, + updateTimestampSeconds: TimeInterval(profileProto.lastUpdateSeconds), sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -120,8 +120,8 @@ public extension VisibleMessage { messageRequestResponseProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) } - if let updateTimestampMs: UInt64 = updateTimestampMs { - profileProto.setProfileUpdateTimestamp(updateTimestampMs) + if let updateTimestampSeconds: TimeInterval = updateTimestampSeconds { + profileProto.setLastUpdateSeconds(UInt64(updateTimestampSeconds)) } do { messageRequestResponseProto.setProfile(try profileProto.build()) @@ -140,7 +140,7 @@ public extension VisibleMessage { displayName: \(displayName ?? "null"), profileKey: \(profileKey?.description ?? "null"), profilePictureUrl: \(profilePictureUrl ?? "null"), - profileUpdateTimestamp: \(updateTimestampMs ?? 0) + UpdateTimestampSeconds: \(updateTimestampSeconds ?? 0) ) """ } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 2419037f94..633e60c3a7 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -1296,8 +1296,8 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui if let _value = profilePicture { builder.setProfilePicture(_value) } - if hasProfileUpdateTimestamp { - builder.setProfileUpdateTimestamp(profileUpdateTimestamp) + if hasLastUpdateSeconds { + builder.setLastUpdateSeconds(lastUpdateSeconds) } return builder } @@ -1316,8 +1316,8 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui proto.profilePicture = valueParam } - @objc public func setProfileUpdateTimestamp(_ valueParam: UInt64) { - proto.profileUpdateTimestamp = valueParam + @objc public func setLastUpdateSeconds(_ valueParam: UInt64) { + proto.lastUpdateSeconds = valueParam } @objc public func build() throws -> SNProtoLokiProfile { @@ -1351,11 +1351,11 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui return proto.hasProfilePicture } - @objc public var profileUpdateTimestamp: UInt64 { - return proto.profileUpdateTimestamp + @objc public var lastUpdateSeconds: UInt64 { + return proto.lastUpdateSeconds } - @objc public var hasProfileUpdateTimestamp: Bool { - return proto.hasProfileUpdateTimestamp + @objc public var hasLastUpdateSeconds: Bool { + return proto.hasLastUpdateSeconds } private init(proto: SessionProtos_LokiProfile) { diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 01530127a9..40f49dead2 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -1,6 +1,5 @@ // DO NOT EDIT. // swift-format-ignore-file -// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: SessionProtos.proto @@ -23,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct SessionProtos_Envelope: @unchecked Sendable { +struct SessionProtos_Envelope { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -86,14 +85,30 @@ struct SessionProtos_Envelope: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case sessionMessage = 6 - case closedGroupMessage = 7 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case sessionMessage // = 6 + case closedGroupMessage // = 7 init() { self = .sessionMessage } + init?(rawValue: Int) { + switch rawValue { + case 6: self = .sessionMessage + case 7: self = .closedGroupMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .sessionMessage: return 6 + case .closedGroupMessage: return 7 + } + } + } init() {} @@ -106,7 +121,15 @@ struct SessionProtos_Envelope: @unchecked Sendable { fileprivate var _serverTimestamp: UInt64? = nil } -struct SessionProtos_TypingMessage: Sendable { +#if swift(>=4.2) + +extension SessionProtos_Envelope.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_TypingMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -133,14 +156,30 @@ struct SessionProtos_TypingMessage: Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Action: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case started = 0 - case stopped = 1 + enum Action: SwiftProtobuf.Enum { + typealias RawValue = Int + case started // = 0 + case stopped // = 1 init() { self = .started } + init?(rawValue: Int) { + switch rawValue { + case 0: self = .started + case 1: self = .stopped + default: return nil + } + } + + var rawValue: Int { + switch self { + case .started: return 0 + case .stopped: return 1 + } + } + } init() {} @@ -149,7 +188,15 @@ struct SessionProtos_TypingMessage: Sendable { fileprivate var _action: SessionProtos_TypingMessage.Action? = nil } -struct SessionProtos_UnsendRequest: Sendable { +#if swift(>=4.2) + +extension SessionProtos_TypingMessage.Action: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_UnsendRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -182,7 +229,7 @@ struct SessionProtos_UnsendRequest: Sendable { fileprivate var _author: String? = nil } -struct SessionProtos_MessageRequestResponse: @unchecked Sendable { +struct SessionProtos_MessageRequestResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -224,7 +271,7 @@ struct SessionProtos_MessageRequestResponse: @unchecked Sendable { fileprivate var _profile: SessionProtos_LokiProfile? = nil } -struct SessionProtos_Content: @unchecked Sendable { +struct SessionProtos_Content { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -321,15 +368,33 @@ struct SessionProtos_Content: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum ExpirationType: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case unknown = 0 - case deleteAfterRead = 1 - case deleteAfterSend = 2 + enum ExpirationType: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case deleteAfterRead // = 1 + case deleteAfterSend // = 2 init() { self = .unknown } + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .deleteAfterRead + case 2: self = .deleteAfterSend + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .deleteAfterRead: return 1 + case .deleteAfterSend: return 2 + } + } + } init() {} @@ -337,7 +402,15 @@ struct SessionProtos_Content: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -struct SessionProtos_CallMessage: Sendable { +#if swift(>=4.2) + +extension SessionProtos_Content.ExpirationType: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_CallMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -370,18 +443,42 @@ struct SessionProtos_CallMessage: Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case preOffer = 6 - case offer = 1 - case answer = 2 - case provisionalAnswer = 3 - case iceCandidates = 4 - case endCall = 5 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case preOffer // = 6 + case offer // = 1 + case answer // = 2 + case provisionalAnswer // = 3 + case iceCandidates // = 4 + case endCall // = 5 init() { self = .preOffer } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .offer + case 2: self = .answer + case 3: self = .provisionalAnswer + case 4: self = .iceCandidates + case 5: self = .endCall + case 6: self = .preOffer + default: return nil + } + } + + var rawValue: Int { + switch self { + case .offer: return 1 + case .answer: return 2 + case .provisionalAnswer: return 3 + case .iceCandidates: return 4 + case .endCall: return 5 + case .preOffer: return 6 + } + } + } init() {} @@ -390,7 +487,15 @@ struct SessionProtos_CallMessage: Sendable { fileprivate var _uuid: String? = nil } -struct SessionProtos_KeyPair: @unchecked Sendable { +#if swift(>=4.2) + +extension SessionProtos_CallMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_KeyPair { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -423,7 +528,7 @@ struct SessionProtos_KeyPair: @unchecked Sendable { fileprivate var _privateKey: Data? = nil } -struct SessionProtos_DataExtractionNotification: Sendable { +struct SessionProtos_DataExtractionNotification { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -449,16 +554,32 @@ struct SessionProtos_DataExtractionNotification: Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case screenshot = 1 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case screenshot // = 1 /// timestamp - case mediaSaved = 2 + case mediaSaved // = 2 init() { self = .screenshot } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .screenshot + case 2: self = .mediaSaved + default: return nil + } + } + + var rawValue: Int { + switch self { + case .screenshot: return 1 + case .mediaSaved: return 2 + } + } + } init() {} @@ -467,7 +588,15 @@ struct SessionProtos_DataExtractionNotification: Sendable { fileprivate var _timestamp: UInt64? = nil } -struct SessionProtos_LokiProfile: Sendable { +#if swift(>=4.2) + +extension SessionProtos_DataExtractionNotification.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_LokiProfile { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -491,14 +620,14 @@ struct SessionProtos_LokiProfile: Sendable { mutating func clearProfilePicture() {self._profilePicture = nil} /// Timestamp of the last profile update - var profileUpdateTimestamp: UInt64 { - get {return _profileUpdateTimestamp ?? 0} - set {_profileUpdateTimestamp = newValue} + var lastUpdateSeconds: UInt64 { + get {return _lastUpdateSeconds ?? 0} + set {_lastUpdateSeconds = newValue} } - /// Returns true if `profileUpdateTimestamp` has been explicitly set. - var hasProfileUpdateTimestamp: Bool {return self._profileUpdateTimestamp != nil} - /// Clears the value of `profileUpdateTimestamp`. Subsequent reads from it will return its default value. - mutating func clearProfileUpdateTimestamp() {self._profileUpdateTimestamp = nil} + /// Returns true if `lastUpdateSeconds` has been explicitly set. + var hasLastUpdateSeconds: Bool {return self._lastUpdateSeconds != nil} + /// Clears the value of `lastUpdateSeconds`. Subsequent reads from it will return its default value. + mutating func clearLastUpdateSeconds() {self._lastUpdateSeconds = nil} var unknownFields = SwiftProtobuf.UnknownStorage() @@ -506,10 +635,10 @@ struct SessionProtos_LokiProfile: Sendable { fileprivate var _displayName: String? = nil fileprivate var _profilePicture: String? = nil - fileprivate var _profileUpdateTimestamp: UInt64? = nil + fileprivate var _lastUpdateSeconds: UInt64? = nil } -struct SessionProtos_DataMessage: @unchecked Sendable { +struct SessionProtos_DataMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -626,16 +755,30 @@ struct SessionProtos_DataMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case expirationTimerUpdate = 2 + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case expirationTimerUpdate // = 2 init() { self = .expirationTimerUpdate } + init?(rawValue: Int) { + switch rawValue { + case 2: self = .expirationTimerUpdate + default: return nil + } + } + + var rawValue: Int { + switch self { + case .expirationTimerUpdate: return 2 + } + } + } - struct Quote: Sendable { + struct Quote { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -673,7 +816,7 @@ struct SessionProtos_DataMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - struct QuotedAttachment: Sendable { + struct QuotedAttachment { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -716,13 +859,27 @@ struct SessionProtos_DataMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case voiceMessage = 1 + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case voiceMessage // = 1 init() { self = .voiceMessage } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .voiceMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .voiceMessage: return 1 + } + } + } init() {} @@ -740,7 +897,7 @@ struct SessionProtos_DataMessage: @unchecked Sendable { fileprivate var _text: String? = nil } - struct Preview: Sendable { + struct Preview { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -782,7 +939,7 @@ struct SessionProtos_DataMessage: @unchecked Sendable { fileprivate var _image: SessionProtos_AttachmentPointer? = nil } - struct Reaction: Sendable { + struct Reaction { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -828,14 +985,30 @@ struct SessionProtos_DataMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Action: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case react = 0 - case remove = 1 + enum Action: SwiftProtobuf.Enum { + typealias RawValue = Int + case react // = 0 + case remove // = 1 init() { self = .react } + init?(rawValue: Int) { + switch rawValue { + case 0: self = .react + case 1: self = .remove + default: return nil + } + } + + var rawValue: Int { + switch self { + case .react: return 0 + case .remove: return 1 + } + } + } init() {} @@ -846,7 +1019,7 @@ struct SessionProtos_DataMessage: @unchecked Sendable { fileprivate var _action: SessionProtos_DataMessage.Reaction.Action? = nil } - struct OpenGroupInvitation: Sendable { + struct OpenGroupInvitation { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -884,7 +1057,23 @@ struct SessionProtos_DataMessage: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -struct SessionProtos_ReceiptMessage: Sendable { +#if swift(>=4.2) + +extension SessionProtos_DataMessage.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +extension SessionProtos_DataMessage.Reaction.Action: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_ReceiptMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -903,14 +1092,30 @@ struct SessionProtos_ReceiptMessage: Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case delivery = 0 - case read = 1 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case delivery // = 0 + case read // = 1 init() { self = .delivery } + init?(rawValue: Int) { + switch rawValue { + case 0: self = .delivery + case 1: self = .read + default: return nil + } + } + + var rawValue: Int { + switch self { + case .delivery: return 0 + case .read: return 1 + } + } + } init() {} @@ -918,7 +1123,15 @@ struct SessionProtos_ReceiptMessage: Sendable { fileprivate var _type: SessionProtos_ReceiptMessage.TypeEnum? = nil } -struct SessionProtos_AttachmentPointer: @unchecked Sendable { +#if swift(>=4.2) + +extension SessionProtos_ReceiptMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_AttachmentPointer { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1034,13 +1247,27 @@ struct SessionProtos_AttachmentPointer: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum Flags: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case voiceMessage = 1 + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case voiceMessage // = 1 init() { self = .voiceMessage } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .voiceMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .voiceMessage: return 1 + } + } + } init() {} @@ -1059,7 +1286,15 @@ struct SessionProtos_AttachmentPointer: @unchecked Sendable { fileprivate var _url: String? = nil } -struct SessionProtos_GroupUpdateMessage: @unchecked Sendable { +#if swift(>=4.2) + +extension SessionProtos_AttachmentPointer.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_GroupUpdateMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1143,7 +1378,7 @@ struct SessionProtos_GroupUpdateMessage: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -struct SessionProtos_GroupUpdateInviteMessage: @unchecked Sendable { +struct SessionProtos_GroupUpdateInviteMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1198,7 +1433,7 @@ struct SessionProtos_GroupUpdateInviteMessage: @unchecked Sendable { fileprivate var _adminSignature: Data? = nil } -struct SessionProtos_GroupUpdatePromoteMessage: @unchecked Sendable { +struct SessionProtos_GroupUpdatePromoteMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1231,7 +1466,7 @@ struct SessionProtos_GroupUpdatePromoteMessage: @unchecked Sendable { fileprivate var _name: String? = nil } -struct SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable { +struct SessionProtos_GroupUpdateInfoChangeMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1276,15 +1511,33 @@ struct SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case name = 1 - case avatar = 2 - case disappearingMessages = 3 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case name // = 1 + case avatar // = 2 + case disappearingMessages // = 3 init() { self = .name } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .name + case 2: self = .avatar + case 3: self = .disappearingMessages + default: return nil + } + } + + var rawValue: Int { + switch self { + case .name: return 1 + case .avatar: return 2 + case .disappearingMessages: return 3 + } + } + } init() {} @@ -1295,7 +1548,15 @@ struct SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable { fileprivate var _adminSignature: Data? = nil } -struct SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable { +#if swift(>=4.2) + +extension SessionProtos_GroupUpdateInfoChangeMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SessionProtos_GroupUpdateMemberChangeMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1333,15 +1594,33 @@ struct SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case added = 1 - case removed = 2 - case promoted = 3 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case added // = 1 + case removed // = 2 + case promoted // = 3 init() { self = .added } + init?(rawValue: Int) { + switch rawValue { + case 1: self = .added + case 2: self = .removed + case 3: self = .promoted + default: return nil + } + } + + var rawValue: Int { + switch self { + case .added: return 1 + case .removed: return 2 + case .promoted: return 3 + } + } + } init() {} @@ -1351,8 +1630,16 @@ struct SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable { fileprivate var _adminSignature: Data? = nil } +#if swift(>=4.2) + +extension SessionProtos_GroupUpdateMemberChangeMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + /// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop) -struct SessionProtos_GroupUpdateMemberLeftMessage: Sendable { +struct SessionProtos_GroupUpdateMemberLeftMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1363,7 +1650,7 @@ struct SessionProtos_GroupUpdateMemberLeftMessage: Sendable { } /// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop) -struct SessionProtos_GroupUpdateMemberLeftNotificationMessage: Sendable { +struct SessionProtos_GroupUpdateMemberLeftNotificationMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1373,7 +1660,7 @@ struct SessionProtos_GroupUpdateMemberLeftNotificationMessage: Sendable { init() {} } -struct SessionProtos_GroupUpdateInviteResponseMessage: Sendable { +struct SessionProtos_GroupUpdateInviteResponseMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1395,7 +1682,7 @@ struct SessionProtos_GroupUpdateInviteResponseMessage: Sendable { fileprivate var _isApproved: Bool? = nil } -struct SessionProtos_GroupUpdateDeleteMemberContentMessage: @unchecked Sendable { +struct SessionProtos_GroupUpdateDeleteMemberContentMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1420,6 +1707,47 @@ struct SessionProtos_GroupUpdateDeleteMemberContentMessage: @unchecked Sendable fileprivate var _adminSignature: Data? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension SessionProtos_Envelope: @unchecked Sendable {} +extension SessionProtos_Envelope.TypeEnum: @unchecked Sendable {} +extension SessionProtos_TypingMessage: @unchecked Sendable {} +extension SessionProtos_TypingMessage.Action: @unchecked Sendable {} +extension SessionProtos_UnsendRequest: @unchecked Sendable {} +extension SessionProtos_MessageRequestResponse: @unchecked Sendable {} +extension SessionProtos_Content: @unchecked Sendable {} +extension SessionProtos_Content.ExpirationType: @unchecked Sendable {} +extension SessionProtos_CallMessage: @unchecked Sendable {} +extension SessionProtos_CallMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_KeyPair: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification.TypeEnum: @unchecked Sendable {} +extension SessionProtos_LokiProfile: @unchecked Sendable {} +extension SessionProtos_DataMessage: @unchecked Sendable {} +extension SessionProtos_DataMessage.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Preview: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction.Action: @unchecked Sendable {} +extension SessionProtos_DataMessage.OpenGroupInvitation: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer.Flags: @unchecked Sendable {} +extension SessionProtos_GroupUpdateMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateInviteMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdatePromoteMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateInfoChangeMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateInfoChangeMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_GroupUpdateMemberChangeMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateMemberChangeMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_GroupUpdateMemberLeftMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateMemberLeftNotificationMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateInviteResponseMessage: @unchecked Sendable {} +extension SessionProtos_GroupUpdateDeleteMemberContentMessage: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "SessionProtos" @@ -1686,11 +2014,7 @@ extension SessionProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageIm var _expirationTimer: UInt32? = nil var _sigTimestamp: UInt64? = nil - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() + static let defaultInstance = _StorageClass() private init() {} @@ -2008,7 +2332,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "displayName"), 2: .same(proto: "profilePicture"), - 3: .same(proto: "profileUpdateTimestamp"), + 3: .same(proto: "lastUpdateSeconds"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2019,7 +2343,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self._displayName) }() case 2: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }() - case 3: try { try decoder.decodeSingularUInt64Field(value: &self._profileUpdateTimestamp) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self._lastUpdateSeconds) }() default: break } } @@ -2036,7 +2360,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = self._profilePicture { try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() - try { if let v = self._profileUpdateTimestamp { + try { if let v = self._lastUpdateSeconds { try visitor.visitSingularUInt64Field(value: v, fieldNumber: 3) } }() try unknownFields.traverse(visitor: &visitor) @@ -2045,7 +2369,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa static func ==(lhs: SessionProtos_LokiProfile, rhs: SessionProtos_LokiProfile) -> Bool { if lhs._displayName != rhs._displayName {return false} if lhs._profilePicture != rhs._profilePicture {return false} - if lhs._profileUpdateTimestamp != rhs._profileUpdateTimestamp {return false} + if lhs._lastUpdateSeconds != rhs._lastUpdateSeconds {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2084,11 +2408,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _blocksCommunityMessageRequests: Bool? = nil var _groupUpdateMessage: SessionProtos_GroupUpdateMessage? = nil - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() + static let defaultInstance = _StorageClass() private init() {} @@ -2723,11 +3043,7 @@ extension SessionProtos_GroupUpdateMessage: SwiftProtobuf.Message, SwiftProtobuf var _deleteMemberContent: SessionProtos_GroupUpdateDeleteMemberContentMessage? = nil var _memberLeftNotificationMessage: SessionProtos_GroupUpdateMemberLeftNotificationMessage? = nil - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() + static let defaultInstance = _StorageClass() private init() {} @@ -3090,8 +3406,8 @@ extension SessionProtos_GroupUpdateMemberLeftMessage: SwiftProtobuf.Message, Swi static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } func traverse(visitor: inout V) throws { @@ -3109,8 +3425,8 @@ extension SessionProtos_GroupUpdateMemberLeftNotificationMessage: SwiftProtobuf. static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } func traverse(visitor: inout V) throws { diff --git a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift index 56b33e00e2..2fe1650440 100644 --- a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift +++ b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift @@ -1,6 +1,5 @@ // DO NOT EDIT. // swift-format-ignore-file -// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: WebSocketResources.proto @@ -29,7 +28,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable { +struct WebSocketProtos_WebSocketRequestMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -85,7 +84,7 @@ struct WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable { fileprivate var _requestID: UInt64? = nil } -struct WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable { +struct WebSocketProtos_WebSocketResponseMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -140,7 +139,7 @@ struct WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable { fileprivate var _body: Data? = nil } -struct WebSocketProtos_WebSocketMessage: Sendable { +struct WebSocketProtos_WebSocketMessage { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -175,15 +174,33 @@ struct WebSocketProtos_WebSocketMessage: Sendable { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: Int, SwiftProtobuf.Enum, Swift.CaseIterable { - case unknown = 0 - case request = 1 - case response = 2 + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case request // = 1 + case response // = 2 init() { self = .unknown } + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .request + case 2: self = .response + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .request: return 1 + case .response: return 2 + } + } + } init() {} @@ -193,6 +210,21 @@ struct WebSocketProtos_WebSocketMessage: Sendable { fileprivate var _response: WebSocketProtos_WebSocketResponseMessage? = nil } +#if swift(>=4.2) + +extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "WebSocketProtos" diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 74e42824b5..a25e477759 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -116,7 +116,7 @@ message LokiProfile { optional string displayName = 1; optional string profilePicture = 2; - optional uint64 profileUpdateTimestamp = 3; // Timestamp of the last profile update + optional uint64 lastUpdateSeconds = 3; // Timestamp of the last profile update } message DataMessage { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index c4787f2caf..4de5e3bcb9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -140,14 +140,13 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - profileUpdateTimestamp: profileUpdateTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -245,14 +244,13 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - profileUpdateTimestamp: profileUpdateTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -607,14 +605,13 @@ extension MessageReceiver { // Update profile if needed if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? sentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - profileUpdateTimestamp: profileUpdateTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index c1e8c81b10..aa966d9d39 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -26,15 +26,12 @@ extension MessageReceiver { // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { - let messageSentTimestampMs: UInt64 = message.sentTimestampMs ?? 0 - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? messageSentTimestampMs) / 1000) - try Profile.updateIfNeeded( db, publicKey: senderId, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), - profileUpdateTimestamp: profileUpdateTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index e55716844a..430b38e325 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -38,14 +38,13 @@ extension MessageReceiver { // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { - let profileUpdateTimestamp: TimeInterval = TimeInterval(Double(profile.updateTimestampMs ?? messageSentTimestampMs) / 1000) try Profile.updateIfNeeded( db, publicKey: sender, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - profileUpdateTimestamp: profileUpdateTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 950cb26736..8752d80003 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -173,7 +173,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) } + updateTimestampSeconds: profile.profileLastUpdated ) } } @@ -272,7 +272,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) }, + updateTimestampSeconds: profile.profileLastUpdated, blocksCommunityMessageRequests: !checkForCommunityMessageRequests ) } @@ -339,7 +339,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, - updateTimestampMs: profile.profileLastUpdated.map { UInt64($0) } + updateTimestampSeconds: profile.profileLastUpdated ) } diff --git a/SessionMessagingKit/Utilities/Profile+Updating.swift b/SessionMessagingKit/Utilities/Profile+Updating.swift index dc5ceb9f60..8baf719cdd 100644 --- a/SessionMessagingKit/Utilities/Profile+Updating.swift +++ b/SessionMessagingKit/Utilities/Profile+Updating.swift @@ -125,7 +125,36 @@ public extension Profile { } .eraseToAnyPublisher() } - } + } + + /// To try to maintain backwards compatibility with profile changes we want to continue to accept profile changes from old clients if + /// we haven't received a profile update from a new client yet otherwise, if we have, then we should only accept profile changes if + /// they are newer that our cached version of the profile data + static func shouldUpdateProfile( + _ profileUpdateTimestamp: TimeInterval?, + profile: Profile, + using dependencies: Dependencies + ) -> Bool { + /// We should consider `libSession` the source-of-truth for profile data for contacts so try to retrieve the profile data from + /// there before falling back to the one fetched from the database + let targetProfile: Profile = ( + dependencies.mutate(cache: .libSession) { $0.profile(contactId: profile.id) } ?? + profile + ) + let finalProfileUpdateTimestamp: TimeInterval = (profileUpdateTimestamp ?? 0) + let finalCachedProfileUpdateTimestamp: TimeInterval = (targetProfile.profileLastUpdated ?? 0) + + /// If neither the profile update or the cached profile have a timestamp then we should just always accept the update + /// + /// **Note:** We check if they are equal to `0` here because the default value from `libSession` will be `0` + /// rather than `null` + guard finalProfileUpdateTimestamp != 0 || finalCachedProfileUpdateTimestamp != 0 else { + return true + } + + /// Otherwise we should only accept the update if it's newer than our cached value + return (finalProfileUpdateTimestamp > finalCachedProfileUpdateTimestamp) + } static func updateIfNeeded( _ db: ObservingDatabase, @@ -133,7 +162,7 @@ public extension Profile { displayNameUpdate: DisplayNameUpdate = .none, displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, - profileUpdateTimestamp: TimeInterval, + profileUpdateTimestamp: TimeInterval?, isReuploadCurrentUserProfilePicture: Bool = false, using dependencies: Dependencies ) throws { @@ -141,10 +170,9 @@ public extension Profile { let profile: Profile = Profile.fetchOrCreate(db, id: publicKey) var profileChanges: [ConfigColumnAssignment] = [] - guard - profile.profileLastUpdated == nil || - profileUpdateTimestamp > profile.profileLastUpdated.defaulting(to: 0) - else { return } + guard shouldUpdateProfile(profileUpdateTimestamp, profile: profile, using: dependencies) else { + return + } // Name switch (displayNameUpdate, isCurrentUser) { From 88482e2c1ff2dcf6e5da0cde198595981cbe0a8b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 23 Sep 2025 14:21:25 +1000 Subject: [PATCH 229/244] fix: profile picture modal shows incorrect pro description for non-pro users --- Session.xcodeproj/project.pbxproj | 8 +++- .../Settings/ThreadSettingsViewModel.swift | 2 +- Session/Settings/SettingsViewModel.swift | 21 +++++++++- .../Modals & Toast/ConfirmationModal.swift | 41 ++++++++----------- SessionUIKit/Components/SessionProBadge.swift | 12 +++++- .../Utilities/String+SessionProBadge.swift | 40 ++++++++++++++++++ SessionUIKit/Utilities/UIView+Utilities.swift | 2 +- 7 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 SessionUIKit/Utilities/String+SessionProBadge.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 16f0a33e0d..677b50906d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; + 949D91222E822D5A0074F595 /* String+SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949D91212E822D520074F595 /* String+SessionProBadge.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */; }; 94AAB14D2E1F39B500A6FA18 /* ProCTAModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */; }; @@ -1044,11 +1045,11 @@ FDE521A62E0E6C8C00061B8E /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; FDE71B032E77CCEE0023F5F9 /* HTTPHeader+FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B022E77CCE90023F5F9 /* HTTPHeader+FileServer.swift */; }; + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */; }; FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */; }; FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */ = {isa = PBXBuildFile; fileRef = FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */; }; FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */; }; - FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; @@ -1574,6 +1575,7 @@ 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; + 949D91212E822D520074F595 /* String+SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SessionProBadge.swift"; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Modal+SwiftUI.swift"; sourceTree = ""; }; 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProCTAModal.swift; sourceTree = ""; }; @@ -2317,11 +2319,11 @@ FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionMessagingKit.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; FDE71B022E77CCE90023F5F9 /* HTTPHeader+FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+FileServer.swift"; sourceTree = ""; }; + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsGroupsViewModel.swift; sourceTree = ""; }; FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsProViewModel.swift; sourceTree = ""; }; FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Session - Anonymous Messenger.storekit"; sourceTree = ""; }; FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; @@ -3342,6 +3344,7 @@ C331FFAE2558FA7700070591 /* Utilities */ = { isa = PBXGroup; children = ( + 949D91212E822D520074F595 /* String+SessionProBadge.swift */, 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */, FD848B9728422F1A000E298B /* Date+Utilities.swift */, FD8A5B282DC060DD004C689B /* Double+Utilities.swift */, @@ -6283,6 +6286,7 @@ FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, 94AAB1512E1F753500A6FA18 /* CyclicGradientView.swift in Sources */, FD8A5B1E2DBF4BBC004C689B /* ScreenLock+Errors.swift in Sources */, + 949D91222E822D5A0074F595 /* String+SessionProBadge.swift in Sources */, FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */, 7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 18b868e5f9..742ff3c964 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1678,7 +1678,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob }, icon: .rightPlus, style: .circular, - showPro: false, + description: nil, accessibility: Accessibility( identifier: "Image picker", label: "Image picker" diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 8071de4d02..9daf4ee913 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -667,7 +667,26 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl }), icon: (currentUrl != nil ? .pencil : .rightPlus), style: .circular, - showPro: dependencies[feature: .sessionProEnabled], + description: { + guard dependencies[feature: .sessionProEnabled] else { return nil } + return dependencies[cache: .libSession].isSessionPro ? + "proAnimatedDisplayPictureModalDescription" + .localized() + .addProBadge( + at: .leading, + font: .systemFont(ofSize: Values.smallFontSize), + textColor: .textSecondary, + proBadgeSize: .small + ): + "proAnimatedDisplayPicturesNonProModalDescription" + .localized() + .addProBadge( + at: .trailing, + font: .systemFont(ofSize: Values.smallFontSize), + textColor: .textSecondary, + proBadgeSize: .small + ) + }(), accessibility: Accessibility( identifier: "Upload", label: "Upload" diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 8a53863bb1..b22ff1e77a 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -43,7 +43,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { target: self, action: #selector(proImageTapped) ) - proImageStackViewContainer.addGestureRecognizer(result) + proDescriptionLabelContainer.addGestureRecognizer(result) result.isEnabled = false return result @@ -185,21 +185,15 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() - private lazy var proImageStackView: UIStackView = { - let proBadge: SessionProBadge = SessionProBadge(size: .small) - let label: UILabel = UILabel() - label.font = .systemFont(ofSize: Values.smallFontSize) - label.themeTextColor = .textSecondary - label.text = "proAnimatedDisplayPictureModalDescription".localized() - - let result: UIStackView = UIStackView(arrangedSubviews: [ proBadge, label ]) - result.axis = .horizontal - result.spacing = Values.verySmallSpacing + private lazy var proDescriptionLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.smallFontSize) + result.themeTextColor = .textSecondary return result }() - private lazy var proImageStackViewContainer: UIView = { + private lazy var proDescriptionLabelContainer: UIView = { let result: UIView = UIView() result.isHidden = true @@ -268,7 +262,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { textToConfirmContainer, textViewContainer, textViewErrorLabel, - proImageStackViewContainer, + proDescriptionLabelContainer, imageViewContainer ] ) @@ -374,10 +368,10 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { profileView.pin(.top, to: .top, of: imageViewContainer, withInset: 20) profileView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -20) - proImageStackViewContainer.addSubview(proImageStackView) - proImageStackView.center(.horizontal, in: proImageStackViewContainer) - proImageStackView.pin(.top, to: .top, of: proImageStackViewContainer) - proImageStackView.pin(.bottom, to: .bottom, of: proImageStackViewContainer) + proDescriptionLabelContainer.addSubview(proDescriptionLabel) + proDescriptionLabel.center(.horizontal, in: proDescriptionLabelContainer) + proDescriptionLabel.pin(.top, to: .top, of: proDescriptionLabelContainer) + proDescriptionLabel.pin(.bottom, to: .bottom, of: proDescriptionLabelContainer) mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) @@ -536,13 +530,14 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { contentStackView.addArrangedSubview(radioButton) } - case .image(let source, let placeholder, let icon, let style, let showPro, let accessibility, let dataManager, _, let onClick): + case .image(let source, let placeholder, let icon, let style, let description, let accessibility, let dataManager, _, let onClick): imageViewContainer.isAccessibilityElement = (accessibility != nil) imageViewContainer.accessibilityIdentifier = accessibility?.identifier imageViewContainer.accessibilityLabel = accessibility?.label mainStackView.spacing = 0 contentStackView.spacing = Values.verySmallSpacing - proImageStackViewContainer.isHidden = !showPro + proDescriptionLabelContainer.isHidden = (description == nil) + proDescriptionLabel.attributedText = description imageViewContainer.isHidden = false profileView.clipsToBounds = (style == .circular) profileView.setDataManager(dataManager) @@ -671,7 +666,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { @objc private func imageViewTapped() { internalOnBodyTap?({ [weak self, info = self.info] valueUpdate in switch (valueUpdate, info.body) { - case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, _, let style, let showPro, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): + case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, _, let style, let description, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): self?.updateContent( with: info.with( body: .image( @@ -681,7 +676,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { placeholder: placeholder, icon: (updatedData == nil ? .rightPlus : .pencil), style: style, - showPro: showPro, + description: description, accessibility: accessibility, dataManager: dataManager, onProBageTapped: onProBadgeTapped, @@ -697,7 +692,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { } @objc private func proImageTapped() { - guard case .image(_, _, _, _, let showPro, _, _, let onProBadgeTapped, _) = info.body, showPro else { return } + guard case .image(_, _, _, _, let description, _, _, let onProBadgeTapped, _) = info.body, (description != nil) else { return } onProBadgeTapped?() } @@ -1057,7 +1052,7 @@ public extension ConfirmationModal.Info { placeholder: ImageDataManager.DataSource?, icon: ProfilePictureView.ProfileIcon = .none, style: ImageStyle, - showPro: Bool, + description: NSAttributedString?, accessibility: Accessibility?, dataManager: ImageDataManagerType, onProBageTapped: (() -> Void)?, diff --git a/SessionUIKit/Components/SessionProBadge.swift b/SessionUIKit/Components/SessionProBadge.swift index 35e0b5837e..c5cce36801 100644 --- a/SessionUIKit/Components/SessionProBadge.swift +++ b/SessionUIKit/Components/SessionProBadge.swift @@ -44,7 +44,7 @@ public class SessionProBadge: UIView { public init(size: Size) { self.size = size - super.init(frame: .zero) + super.init(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) self.setupView() } @@ -76,4 +76,14 @@ public class SessionProBadge: UIView { self.set(.width, to: self.size.width) self.set(.height, to: self.size.height) } + + public func toImage() -> UIImage { + self.proImageView.frame = CGRect( + x: (size.width - size.proFontWidth) / 2, + y: (size.height - size.proFontHeight) / 2, + width: size.proFontWidth, + height: size.proFontHeight + ) + return self.toImage(isOpaque: self.isOpaque, scale: UIScreen.main.scale) + } } diff --git a/SessionUIKit/Utilities/String+SessionProBadge.swift b/SessionUIKit/Utilities/String+SessionProBadge.swift new file mode 100644 index 0000000000..15f36582cc --- /dev/null +++ b/SessionUIKit/Utilities/String+SessionProBadge.swift @@ -0,0 +1,40 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public extension String { + enum SessionProBadgePosition { + case leading, trailing + } + + func addProBadge( + at postion: SessionProBadgePosition, + font: UIFont, + textColor: ThemeValue = .textPrimary, + proBadgeSize: SessionProBadge.Size, + spacing: String = " " + ) -> NSMutableAttributedString { + let image: UIImage = SessionProBadge(size: proBadgeSize).toImage() + let base = NSMutableAttributedString() + let attachment = NSTextAttachment() + attachment.image = image + + // Vertical alignment tweak to align to baseline + let cap = font.capHeight + let dy = (cap - image.size.height) / 2 + attachment.bounds = CGRect(x: 0, y: dy, width: image.size.width, height: image.size.height) + + switch postion { + case .leading: + base.append(NSAttributedString(attachment: attachment)) + base.append(NSAttributedString(string: spacing)) + base.append(NSAttributedString(string: self, attributes: [.font: font, .themeForegroundColor: textColor])) + case .trailing: + base.append(NSAttributedString(string: self, attributes: [.font: font, .themeForegroundColor: textColor])) + base.append(NSAttributedString(string: spacing)) + base.append(NSAttributedString(attachment: attachment)) + } + + return base + } +} diff --git a/SessionUIKit/Utilities/UIView+Utilities.swift b/SessionUIKit/Utilities/UIView+Utilities.swift index b5518ad1f0..28a13ba074 100644 --- a/SessionUIKit/Utilities/UIView+Utilities.swift +++ b/SessionUIKit/Utilities/UIView+Utilities.swift @@ -3,7 +3,7 @@ import UIKit public extension UIView { - func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage? { + func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = isOpaque From 3d7699bf2e5da4eb89f80b5083214b2fcc410210 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 23 Sep 2025 16:34:25 +1000 Subject: [PATCH 230/244] Fixed a crash when unsubscribing from group PNs --- Session.xcodeproj/project.pbxproj | 16 ++++++++-------- .../Models/SubscribeRequest.swift | 9 ++++----- .../Models/SubscribeResponse.swift | 4 ++++ .../Models/UnsubscribeRequest.swift | 9 ++++----- .../Models/UnsubscribeResponse.swift | 4 ++++ .../PushNotification/PushNotificationAPI.swift | 15 +++++++++++++++ 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2ab6a4f486..1924b0e25c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8320,7 +8320,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 633; + CURRENT_PROJECT_VERSION = 635; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8360,7 +8360,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.3; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8401,7 +8401,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 633; + CURRENT_PROJECT_VERSION = 635; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8436,7 +8436,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.3; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8882,7 +8882,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 633; + CURRENT_PROJECT_VERSION = 635; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8921,7 +8921,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.3; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9469,7 +9469,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 633; + CURRENT_PROJECT_VERSION = 635; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9502,7 +9502,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.3; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", diff --git a/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift index f28f2eb92c..400c8884f8 100644 --- a/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift @@ -115,12 +115,11 @@ public extension Network.PushNotification { // MARK: - Coding public func encode(to encoder: Encoder) throws { - guard subscriptions.count > 1 else { - try subscriptions[0].encode(to: encoder) - return + switch subscriptions.count { + case 0: return + case 1: try subscriptions[0].encode(to: encoder) + default: try subscriptions.encode(to: encoder) } - - try subscriptions.encode(to: encoder) } } } diff --git a/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift index 59c8404399..5e5f3f193f 100644 --- a/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift @@ -32,6 +32,10 @@ public extension Network.PushNotification { public let subResponses: [SubResponse] + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } + public init(from decoder: Decoder) throws { guard let container: SingleValueDecodingContainer = try? decoder.singleValueContainer(), diff --git a/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift index 911632d92b..0c98e9d3fe 100644 --- a/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift @@ -74,12 +74,11 @@ extension Network.PushNotification { // MARK: - Coding public func encode(to encoder: Encoder) throws { - guard subscriptions.count > 1 else { - try subscriptions[0].encode(to: encoder) - return + switch subscriptions.count { + case 0: return + case 1: try subscriptions[0].encode(to: encoder) + default: try subscriptions.encode(to: encoder) } - - try subscriptions.encode(to: encoder) } } } diff --git a/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift index 4e5e0f9e56..3093a9371f 100644 --- a/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift @@ -32,6 +32,10 @@ public extension Network.PushNotification { public let subResponses: [SubResponse] + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } + public init(from decoder: Decoder) throws { guard let container: SingleValueDecodingContainer = try? decoder.singleValueContainer(), diff --git a/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift index dfc2cbee59..d852beeaed 100644 --- a/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift +++ b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift @@ -16,6 +16,13 @@ public extension Network.PushNotification { guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { throw NetworkError.invalidPreparedRequest } + guard !swarms.isEmpty else { + return try Network.PreparedRequest.cached( + SubscribeResponse(subResponses: []), + endpoint: Endpoint.subscribe, + using: dependencies + ) + } guard let notificationsEncryptionKey: Data = try? dependencies[singleton: .keychain].getOrGenerateEncryptionKey( forKey: .pushNotificationEncryptionKey, @@ -93,6 +100,14 @@ public extension Network.PushNotification { swarms: [(sessionId: SessionId, authMethod: AuthenticationMethod)], using dependencies: Dependencies ) throws -> Network.PreparedRequest { + guard !swarms.isEmpty else { + return try Network.PreparedRequest.cached( + UnsubscribeResponse(subResponses: []), + endpoint: Endpoint.subscribe, + using: dependencies + ) + } + return try Network.PreparedRequest( request: Request( method: .post, From 23b0d4fae5363e2002d9d28d8b836a96e54a2491 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 23 Sep 2025 17:12:58 +1000 Subject: [PATCH 231/244] fix unit test --- .../Jobs/DisplayPictureDownloadJobSpec.swift | 6 +++++- .../Sending & Receiving/MessageReceiverGroupsSpec.swift | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index c0f3d69608..1389d05588 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -23,7 +23,11 @@ class DisplayPictureDownloadJobSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } + initialSetup: { + $0.defaultInitialSetup() + $0.when { $0.profile(contactId: .any, threadId: .any, threadVariant: .any, visibleMessage: .any) } + .thenReturn(nil) + } ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index ace3bcf3c0..2926f10f11 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -404,7 +404,8 @@ class MessageReceiverGroupsSpec: QuickSpec { displayName: "TestName", profileKey: Data((0.. Date: Wed, 24 Sep 2025 00:38:33 +0000 Subject: [PATCH 232/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 107 ++++++++---------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index e1b245a269..77b4fd6490 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -354435,6 +354435,17 @@ } } }, + "proBetaFeatures" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Beta Features" + } + } + } + }, "proBilledAnnually" : { "extractionState" : "manual", "localizations" : { @@ -355221,34 +355232,10 @@ "proFaqDescription" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} TVS-da tez-tez verilən suallara cavab tapın." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Najděte odpovědi na časté dotazy v nápovědě {app_name}." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Find answers to common questions in the {app_name} FAQ." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vind antwoorden op veelgestelde vragen in de {app_name} FAQ." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відповіді на загальні запитання знайдеш у ЧаПи {app_name}." + "value" : "Find answers to common questions in the {app_pro} FAQ." } } } @@ -355920,41 +355907,6 @@ } } }, - "proFeatures" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{pro} özəllikləri" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funkce {pro}" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "{pro} Features" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{pro} functies" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Можливості {pro}" - } - } - } - }, "profile" : { "extractionState" : "manual", "localizations" : { @@ -359569,6 +359521,17 @@ } } }, + "proLargerGroupsTooltip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Larger group chats (up to 300 members) are coming soon for all Pro Beta users!" + } + } + } + }, "proLongerMessages" : { "extractionState" : "manual", "localizations" : { @@ -363127,6 +363090,17 @@ } } }, + "proRenewBeta" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew {pro} Beta" + } + } + } + }, "proRequestedRefund" : { "extractionState" : "manual", "localizations" : { @@ -421201,6 +421175,17 @@ } } }, + "upgradeSession" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgrade {app_name}" + } + } + } + }, "upgradeTo" : { "extractionState" : "manual", "localizations" : { @@ -428944,7 +428929,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your CPU does not support SSE 4.2, which is required for Session to run on Linux x64. Please upgrade your CPU or use a different operating system." + "value" : "Your CPU does not support SSE 4.2 instructions, which are required by Session on Linux x64 operating systems to process images. Please upgrade to a compatible CPU or use a different operating system." } } } From 1cc32be7eb523158d1387b712c8d6889e492c6d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Sep 2025 11:53:42 +1000 Subject: [PATCH 233/244] Flagged a bunch of functions as @MainActor to try to resolve crash --- .../ConversationVC+Interaction.swift | 22 ++++++------ .../Conversations/Input View/InputView.swift | 34 +++++++++---------- .../Input View/MentionSelectionView.swift | 2 +- Session/Home/HomeVC.swift | 2 +- .../GIFs/GifPickerViewController.swift | 4 +-- Session/Meta/SessionApp.swift | 6 ++-- .../NotificationActionHandler.swift | 28 +++++++-------- Session/Shared/Views/SessionCell.swift | 7 ++-- SessionUIKit/Components/ExpandableLabel.swift | 6 ++-- .../Components/Input View/InputTextView.swift | 10 +++--- .../Input View/InputViewButton.swift | 8 ++--- SessionUIKit/Style Guide/ThemeManager.swift | 12 ++++--- .../AttachmentApprovalViewController.swift | 21 ++++++------ .../AttachmentPrepViewController.swift | 5 ++- .../AttachmentTextToolbar.swift | 14 ++++---- 15 files changed, 89 insertions(+), 92 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ad0541a19d..50caf2f972 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -239,7 +239,7 @@ extension ConversationVC: // MARK: - Session Pro CTA - @discardableResult func showSessionProCTAIfNeeded() -> Bool { + @discardableResult @MainActor func showSessionProCTAIfNeeded() -> Bool { let dependencies: Dependencies = viewModel.dependencies guard dependencies[feature: .sessionProEnabled] && (!viewModel.isSessionPro) else { return false @@ -536,13 +536,13 @@ extension ConversationVC: // MARK: - InputViewDelegate - func handleDisabledInputTapped() { + @MainActor func handleDisabledInputTapped() { guard viewModel.threadData.threadIsBlocked == true else { return } self.showBlockedModalIfNeeded() } - func handleCharacterLimitLabelTapped() { + @MainActor func handleCharacterLimitLabelTapped() { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -582,7 +582,7 @@ extension ConversationVC: present(confirmationModal, animated: true, completion: nil) } - func handleDisabledAttachmentButtonTapped() { + @MainActor func handleDisabledAttachmentButtonTapped() { /// This logic was added because an Apple reviewer rejected an emergency update as they thought these buttons were /// unresponsive (even though there is copy on the screen communicating that they are intentionally disabled) - in order /// to prevent this happening in the future we've added this toast when pressing on the disabled button @@ -599,7 +599,7 @@ extension ConversationVC: ) } - func handleDisabledVoiceMessageButtonTapped() { + @MainActor func handleDisabledVoiceMessageButtonTapped() { /// This logic was added because an Apple reviewer rejected an emergency update as they thought these buttons were /// unresponsive (even though there is copy on the screen communicating that they are intentionally disabled) - in order /// to prevent this happening in the future we've added this toast when pressing on the disabled button @@ -618,7 +618,7 @@ extension ConversationVC: // MARK: --Message Sending - func handleSendButtonTapped() { + @MainActor func handleSendButtonTapped() { guard LibSession.numberOfCharactersLeft( for: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines), isSessionPro: viewModel.isSessionPro @@ -634,7 +634,7 @@ extension ConversationVC: ) } - func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { + @MainActor func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -870,7 +870,7 @@ extension ConversationVC: } } - func showLinkPreviewSuggestionModal() { + @MainActor func showLinkPreviewSuggestionModal() { // Hides accessory view while link preview confirmation is presented hideInputAccessoryView() @@ -900,7 +900,7 @@ extension ConversationVC: present(linkPreviewModal, animated: true, completion: nil) } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { // Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to // appear (as that is not expected/correct behaviour) guard !viewIsAppearing else { return } @@ -926,7 +926,7 @@ extension ConversationVC: // MARK: --Attachments - func didPasteImageFromPasteboard(_ image: UIImage) { + @MainActor func didPasteImageFromPasteboard(_ image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } let dataSource = DataSourceValue(data: imageData, dataType: .jpeg, using: viewModel.dependencies) @@ -947,7 +947,7 @@ extension ConversationVC: // MARK: --Mentions - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { guard let currentMentionStartIndex = currentMentionStartIndex else { return } mentions.append(mentionInfo) diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index b75531a7e0..175222f4a0 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -306,12 +306,12 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // MARK: - Updating - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() self.bottomStackView?.alignment = (inputTextView.contentSize.height > inputTextView.minHeight) ? .top : .center } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { let hasText = !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty sendButton.isHidden = !hasText voiceMessageButtonContainer.isHidden = hasText @@ -320,7 +320,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M delegate?.inputTextViewDidChangeContent(inputTextView) } - func updateNumberOfCharactersLeft(_ text: String) { + @MainActor func updateNumberOfCharactersLeft(_ text: String) { let numberOfCharactersLeft: Int = LibSession.numberOfCharactersLeft( for: text.trimmingCharacters(in: .whitespacesAndNewlines), isSessionPro: dependencies[cache: .libSession].isSessionPro @@ -331,7 +331,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M characterLimitLabelTapGestureRecognizer.isEnabled = (numberOfCharactersLeft < Self.thresholdForCharacterLimit) } - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { delegate?.didPasteImageFromPasteboard(image) } @@ -514,14 +514,14 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return super.point(inside: point, with: event) } - func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) { + @MainActor func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) { if inputViewButton == sendButton { delegate?.handleSendButtonTapped() } if inputViewButton == voiceMessageButton && inputState.allowedInputTypes != .all { delegate?.handleDisabledVoiceMessageButtonTapped() } } - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { + @MainActor func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { guard inputViewButton == voiceMessageButton else { return } guard inputState.allowedInputTypes == .all else { return } @@ -532,7 +532,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M delegate?.startVoiceMessageRecording() } - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { + @MainActor func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { guard let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton, @@ -542,7 +542,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M voiceMessageRecordingView.handleLongPressMoved(to: location) } - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { + @MainActor func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { guard let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton, @@ -626,7 +626,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M } } - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { delegate?.handleMentionSelected(mentionInfo, from: view) } @@ -662,12 +662,12 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // MARK: - Delegate protocol InputViewDelegate: ExpandingAttachmentsButtonDelegate, VoiceMessageRecordingViewDelegate { - func showLinkPreviewSuggestionModal() - func handleSendButtonTapped() - func handleDisabledInputTapped() - func handleDisabledVoiceMessageButtonTapped() - func handleCharacterLimitLabelTapped() - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) - func didPasteImageFromPasteboard(_ image: UIImage) + @MainActor func showLinkPreviewSuggestionModal() + @MainActor func handleSendButtonTapped() + @MainActor func handleDisabledInputTapped() + @MainActor func handleDisabledVoiceMessageButtonTapped() + @MainActor func handleCharacterLimitLabelTapped() + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) + @MainActor func didPasteImageFromPasteboard(_ image: UIImage) } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 2f1703d3a8..5abdeebb4b 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -218,5 +218,5 @@ private extension MentionSelectionView { // MARK: - Delegate protocol MentionSelectionViewDelegate: AnyObject { - func handleMentionSelected(_ mention: MentionInfo, from view: MentionSelectionView) + @MainActor func handleMentionSelected(_ mention: MentionInfo, from view: MentionSelectionView) } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index ecab5c8318..cf9e8ed989 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -496,7 +496,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi } } - private func updateNavBarButtons( + @MainActor private func updateNavBarButtons( userProfile: Profile, serviceNetwork: ServiceNetwork, forceOffline: Bool diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index b1557c2166..b741b3d384 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -19,7 +19,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect didSet { Log.debug(.giphy, "ViewController viewMode: \(viewMode)") - updateContents() + Task { @MainActor [weak self] in self?.updateContents() } } } @@ -220,7 +220,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect return label } - private func updateContents() { + @MainActor private func updateContents() { guard let noResultsView = self.noResultsView else { Log.error(.giphy, "ViewController missing noResultsView") return diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 412d4be4b3..e311b2ed71 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -158,7 +158,7 @@ public class SessionApp: SessionAppType { /// Show Session Network Page for this release. We'll be able to extend this fuction to show other screens that is new /// or we want to promote in the future. - public func showPromotedScreen() { + @MainActor public func showPromotedScreen() { guard let homeViewController: HomeVC = self.homeViewController else { return } let viewController: SessionHostingViewController = SessionHostingViewController( @@ -243,7 +243,7 @@ public protocol SessionAppType { @MainActor var homePresentedViewController: UIViewController? { get } func setHomeViewController(_ homeViewController: HomeVC) - func showHomeView() + @MainActor func showHomeView() @MainActor func presentConversationCreatingIfNeeded( for threadId: String, variant: SessionThread.Variant, @@ -253,7 +253,7 @@ public protocol SessionAppType { ) func createNewConversation() func resetData(onReset: (() -> ())) - func showPromotedScreen() + @MainActor func showPromotedScreen() } public extension SessionAppType { diff --git a/Session/Notifications/NotificationActionHandler.swift b/Session/Notifications/NotificationActionHandler.swift index 083aa94356..74b18f89f8 100644 --- a/Session/Notifications/NotificationActionHandler.swift +++ b/Session/Notifications/NotificationActionHandler.swift @@ -60,17 +60,17 @@ public class NotificationActionHandler { switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier: Log.debug("[NotificationActionHandler] Default action") - switch categoryIdentifier { - case NotificationCategory.info.identifier: - return showPromotedScreen() - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - default: - return showThread(userInfo: userInfo) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + Task(priority: .userInitiated) { @MainActor [weak self] in + switch categoryIdentifier { + case NotificationCategory.info.identifier: self?.showPromotedScreen() + default: self?.showThread(userInfo: userInfo) + } } + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + case UNNotificationDismissActionIdentifier: // TODO - mark as read? Log.debug("[NotificationActionHandler] Dismissed notification") @@ -232,7 +232,7 @@ public class NotificationActionHandler { .eraseToAnyPublisher() } - @MainActor func showThread(userInfo: [AnyHashable: Any]) -> AnyPublisher { + @MainActor func showThread(userInfo: [AnyHashable: Any]) { guard let threadId = userInfo[NotificationUserInfoKey.threadId] as? String, let threadVariantRaw = userInfo[NotificationUserInfoKey.threadVariantRaw] as? Int, @@ -249,18 +249,14 @@ public class NotificationActionHandler { dismissing: dependencies[singleton: .app].homePresentedViewController, animated: (UIApplication.shared.applicationState == .active) ) - - return Just(()).eraseToAnyPublisher() } - func showHomeVC() -> AnyPublisher { + @MainActor func showHomeVC() { dependencies[singleton: .app].showHomeView() - return Just(()).eraseToAnyPublisher() } - func showPromotedScreen() -> AnyPublisher { + @MainActor func showPromotedScreen() { dependencies[singleton: .app].showPromotedScreen() - return Just(()).eraseToAnyPublisher() } private func markAsRead(threadId: String) -> AnyPublisher { diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 9025578221..f30fd2c3cf 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -316,10 +316,10 @@ public class SessionCell: UITableViewCell { botSeparator.isHidden = true } - public func update( + @MainActor public func update( with info: Info, tableSize: CGSize, - onToggleExpansion: (() -> Void)? = nil, + onToggleExpansion: (@MainActor () -> Void)? = nil, using dependencies: Dependencies ) { /// Need to do this here as `prepareForReuse` doesn't always seem to get called @@ -549,7 +549,8 @@ public class SessionCell: UITableViewCell { expandableDescriptionLabel.accessibilityIdentifier = info.description?.accessibility?.identifier expandableDescriptionLabel.accessibilityLabel = info.description?.accessibility?.label expandableDescriptionLabel.isHidden = (info.description == nil) - expandableDescriptionLabel.onToggleExpansion = (info.description?.interaction == .expandable ? onToggleExpansion : nil) + expandableDescriptionLabel.onToggleExpansion = (info.description?.interaction == .expandable ? + onToggleExpansion : nil) trailingAccessoryView.update( with: info.trailingAccessory, tintColor: info.styling.tintColor, diff --git a/SessionUIKit/Components/ExpandableLabel.swift b/SessionUIKit/Components/ExpandableLabel.swift index f5b1d712ba..b28aecb0a6 100644 --- a/SessionUIKit/Components/ExpandableLabel.swift +++ b/SessionUIKit/Components/ExpandableLabel.swift @@ -7,7 +7,7 @@ public class ExpandableLabel: UIView { private var layoutLoopCounter: Int = 0 private var isExpanded: Bool = false private var toggleDebounceTimer: Timer? - public var onToggleExpansion: (() -> Void)? + public var onToggleExpansion: (@MainActor () -> Void)? public var font: UIFont { get { label.font } @@ -164,7 +164,7 @@ public class ExpandableLabel: UIView { // MARK: - Interaction - private func toggleExpansion() { + @MainActor private func toggleExpansion() { isExpanded.toggle() buttonLabel.text = (isExpanded ? "viewLess".localized() : "viewMore".localized()) label.numberOfLines = isExpanded ? 0 : (maxNumberOfLines - 1) @@ -178,7 +178,7 @@ public class ExpandableLabel: UIView { toggleDebounceTimer?.invalidate() toggleDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in - self?.toggleExpansion() + Task { @MainActor [weak self] in self?.toggleExpansion() } } } } diff --git a/SessionUIKit/Components/Input View/InputTextView.swift b/SessionUIKit/Components/Input View/InputTextView.swift index 1f9fd8c204..069dec0413 100644 --- a/SessionUIKit/Components/Input View/InputTextView.swift +++ b/SessionUIKit/Components/Input View/InputTextView.swift @@ -92,11 +92,11 @@ public final class InputTextView: UITextView, UITextViewDelegate { // MARK: - Updating - public func textViewDidChange(_ textView: UITextView) { + @MainActor public func textViewDidChange(_ textView: UITextView) { handleTextChanged() } - private func handleTextChanged() { + @MainActor private func handleTextChanged() { defer { snDelegate?.inputTextViewDidChangeContent(self) } placeholderLabel.isHidden = !(text ?? "").isEmpty @@ -118,7 +118,7 @@ public final class InputTextView: UITextView, UITextViewDelegate { // MARK: - InputTextViewDelegate public protocol InputTextViewDelegate: AnyObject { - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) } diff --git a/SessionUIKit/Components/Input View/InputViewButton.swift b/SessionUIKit/Components/Input View/InputViewButton.swift index 184d527fa4..37c90ead2f 100644 --- a/SessionUIKit/Components/Input View/InputViewButton.swift +++ b/SessionUIKit/Components/Input View/InputViewButton.swift @@ -192,8 +192,8 @@ public final class InputViewButton: UIView { // MARK: - Delegate public protocol InputViewButtonDelegate: AnyObject { - func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) + @MainActor func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) + @MainActor func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) + @MainActor func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) + @MainActor func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) } diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index e2dff643f2..6ce09450c0 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -221,7 +221,7 @@ public enum ThemeManager { SNUIKit.mainWindow?.backgroundColor = color(for: .backgroundPrimary, in: currentTheme, with: primaryColor) } - public static func onThemeChange(observer: AnyObject, callback: @escaping (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { + public static func onThemeChange(observer: AnyObject, callback: @escaping @MainActor (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { ThemeManager.uiRegistry.setObject( ThemeApplier( existingApplier: ThemeManager.get(for: observer), @@ -456,14 +456,14 @@ internal class ThemeApplier { case controlState } - private let applyTheme: (Theme) -> () + private let applyTheme: @MainActor (Theme) -> () private let info: [AnyHashable] private var otherAppliers: [ThemeApplier]? init( existingApplier: ThemeApplier?, info: [AnyHashable], - applyTheme: @escaping (Theme) -> () + applyTheme: @escaping @MainActor (Theme) -> () ) { self.applyTheme = applyTheme self.info = info @@ -478,7 +478,9 @@ internal class ThemeApplier { // Automatically apply the theme immediately (if the database has been setup) if SNUIKit.config?.isStorageValid == true || ThemeManager.hasLoadedTheme { - self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) + Task { @MainActor [weak self] in + self?.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) + } } } @@ -509,7 +511,7 @@ internal class ThemeApplier { return self } - fileprivate func apply(theme: Theme, isInitialApplication: Bool = false) { + @MainActor fileprivate func apply(theme: Theme, isInitialApplication: Bool = false) { self.applyTheme(theme) // For the initial application of a ThemeApplier we don't want to apply the other diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 0332840e9c..8a2b9917ad 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -276,14 +276,14 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Contents - private func updateContents() { + @MainActor private func updateContents() { updateNavigationBar() updateInputAccessory() } // MARK: - Input Accessory - public func updateInputAccessory() { + @MainActor public func updateInputAccessory() { var currentPageViewController: AttachmentPrepViewController? if pageViewControllers?.count == 1 { @@ -430,7 +430,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC animated: animated ) { [weak self] finished in completion?(finished) - self?.updateContents() + + Task { @MainActor [weak self] in self?.updateContents() } } } @@ -639,7 +640,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Session Pro CTA - @discardableResult func showSessionProCTAIfNeeded() -> Bool { + @discardableResult @MainActor func showSessionProCTAIfNeeded() -> Bool { guard dependencies[feature: .sessionProEnabled] && (!isSessionPro) else { return false } @@ -660,7 +661,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return true } - func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { + @MainActor func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -687,7 +688,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - AttachmentTextToolbarDelegate extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { - func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() let confirmationModal: ConfirmationModal = ConfirmationModal( @@ -709,7 +710,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { present(confirmationModal, animated: true, completion: nil) } - func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) { guard let text = attachmentTextToolbar.text, LibSession.numberOfCharactersLeft( @@ -737,7 +738,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { ) } - func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) { approvalDelegate?.attachmentApproval(self, didChangeMessageText: attachmentTextToolbar.text) } } @@ -745,11 +746,11 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { // MARK: - extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate { - func prepViewControllerUpdateNavigationBar() { + @MainActor func prepViewControllerUpdateNavigationBar() { updateNavigationBar() } - func prepViewControllerUpdateControls() { + @MainActor func prepViewControllerUpdateControls() { updateInputAccessory() } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index 0844c1ee20..3aee88512b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -9,9 +9,8 @@ import SessionMessagingKit import SessionUtilitiesKit protocol AttachmentPrepViewControllerDelegate: AnyObject { - func prepViewControllerUpdateNavigationBar() - - func prepViewControllerUpdateControls() + @MainActor func prepViewControllerUpdateNavigationBar() + @MainActor func prepViewControllerUpdateControls() } // MARK: - diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index 26204358e0..c516ed9a2a 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -10,9 +10,9 @@ import Combine let kMaxMessageBodyCharacterCount = 2000 protocol AttachmentTextToolbarDelegate: AnyObject { - func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) - func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) - func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) } // MARK: - @@ -204,17 +204,15 @@ extension AttachmentTextToolbar: InputViewButtonDelegate { } extension AttachmentTextToolbar: InputTextViewDelegate { - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() self.bottomStackView?.alignment = (inputTextView.contentSize.height > inputTextView.minHeight) ? .top : .center } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { updateNumberOfCharactersLeft(text ?? "") delegate?.attachmentTextToolbarDidChange(self) } - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { - - } + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) {} } From 4248350bebca48866c625d10e8abe20d2617ab12 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 24 Sep 2025 13:09:08 +1000 Subject: [PATCH 234/244] Bumped build number --- Session.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1924b0e25c..705fb6d0b5 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8320,7 +8320,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 635; + CURRENT_PROJECT_VERSION = 636; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8401,7 +8401,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 635; + CURRENT_PROJECT_VERSION = 636; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8882,7 +8882,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 635; + CURRENT_PROJECT_VERSION = 636; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9469,7 +9469,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 635; + CURRENT_PROJECT_VERSION = 636; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From 644c76ceb932d809c7f1d77a5b626d188c3790ab Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Sep 2025 16:53:21 +1000 Subject: [PATCH 235/244] Fixing a number of layout issues and constraint violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed a bunch of constraint violations and bugs with the VisibleMessageCell and some of it's components • Fixed a bug where some colours wouldn't be able to be resolved correctly (UIColor.black.alpha isn't a UIColor it's a UICachedDeviceWhiteColor so casting to `Self` on an extension of `UIColor` fails) • Fixed a bug where the theme preview UI had a "highlighted" state when pressed --- Session/Conversations/ConversationVC.swift | 1 - .../Message Cells/CallMessageCell.swift | 72 ++-- .../Content Views/DeletedMessageView.swift | 18 +- .../Content Views/MediaView.swift | 13 +- .../Content Views/QuoteView.swift | 1 + .../Message Cells/VisibleMessageCell.swift | 311 ++++++++++-------- Session/Settings/AppearanceViewModel.swift | 3 +- .../Settings/PrivacySettingsViewModel.swift | 94 ++++-- .../Views/ThemeMessagePreviewView.swift | 16 +- SessionUIKit/Style Guide/ThemeManager.swift | 25 +- 10 files changed, 317 insertions(+), 237 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 0b13d35bbd..4296ed5813 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -365,7 +365,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa .backgroundPrimary, .backgroundPrimary ] - result.set(.height, to: 92) return result }() diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 47dca8d33b..25d3aeb47a 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -28,18 +28,27 @@ final class CallMessageCell: MessageCell { // MARK: - UI private lazy var topConstraint: NSLayoutConstraint = mainStackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) - private lazy var iconImageViewWidthConstraint: NSLayoutConstraint = iconImageView.set(.width, to: 0) - private lazy var iconImageViewHeightConstraint: NSLayoutConstraint = iconImageView.set(.height, to: 0) - private lazy var infoImageViewWidthConstraint: NSLayoutConstraint = infoImageView.set(.width, to: 0) - private lazy var infoImageViewHeightConstraint: NSLayoutConstraint = infoImageView.set(.height, to: 0) - private lazy var iconImageView: UIImageView = UIImageView() + private lazy var iconImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.themeTintColor = .textPrimary + result.set(.width, to: CallMessageCell.iconSize) + result.set(.height, to: CallMessageCell.iconSize) + result.setContentHugging(.horizontal, to: .required) + result.setCompressionResistance(.horizontal, to: .required) + + return result + }() private lazy var infoImageView: UIImageView = { let result: UIImageView = UIImageView( image: UIImage(named: "ic_info")? .withRenderingMode(.alwaysTemplate) ) result.themeTintColor = .textPrimary + result.set(.width, to: CallMessageCell.iconSize) + result.set(.height, to: CallMessageCell.iconSize) + result.setContentHugging(.horizontal, to: .required) + result.setCompressionResistance(.horizontal, to: .required) return result }() @@ -62,38 +71,25 @@ final class CallMessageCell: MessageCell { result.textAlignment = .center result.lineBreakMode = .byWordWrapping result.numberOfLines = 0 + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) return result }() - private lazy var container: UIView = { - let result: UIView = UIView() + private lazy var contentStackView: UIStackView = { + let result: UIStackView = UIStackView(arrangedSubviews: [iconImageView, label, infoImageView]) + result.axis = .horizontal + result.alignment = .center + result.spacing = CallMessageCell.horizontalInset + + return result + }() + + private lazy var container: UIStackView = { + let result: UIStackView = UIStackView() result.themeBackgroundColor = .backgroundSecondary result.layer.cornerRadius = 18 - result.addSubview(label) - - label.pin(.top, to: .top, of: result, withInset: CallMessageCell.verticalInset) - label.pin( - .left, - to: .left, - of: result, - withInset: ((CallMessageCell.horizontalInset * 2) + infoImageView.bounds.size.width) - ) - label.pin( - .right, - to: .right, - of: result, - withInset: -((CallMessageCell.horizontalInset * 2) + infoImageView.bounds.size.width) - ) - label.pin(.bottom, to: .bottom, of: result, withInset: -CallMessageCell.verticalInset) - - result.addSubview(iconImageView) - iconImageView.center(.vertical, in: result) - iconImageView.pin(.left, to: .left, of: result, withInset: CallMessageCell.horizontalInset) - - result.addSubview(infoImageView) - infoImageView.center(.vertical, in: result) - infoImageView.pin(.right, to: .right, of: result, withInset: -CallMessageCell.horizontalInset) return result }() @@ -112,10 +108,14 @@ final class CallMessageCell: MessageCell { override func setUpViewHierarchy() { super.setUpViewHierarchy() - iconImageViewWidthConstraint.isActive = true - iconImageViewHeightConstraint.isActive = true + container.addSubview(contentStackView) addSubview(mainStackView) + contentStackView.pin(.top, to: .top, of: container, withInset: CallMessageCell.verticalInset) + contentStackView.pin(.leading, to: .leading, of: container, withInset: CallMessageCell.horizontalInset) + contentStackView.pin(.trailing, to: .trailing, of: container, withInset: -CallMessageCell.horizontalInset) + contentStackView.pin(.bottom, to: .bottom, of: container, withInset: -CallMessageCell.verticalInset) + topConstraint.isActive = true mainStackView.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin) mainStackView.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin) @@ -163,8 +163,7 @@ final class CallMessageCell: MessageCell { default: return nil } }() - iconImageViewWidthConstraint.constant = (iconImageView.image != nil ? CallMessageCell.iconSize : 0) - iconImageViewHeightConstraint.constant = (iconImageView.image != nil ? CallMessageCell.iconSize : 0) + iconImageView.isHidden = (iconImageView.image == nil) let shouldShowInfoIcon: Bool = ( ( @@ -175,8 +174,7 @@ final class CallMessageCell: MessageCell { Permissions.microphone != .granted ) ) - infoImageViewWidthConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) - infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) + infoImageView.isHidden = !shouldShowInfoIcon label.text = cellViewModel.body diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index baed864ba4..58ed68ab64 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -41,6 +41,12 @@ final class DeletedMessageView: UIView { imageView.set(.width, to: DeletedMessageView.iconSize) imageView.set(.height, to: DeletedMessageView.iconSize) + let imageViewContainer: UIView = UIView() + imageViewContainer.addSubview(imageView) + imageView.center(.vertical, in: imageViewContainer) + imageView.pin(.leading, to: .leading, of: imageViewContainer) + imageView.pin(.trailing, to: .trailing, of: imageViewContainer) + // Body label let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) @@ -57,26 +63,26 @@ final class DeletedMessageView: UIView { titleLabel.alpha = Values.highOpacity titleLabel.lineBreakMode = .byTruncatingTail titleLabel.numberOfLines = 2 + titleLabel.setContentHugging(.vertical, to: .required) + titleLabel.setCompressionResistance(.vertical, to: .required) // Stack view let stackView = UIStackView(arrangedSubviews: [ - imageView, + imageViewContainer, titleLabel ]) stackView.axis = .horizontal - stackView.alignment = .center + stackView.alignment = .fill stackView.spacing = Values.smallSpacing stackView.isLayoutMarginsRelativeArrangement = true stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 6) addSubview(stackView) - let calculatedSize: CGSize = stackView.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 999)) - stackView.pin(.top, to: .top, of: self, withInset: Self.verticalInset) stackView.pin(.leading, to: .leading, of: self, withInset: Self.horizontalInset) stackView.pin(.trailing, to: .trailing, of: self, withInset: -Self.horizontalInset) stackView.pin(.bottom, to: .bottom, of: self, withInset: -Self.verticalInset) - - stackView.set(.height, greaterThanOrEqualTo: calculatedSize.height) + stackView.setContentHugging(.vertical, to: .required) + stackView.setCompressionResistance(.vertical, to: .required) } } diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 09e9b1e7b0..e1e942ee2d 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -117,7 +117,8 @@ public class MediaView: UIView { let result: GradientView = GradientView() result.themeBackgroundGradient = [ .value(.black, alpha: 0), - .value(.black, alpha: 0.4) + .value(.black, alpha: 0.75), + .value(.black, alpha: 0.75) ] result.isHidden = true @@ -129,7 +130,6 @@ public class MediaView: UIView { result.font = .systemFont(ofSize: Values.smallFontSize) result.text = attachment.duration.map { Format.duration($0) } result.themeTextColor = .white - result.isHidden = true return result }() @@ -162,14 +162,12 @@ public class MediaView: UIView { errorIconView.center(in: self) addSubview(durationBackgroundView) - durationBackgroundView.set(.height, to: 40) durationBackgroundView.pin(.leading, to: .leading, of: imageView) durationBackgroundView.pin(.trailing, to: .trailing, of: imageView) durationBackgroundView.pin(.bottom, to: .bottom, of: imageView) - addSubview(durationLabel) - durationLabel.pin(.trailing, to: .trailing, of: imageView, withInset: -Values.smallSpacing) - durationLabel.pin(.bottom, to: .bottom, of: imageView, withInset: -Values.smallSpacing) + durationBackgroundView.addSubview(durationLabel) + durationLabel.pin(to: durationBackgroundView, withInset: Values.smallSpacing) addSubview(playButtonIcon) playButtonIcon.set(.width, to: 72) @@ -215,13 +213,12 @@ public class MediaView: UIView { !loadingIndicator.isHidden || !attachment.isVideo ) - durationLabel.isHidden = ( + durationBackgroundView.isHidden = ( shouldSupressControls || attachment.duration == nil || !loadingIndicator.isHidden || !attachment.isVideo ) - durationBackgroundView.isHidden = durationLabel.isHidden } @MainActor diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index 1c7d67b06c..d5248fefbf 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -220,6 +220,7 @@ final class QuoteView: UIView { authorLabel.lineBreakMode = .byTruncatingTail authorLabel.isHidden = (authorLabel.text == nil) authorLabel.numberOfLines = 1 + authorLabel.setCompressionResistance(.vertical, to: .required) let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ]) labelStackView.axis = .vertical diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0dbde5e460..15284f7e42 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -9,6 +9,7 @@ import SessionMessagingKit final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private static let maxNumberOfLinesAfterTruncation: Int = 25 + private var isPreview: Bool = false private var isHandlingLongPress: Bool = false private var previousX: CGFloat = 0 @@ -33,29 +34,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ] } - // Constraints - internal lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) - private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) - private lazy var profilePictureViewLeadingConstraint = profilePictureView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.groupThreadHSpacing) - internal lazy var contentViewLeadingConstraint1 = snContentView.pin(.leading, to: .trailing, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) - private lazy var contentViewLeadingConstraint2 = snContentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: VisibleMessageCell.gutterSize) - private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) - internal lazy var contentViewTrailingConstraint1 = snContentView.pin(.trailing, to: .trailing, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) - private lazy var contentViewTrailingConstraint2 = snContentView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -VisibleMessageCell.gutterSize) - private lazy var contentBottomConstraint = snContentView.bottomAnchor - .constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -1) - - private lazy var underBubbleStackViewIncomingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: snContentView) - private lazy var underBubbleStackViewIncomingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) - private lazy var underBubbleStackViewOutgoingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.contactThreadHSpacing) - private lazy var underBubbleStackViewOutgoingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: snContentView) - private lazy var underBubbleStackViewNoHeightConstraint: NSLayoutConstraint = underBubbleStackView.set(.height, to: 0) - - private lazy var timerViewOutgoingMessageConstraint = timerView.pin(.trailing, to: .trailing, of: messageStatusContainerView) - private lazy var timerViewIncomingMessageConstraint = timerView.pin(.leading, to: .leading, of: messageStatusContainerView) - private lazy var messageStatusLabelOutgoingMessageConstraint = messageStatusLabel.pin(.trailing, to: .leading, of: timerView, withInset: -2) - private lazy var messageStatusLabelIncomingMessageConstraint = messageStatusLabel.pin(.leading, to: .trailing, of: timerView, withInset: 2) - private lazy var panGestureRecognizer: UIPanGestureRecognizer = { let result = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) result.delegate = self @@ -69,18 +47,54 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView, replyButton, timerView, - messageStatusContainerView, + messageStatusStackView, reactionContainerView ] + private lazy var leadingSpacer: UIView = { + let result: UIView = UIView() + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) + + return result + }() + + private lazy var trailingSpacer: UIView = { + let result: UIView = UIView() + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) + + return result + }() + + private lazy var profilePictureViewContainer: UIView = { + let result: UIView = UIView() + + return result + }() + private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, dataManager: nil ) + lazy var contentHStack: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [leadingSpacer, profilePictureViewContainer, mainVStack, trailingSpacer] + ) + result.axis = .horizontal + result.alignment = .fill + result.spacing = VisibleMessageCell.groupThreadHSpacing + + return result + }() + lazy var bubbleBackgroundView: UIView = { let result = UIView() result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -89,12 +103,32 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.clipsToBounds = true result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius result.set(.width, greaterThanOrEqualTo: VisibleMessageCell.largeCornerRadius * 2) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + + return result + }() + + private lazy var mainVStack: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [authorLabel, snContentView, underBubbleStackView] + ) + result.axis = .vertical + result.alignment = .fill + result.setCustomSpacing(VisibleMessageCell.authorLabelBottomSpacing, after: authorLabel) + result.setCustomSpacing(Values.verySmallSpacing, after: snContentView) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() private lazy var authorLabel: UILabel = { - let result = UILabel() + let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -103,6 +137,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.axis = .vertical result.spacing = Values.verySmallSpacing result.alignment = .leading + result.setContentHugging(.horizontal, to: .defaultHigh) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.horizontal, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -140,24 +179,39 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return result }() - - private lazy var timerView: DisappearingMessageTimerView = DisappearingMessageTimerView() lazy var underBubbleStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: []) + let result = UIStackView( + arrangedSubviews: [reactionContainerView, messageStatusStackView] + ) result.setContentHuggingPriority(.required, for: .vertical) result.setContentCompressionResistancePriority(.required, for: .vertical) result.axis = .vertical result.spacing = Values.verySmallSpacing result.alignment = .trailing + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() private lazy var reactionContainerView = ReactionContainerView() - internal lazy var messageStatusContainerView: UIView = { - let result = UIView() + internal lazy var messageStatusStackView: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [messageStatusLabel, messageStatusImageView, timerView] + ) + result.axis = .horizontal + result.alignment = .center + result.spacing = 2 + + return result + }() + + private lazy var timerView: DisappearingMessageTimerView = { + let result: DisappearingMessageTimerView = DisappearingMessageTimerView() + result.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) + result.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) return result }() @@ -178,11 +232,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.accessibilityLabel = "Message sent status tick" result.contentMode = .scaleAspectFit result.themeTintColor = .messageBubble_deliveryStatus + result.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) + result.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) return result }() - - internal lazy var messageStatusLabelPaddingView: UIView = UIView() // MARK: - Settings @@ -213,70 +267,70 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { enum Direction { case incoming, outgoing } + // MARK: - Initialization + + @MainActor init(isPreview: Bool) { + self.isPreview = isPreview + + super.init(style: .default, reuseIdentifier: nil) + + /// When a `UITableViewCell` is added as a subview instead of rendered as a cell directly within a `UITableView` the + /// `contentView` doesn't have constraints (because the table manages those) so we need to manually add them + contentView.pin(to: self) + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + @MainActor required init?(coder: NSCoder) { + super.init(coder: coder) + } + // MARK: - Lifecycle override func setUpViewHierarchy() { super.setUpViewHierarchy() + contentView.addSubview(contentHStack) + contentView.addSubview(replyButton) - // Author label - addSubview(authorLabel) - authorLabelTopConstraint.isActive = true - authorLabelHeightConstraint.isActive = true + profilePictureViewContainer.addSubview(profilePictureView) + bubbleBackgroundView.addSubview(bubbleView) - // Profile picture view - addSubview(profilePictureView) - profilePictureViewLeadingConstraint.isActive = true + replyButton.addSubview(replyIconImageView) - // Content view - addSubview(snContentView) - contentViewLeadingConstraint1.isActive = true - contentViewTopConstraint.isActive = true - contentViewTrailingConstraint1.isActive = true - snContentView.pin(.bottom, to: .bottom, of: profilePictureView) + contentHStack.pin(.top, to: .top, of: contentView) + contentHStack.pin( + .leading, + to: .leading, + of: contentView, + withInset: (isPreview ? 0 : VisibleMessageCell.contactThreadHSpacing) + ) + contentHStack.pin( + .trailing, + to: .trailing, + of: contentView, + withInset: (isPreview ? 0 : -VisibleMessageCell.contactThreadHSpacing) + ) + contentHStack + .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.verySmallSpacing) + .setting(priority: .defaultHigh) /// Avoid breaking encapsulated height + + // Profile picture view + profilePictureView.pin(.bottom, to: .bottom, of: profilePictureViewContainer) + profilePictureView.pin(.leading, to: .leading, of: profilePictureViewContainer) + profilePictureView.pin(.trailing, to: .trailing, of: profilePictureViewContainer) // Bubble background view - bubbleBackgroundView.addSubview(bubbleView) bubbleView.pin(to: bubbleBackgroundView) // Reply button - addSubview(replyButton) - replyButton.addSubview(replyIconImageView) replyIconImageView.center(in: replyButton) replyButton.pin(.leading, to: .trailing, of: snContentView, withInset: Values.smallSpacing) replyButton.center(.vertical, in: snContentView) - // Remaining constraints - authorLabel.pin(.leading, to: .leading, of: snContentView, withInset: VisibleMessageCell.authorLabelInset) - authorLabel.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) - - // Under bubble content - addSubview(underBubbleStackView) - underBubbleStackView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing) - underBubbleStackView.pin(.bottom, to: .bottom, of: self) - - underBubbleStackView.addArrangedSubview(reactionContainerView) - underBubbleStackView.addArrangedSubview(messageStatusContainerView) - underBubbleStackView.addArrangedSubview(messageStatusLabelPaddingView) - - messageStatusContainerView.addSubview(messageStatusLabel) - messageStatusContainerView.addSubview(messageStatusImageView) - messageStatusContainerView.addSubview(timerView) - - reactionContainerView.widthAnchor - .constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor) - .isActive = true - messageStatusImageView.pin(.top, to: .top, of: messageStatusContainerView) - messageStatusImageView.pin(.bottom, to: .bottom, of: messageStatusContainerView) - messageStatusImageView.pin(.trailing, to: .trailing, of: messageStatusContainerView) - messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) - messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) - timerView.pin(.top, to: .top, of: messageStatusContainerView) - timerView.pin(.bottom, to: .bottom, of: messageStatusContainerView) - timerView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) - timerView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) - messageStatusLabel.center(.vertical, in: messageStatusContainerView) - messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) - messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) + // Reactions container + reactionContainerView.set(.width, lessThanOrEqualTo: .width, of: underBubbleStackView) } // MARK: - Updating @@ -301,6 +355,18 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { cellViewModel.isOnlyMessageInCluster ) ) + contentHStack.layoutMargins = UIEdgeInsets( + top: (shouldAddTopInset ? Values.mediumSpacing : 0), + left: 0, + bottom: 0, + right: 0 + ) + + // Author label + authorLabel.isHidden = (cellViewModel.senderName == nil) + authorLabel.text = cellViewModel.senderName + authorLabel.themeTextColor = .textPrimary + let isGroupThread: Bool = ( cellViewModel.threadVariant == .community || cellViewModel.threadVariant == .legacyGroup || @@ -309,11 +375,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Profile picture view (should always be handled as a standard 'contact' profile picture) let profileShouldBeVisible: Bool = ( + isGroupThread && cellViewModel.canHaveProfile && cellViewModel.shouldShowProfile && cellViewModel.profile != nil ) - profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0) profilePictureView.isHidden = !cellViewModel.canHaveProfile profilePictureView.alpha = (profileShouldBeVisible ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) @@ -327,13 +393,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) // Bubble view - contentViewLeadingConstraint1.isActive = cellViewModel.variant.isIncoming - contentViewLeadingConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing) - contentViewLeadingConstraint2.isActive = cellViewModel.variant.isOutgoing - contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing) - contentViewTrailingConstraint1.isActive = cellViewModel.variant.isOutgoing - contentViewTrailingConstraint2.isActive = cellViewModel.variant.isIncoming - let bubbleBackgroundColor: ThemeValue = (cellViewModel.variant.isIncoming ? .messageBubble_incomingBackground : .messageBubble_outgoingBackground) bubbleView.themeBackgroundColor = bubbleBackgroundColor bubbleBackgroundView.themeBackgroundColor = bubbleBackgroundColor @@ -352,17 +411,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bubbleView.accessibilityLabel = bodyTappableLabel?.attributedText?.string bubbleView.isAccessibilityElement = true - // Author label - authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0) - authorLabel.isHidden = (cellViewModel.senderName == nil) - authorLabel.text = cellViewModel.senderName - authorLabel.themeTextColor = .textPrimary - - let authorLabelAvailableWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * VisibleMessageCell.authorLabelInset) - let authorLabelAvailableSpace = CGSize(width: authorLabelAvailableWidth, height: .greatestFiniteMagnitude) - let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace) - authorLabelHeightConstraint.constant = (cellViewModel.senderName != nil ? authorLabelSize.height : 0) - // Flip horizontally for RTL languages replyIconImageView.transform = CGAffineTransform.identity .scaledBy( @@ -378,13 +426,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { removeGestureRecognizer(panGestureRecognizer) } - // Under bubble content - underBubbleStackView.alignment = (cellViewModel.variant.isOutgoing ?.trailing : .leading) - underBubbleStackViewIncomingLeadingConstraint.isActive = !cellViewModel.variant.isOutgoing - underBubbleStackViewIncomingTrailingConstraint.isActive = !cellViewModel.variant.isOutgoing - underBubbleStackViewOutgoingLeadingConstraint.isActive = cellViewModel.variant.isOutgoing - underBubbleStackViewOutgoingTrailingConstraint.isActive = cellViewModel.variant.isOutgoing - // Reaction view reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty != false) populateReaction( @@ -407,7 +448,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.image = image messageStatusLabel.accessibilityIdentifier = "Message sent status: \(statusText ?? "invalid")" messageStatusImageView.themeTintColor = tintColor - messageStatusContainerView.isHidden = ( + messageStatusStackView.isHidden = ( (cellViewModel.expiresInSeconds ?? 0) == 0 && ( !cellViewModel.variant.isOutgoing || cellViewModel.variant.isDeletedMessage || @@ -418,10 +459,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) ) ) - messageStatusLabelPaddingView.isHidden = ( - messageStatusContainerView.isHidden || - cellViewModel.isLast - ) // Timer if @@ -444,16 +481,24 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.isHidden = false } - timerViewOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing - timerViewIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming - messageStatusLabelOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing - messageStatusLabelIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming + // Hide the underBubbleStackView if all of it's content is hidden + underBubbleStackView.isHidden = !underBubbleStackView.arrangedSubviews.contains { !$0.isHidden } - // Set the height of the underBubbleStackView to 0 if it has no content (need to do this - // otherwise it can randomly stretch) - underBubbleStackViewNoHeightConstraint.isActive = underBubbleStackView.arrangedSubviews - .filter { !$0.isHidden } - .isEmpty + if cellViewModel.variant.isOutgoing { + leadingSpacer.isHidden = false + trailingSpacer.isHidden = true + + snContentView.alignment = .trailing + underBubbleStackView.alignment = .trailing + } + else { + leadingSpacer.isHidden = true + trailingSpacer.isHidden = false + contentHStack.spacing = (cellViewModel.canHaveProfile ? VisibleMessageCell.groupThreadHSpacing : 0) + + snContentView.alignment = .leading + underBubbleStackView.alignment = .leading + } } private func populateContentView( @@ -553,6 +598,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let stackView = UIStackView(arrangedSubviews: []) stackView.axis = .vertical stackView.spacing = 2 + stackView.setContentHugging(.vertical, to: .required) + stackView.setCompressionResistance(.vertical, to: .required) // Quote view if let quote: Quote = cellViewModel.quote { @@ -586,10 +633,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -673,10 +718,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -710,10 +753,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -906,6 +947,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bodyTappableLabel = nil bodyTappableLabelHeight = 0 bodayTappableLabelHeightConstraint = nil + viewsToMoveForReply.forEach { $0.transform = .identity } replyButton.alpha = 0 timerView.prepareForReuse() @@ -1045,7 +1087,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } else if snContentView.bounds.contains(snContentView.convert(location, from: self)) { if !readMoreButton.isHidden && readMoreButton.bounds.contains(readMoreButton.convert(location, from: self)) { - bodayTappableLabelHeightConstraint?.constant = self.bodyTappableLabelHeight + bodyTappableLabel?.numberOfLines = 0 bodyTappableLabel?.invalidateIntrinsicContentSize() readMoreButton.isHidden = true self.bodyContainerStackView?.removeArrangedSubview(readMoreButton) @@ -1363,7 +1405,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { using dependencies: Dependencies ) -> (label: TappableLabel, height: CGFloat) { let result: TappableLabel = TappableLabel() - result.setContentCompressionResistancePriority(.required, for: .vertical) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) result.themeAttributedText = VisibleMessageCell.getBodyAttributedText( for: cellViewModel, textColor: textColor, diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index 7f474ee65c..075c9390c7 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -184,7 +184,8 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ id: .primaryColorPreview, leadingAccessory: .custom( info: ThemeMessagePreviewView.Info() - ) + ), + isEnabled: false ) ] ), diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 70fcf82ea1..66eaf0e948 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -458,35 +458,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav subtitle: SessionCell.TextInfo( "typingIndicatorsDescription".localized(), font: .subtitle, - extraViewGenerator: { - let targetHeight: CGFloat = 20 - let targetWidth: CGFloat = ceil(20 * (targetHeight / 12)) - let result: UIView = UIView( - frame: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight) - ) - result.set(.width, to: targetWidth) - result.set(.height, to: targetHeight) - - // Use a transform scale to reduce the size of the typing indicator to the - // desired size (this way the animation remains intact) - let cell: TypingIndicatorCell = TypingIndicatorCell() - cell.transform = CGAffineTransform( - scaleX: targetHeight / cell.bounds.height, - y: targetHeight / cell.bounds.height - ) - cell.typingIndicatorView.startAnimation() - result.addSubview(cell) - - // Note: Because we are messing with the transform these values don't work - // logically so we inset the positioning to make it look visually centered - // within the layout inspector - cell.center(.vertical, in: result, withInset: -(targetHeight * 0.15)) - cell.center(.horizontal, in: result, withInset: -(targetWidth * 0.35)) - cell.set(.width, to: .width, of: result) - cell.set(.height, to: .height, of: result) - - return result - } + extraViewGenerator: { TypingIndicatorPreviewView() } ), trailingAccessory: .toggle( state.typingIndicatorsEnabled, @@ -550,3 +522,67 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav } } } + +// MARK: - Info + +private final class TypingIndicatorPreviewView: UIView { + static var size: CGSize = CGSize(width: 24, height: 14) + + // MARK: - Components + + private lazy var bubbleView: UIView = { + let result: UIView = UIView() + result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius + result.layer.mask = bubbleViewMaskLayer + result.themeBackgroundColor = .messageBubble_incomingBackground + + return result + }() + + private let bubbleViewMaskLayer: CAShapeLayer = { + let result: CAShapeLayer = CAShapeLayer() + let maskPath: UIBezierPath = UIBezierPath( + roundedRect: CGRect(origin: .zero, size: TypingIndicatorPreviewView.size), + byRoundingCorners: .allCorners, + cornerRadii: CGSize( + width: VisibleMessageCell.largeCornerRadius, + height: VisibleMessageCell.largeCornerRadius + ) + ) + + result.path = maskPath.cgPath + + return result + }() + public lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView() + + // MARK: - Initialization + + init() { + super.init(frame: .zero) + + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + private func setupLayout() { + addSubview(bubbleView) + bubbleView.addSubview(typingIndicatorView) + + set(.width, to: TypingIndicatorPreviewView.size.width) + set(.height, to: TypingIndicatorPreviewView.size.height) + + bubbleView.pin(to: self) + typingIndicatorView.center(in: bubbleView) + + // Use a transform scale to reduce the size of the typing indicator to the + // desired size (this way the animation remains intact) + typingIndicatorView.transform = CGAffineTransform(scaleX: 0.4, y: 0.4) + typingIndicatorView.startAnimation() + } +} diff --git a/Session/Settings/Views/ThemeMessagePreviewView.swift b/Session/Settings/Views/ThemeMessagePreviewView.swift index 166bbab9f7..8ff0d41212 100644 --- a/Session/Settings/Views/ThemeMessagePreviewView.swift +++ b/Session/Settings/Views/ThemeMessagePreviewView.swift @@ -13,8 +13,7 @@ final class ThemeMessagePreviewView: UIView { // MARK: - Components private lazy var incomingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell() - result.translatesAutoresizingMaskIntoConstraints = true + let result: VisibleMessageCell = VisibleMessageCell(isPreview: true) result.update( with: MessageViewModel( variant: .standardIncoming, @@ -34,16 +33,11 @@ final class ThemeMessagePreviewView: UIView { using: dependencies ) - // Remove built-in padding - result.authorLabelTopConstraint.constant = 0 - result.contentViewLeadingConstraint1.constant = 0 - return result }() private lazy var outgoingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell() - result.translatesAutoresizingMaskIntoConstraints = true + let result: VisibleMessageCell = VisibleMessageCell(isPreview: true) result.update( with: MessageViewModel( variant: .standardOutgoing, @@ -58,10 +52,6 @@ final class ThemeMessagePreviewView: UIView { using: dependencies ) - // Remove built-in padding - result.authorLabelTopConstraint.constant = 0 - result.contentViewTrailingConstraint1.constant = 0 - return result }() @@ -91,8 +81,10 @@ final class ThemeMessagePreviewView: UIView { private func setupLayout() { incomingMessagePreview.pin(.top, to: .top, of: self) incomingMessagePreview.pin(.leading, to: .leading, of: self) + incomingMessagePreview.pin(.trailing, to: .trailing, of: self) outgoingMessagePreview.pin(.top, to: .bottom, of: incomingMessagePreview, withInset: Values.mediumSpacing) + outgoingMessagePreview.pin(.leading, to: .leading, of: self) outgoingMessagePreview.pin(.trailing, to: .trailing, of: self) outgoingMessagePreview.pin(.bottom, to: .bottom, of: self) } diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 6ce09450c0..66a926c2d9 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -243,7 +243,7 @@ public enum ThemeManager { switch value { case .value(let value, let alpha): let color: T? = color(for: value, in: theme, with: primaryColor) - return color?.alpha(alpha) + return color?.alpha(alpha) as? T case .primary: return T.resolve(primaryColor) case .explicitPrimary(let explicitPrimary): return T.resolve(explicitPrimary) @@ -252,8 +252,8 @@ public enum ThemeManager { let color: T? = color(for: value, in: theme, with: primaryColor)! switch (currentTheme.interfaceStyle, alwaysDarken) { - case (.light, _), (_, true): return color?.brighten(-0.06) - default: return color?.brighten(0.08) + case (.light, _), (_, true): return color?.brighten(-0.06) as? T + default: return color?.brighten(0.08) as? T } case .dynamicForInterfaceStyle(let light, let dark): @@ -540,21 +540,28 @@ extension Array { // MARK: - ColorType internal protocol ColorType { + /// Apple have done some odd schenanigans with `UIColor` where some types aren't _actually_ `UIColor` but a special + /// type (eg. `UIColor.black` and `UIColor.white` are `UICachedDeviceWhiteColor`), due to this casting to + /// `Self` in an extension on `UIColor` ends up failing (because calling `alpha(_)` on a `UICachedDeviceWhiteColor` + /// expects you to return a `UICachedDeviceWhiteColor`, but the alpha-applied output is a standard `UIColor` which can't + /// convert to `Self`), by defining an explicit `BaseColorType` we return an explicit type and avoid weird private types + associatedtype BaseColorType + var isPrimary: Bool { get } - func alpha(_ alpha: Double) -> Self? - func brighten(_ amount: Double) -> Self? + func alpha(_ alpha: Double) -> BaseColorType? + func brighten(_ amount: Double) -> BaseColorType? } extension UIColor: ColorType { internal var isPrimary: Bool { self == UIColor.primary() } - internal func alpha(_ alpha: Double) -> Self? { - return self.withAlphaComponent(CGFloat(alpha)) as? Self + internal func alpha(_ alpha: Double) -> UIColor? { + return self.withAlphaComponent(CGFloat(alpha)) } - internal func brighten(_ amount: Double) -> Self? { - return self.brighten(by: amount) as? Self + internal func brighten(_ amount: Double) -> UIColor? { + return self.brighten(by: amount) } } From 8a4f08e1f175f3b1db7b37c38a8e75c30b972e7a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Sep 2025 14:03:20 +1000 Subject: [PATCH 236/244] Fixed a bunch of constraint violation warnings --- Session/Conversations/ConversationVC.swift | 1 + .../Message Cells/CallMessageCell.swift | 1 + .../Content Views/MediaLoaderView.swift | 21 +++- .../Content Views/QuoteView.swift | 2 + .../Content Views/ReactionContainerView.swift | 4 +- .../Content Views/ReactionView.swift | 2 +- .../Message Cells/DateHeaderCell.swift | 1 + .../Message Cells/InfoMessageCell.swift | 1 + .../Message Cells/MessageCell.swift | 1 + .../Message Cells/TypingIndicatorCell.swift | 1 + .../Message Cells/UnreadMarkerCell.swift | 1 + .../Message Cells/VisibleMessageCell.swift | 118 +++++++++--------- .../MessageInfoScreen.swift | 7 +- Session/Path/PathStatusView.swift | 5 +- .../Views/ThemeMessagePreviewView.swift | 16 ++- .../Shared/SessionTableViewController.swift | 13 +- .../Shared/Types/SessionCell+Accessory.swift | 16 +-- .../Views/SessionCell+AccessoryView.swift | 55 +++++--- Session/Shared/Views/SessionCell.swift | 43 +++---- Session/Shared/Views/SessionFooterView.swift | 14 +-- Session/Shared/Views/SessionHeaderView.swift | 8 +- .../Components/ProfilePictureView.swift | 1 + SessionUIKit/Components/Separator.swift | 2 + 23 files changed, 188 insertions(+), 146 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 4296ed5813..671fbaa391 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -1719,6 +1719,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa shouldExpanded: viewModel.messageExpandedInteractionIds .contains(cellViewModel.id), lastSearchText: viewModel.lastSearchedText, + tableSize: tableView.bounds.size, using: viewModel.dependencies ) cell.delegate = self diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 25d3aeb47a..75fbbfa946 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -130,6 +130,7 @@ final class CallMessageCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard diff --git a/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift b/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift index b8a56e1d1e..ba98b62b81 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift @@ -5,9 +5,12 @@ import SessionUIKit final class MediaLoaderView: UIView { private let bar = UIView() + private var cachedWidth: CGFloat = 0 private lazy var barLeftConstraint = bar.pin(.left, to: .left, of: self) - private lazy var barRightConstraint = bar.pin(.right, to: .right, of: self) + private lazy var barRightConstraint = bar + .pin(.right, to: .right, of: self) + .setting(priority: .defaultHigh) // MARK: - Lifecycle @@ -30,14 +33,22 @@ final class MediaLoaderView: UIView { barLeftConstraint.isActive = true bar.pin(.top, to: .top, of: self) barRightConstraint.isActive = true - bar.pin(.bottom, to: .bottom, of: self) + bar.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) step1() } + override func layoutSubviews() { + super.layoutSubviews() + + if cachedWidth != bounds.width { + cachedWidth = bounds.width + } + } + // MARK: - Animation func step1() { - barRightConstraint.constant = -bounds.width + barRightConstraint.constant = -cachedWidth UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } self.barRightConstraint.constant = 0 @@ -51,7 +62,7 @@ final class MediaLoaderView: UIView { barLeftConstraint.constant = 0 UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } - self.barLeftConstraint.constant = self.bounds.width + self.barLeftConstraint.constant = cachedWidth self.layoutIfNeeded() }, completion: { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in @@ -75,7 +86,7 @@ final class MediaLoaderView: UIView { barRightConstraint.constant = 0 UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } - self.barRightConstraint.constant = -self.bounds.width + self.barRightConstraint.constant = -cachedWidth self.layoutIfNeeded() }, completion: { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index d5248fefbf..ecc93aef65 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -90,6 +90,7 @@ final class QuoteView: UIView { mainStackView.isLayoutMarginsRelativeArrangement = true mainStackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: smallSpacing) mainStackView.alignment = .center + mainStackView.setCompressionResistance(.vertical, to: .required) // Content view let contentView = UIView() @@ -228,6 +229,7 @@ final class QuoteView: UIView { labelStackView.distribution = .equalCentering labelStackView.isLayoutMarginsRelativeArrangement = true labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0) + labelStackView.setCompressionResistance(.vertical, to: .required) mainStackView.addArrangedSubview(labelStackView) // Constraints diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 80f11e7122..35cf6343e3 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -107,7 +107,7 @@ final class ReactionContainerView: UIView { mainStackView.pin(.top, to: .top, of: self) mainStackView.pin(.leading, to: .leading, of: self) - mainStackView.pin(.trailing, to: .trailing, of: self) + mainStackView.pin(.trailing, to: .trailing, of: self).setting(priority: .defaultHigh) mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -Values.verySmallSpacing) reactionContainerView.set(.width, to: .width, of: mainStackView) collapseButton.set(.width, to: .width, of: mainStackView) @@ -125,6 +125,8 @@ final class ReactionContainerView: UIView { self.reactionViews = [] self.reactionContainerView.arrangedSubviews.forEach { $0.removeFromSuperview() } + guard !reactions.isEmpty else { return } + let collapsedCount: Int = { // If there are already more than 'maxEmojiBeforeCollapse' then no need to calculate, just // always collapse diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 6a24fcc001..2250290531 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -152,6 +152,6 @@ final class ExpandingReactionButton: UIView { rightMargin += margin } - set(.width, to: rightMargin - margin + size) + set(.width, to: rightMargin - margin + size).setting(priority: .defaultHigh) } } diff --git a/Session/Conversations/Message Cells/DateHeaderCell.swift b/Session/Conversations/Message Cells/DateHeaderCell.swift index 352f49359e..f06c123333 100644 --- a/Session/Conversations/Message Cells/DateHeaderCell.swift +++ b/Session/Conversations/Message Cells/DateHeaderCell.swift @@ -45,6 +45,7 @@ final class DateHeaderCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .dateHeader else { return } diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index a5196a0b88..e21578ad1d 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -93,6 +93,7 @@ final class InfoMessageCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.variant.isInfoMessage else { return } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index ec40f8e72e..cbb7caa696 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -89,6 +89,7 @@ public class MessageCell: UITableViewCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { preconditionFailure("Must be overridden by subclasses.") diff --git a/Session/Conversations/Message Cells/TypingIndicatorCell.swift b/Session/Conversations/Message Cells/TypingIndicatorCell.swift index 6600b22898..42b530ffc3 100644 --- a/Session/Conversations/Message Cells/TypingIndicatorCell.swift +++ b/Session/Conversations/Message Cells/TypingIndicatorCell.swift @@ -46,6 +46,7 @@ final class TypingIndicatorCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .typingIndicator else { return } diff --git a/Session/Conversations/Message Cells/UnreadMarkerCell.swift b/Session/Conversations/Message Cells/UnreadMarkerCell.swift index 4d33afe6b6..12313e50d5 100644 --- a/Session/Conversations/Message Cells/UnreadMarkerCell.swift +++ b/Session/Conversations/Message Cells/UnreadMarkerCell.swift @@ -66,6 +66,7 @@ final class UnreadMarkerCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .unreadMarker else { return } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 15284f7e42..0e89a38882 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -9,7 +9,6 @@ import SessionMessagingKit final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private static let maxNumberOfLinesAfterTruncation: Int = 25 - private var isPreview: Bool = false private var isHandlingLongPress: Bool = false private var previousX: CGFloat = 0 @@ -42,6 +41,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // MARK: - UI Components + private lazy var contentHStackTopConstraint: NSLayoutConstraint = + contentHStack.pin(.top, to: .top, of: contentView) + private lazy var viewsToMoveForReply: [UIView] = [ snContentView, profilePictureView, @@ -78,7 +80,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { dataManager: nil ) - lazy var contentHStack: UIStackView = { + public lazy var contentHStack: UIStackView = { let result: UIStackView = UIStackView( arrangedSubviews: [leadingSpacer, profilePictureViewContainer, mainVStack, trailingSpacer] ) @@ -267,30 +269,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { enum Direction { case incoming, outgoing } - // MARK: - Initialization - - @MainActor init(isPreview: Bool) { - self.isPreview = isPreview - - super.init(style: .default, reuseIdentifier: nil) - - /// When a `UITableViewCell` is added as a subview instead of rendered as a cell directly within a `UITableView` the - /// `contentView` doesn't have constraints (because the table manages those) so we need to manually add them - contentView.pin(to: self) - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - @MainActor required init?(coder: NSCoder) { - super.init(coder: coder) - } - // MARK: - Lifecycle override func setUpViewHierarchy() { super.setUpViewHierarchy() + contentView.addSubview(contentHStack) contentView.addSubview(replyButton) @@ -299,18 +282,18 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { replyButton.addSubview(replyIconImageView) - contentHStack.pin(.top, to: .top, of: contentView) + contentHStackTopConstraint.isActive = true contentHStack.pin( .leading, to: .leading, of: contentView, - withInset: (isPreview ? 0 : VisibleMessageCell.contactThreadHSpacing) + withInset: VisibleMessageCell.contactThreadHSpacing ) contentHStack.pin( .trailing, to: .trailing, of: contentView, - withInset: (isPreview ? 0 : -VisibleMessageCell.contactThreadHSpacing) + withInset: -VisibleMessageCell.contactThreadHSpacing ) contentHStack .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.verySmallSpacing) @@ -341,6 +324,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { self.dependencies = dependencies @@ -355,12 +339,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { cellViewModel.isOnlyMessageInCluster ) ) - contentHStack.layoutMargins = UIEdgeInsets( - top: (shouldAddTopInset ? Values.mediumSpacing : 0), - left: 0, - bottom: 0, - right: 0 - ) + contentHStackTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0) // Author label authorLabel.isHidden = (cellViewModel.senderName == nil) @@ -404,6 +383,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { playbackInfo: playbackInfo, shouldExpanded: shouldExpanded, lastSearchText: lastSearchText, + tableSize: tableSize, using: dependencies ) @@ -432,6 +412,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { for: cellViewModel, maxWidth: VisibleMessageCell.getMaxWidth( for: cellViewModel, + cellWidth: tableSize.width, includingOppositeGutter: false ), showExpandedReactions: showExpandedReactions @@ -506,6 +487,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { playbackInfo: ConversationViewModel.PlaybackInfo?, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { let bodyLabelTextColor: ThemeValue = (cellViewModel.variant.isOutgoing ? @@ -540,7 +522,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let deletedMessageView: DeletedMessageView = DeletedMessageView( textColor: bodyLabelTextColor, variant: cellViewModel.variant, - maxWidth: (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + maxWidth: ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset + ) ) bubbleView.addSubview(deletedMessageView) deletedMessageView.pin(to: bubbleView) @@ -553,7 +540,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // FIXME: We should support rendering link previews alongside the other variants (bigger refactor) guard cellViewModel.cellType != .textOnlyMessage else { let inset: CGFloat = 12 - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset) if let linkPreview: LinkPreview = cellViewModel.linkPreview { switch linkPreview.variant { @@ -678,7 +669,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { /// Add any quote & body if present let inset: CGFloat = 12 - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset + ) switch (cellViewModel.quote, cellViewModel.body) { /// Both quote and body @@ -801,7 +797,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { case .mediaMessage: // Album view - let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel) + let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) let albumView = MediaAlbumView( items: (cellViewModel.attachments? .filter { $0.isVisualMedia }) @@ -811,7 +810,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { using: dependencies ) self.albumView = albumView - let size = getSize(for: cellViewModel) + let size = getSize(for: cellViewModel, tableSize: tableSize) albumView.set(.width, to: size.width) albumView.set(.height, to: size.height) albumView.accessibilityLabel = "contentDescriptionMediaMessage".localized() @@ -1201,12 +1200,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return CGFloat(maxNumberOfLinesAfterTruncation) * UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)).lineHeight } - private func getSize(for cellViewModel: MessageViewModel) -> CGSize { + private func getSize(for cellViewModel: MessageViewModel, tableSize: CGSize) -> CGSize { guard let mediaAttachments: [Attachment] = cellViewModel.attachments?.filter({ $0.isVisualMedia }) else { preconditionFailure() } - let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: cellViewModel) + let maxMessageWidth = VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) let defaultSize = MediaAlbumView.layoutSize(forMaxMessageWidth: maxMessageWidth, items: mediaAttachments) guard @@ -1246,27 +1248,29 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return CGSize(width: finalWidth, height: finalHeight) } - static func getMaxWidth(for cellViewModel: MessageViewModel, includingOppositeGutter: Bool = true) -> CGFloat { - let screen: CGRect = UIScreen.main.bounds - let width: CGFloat = UIDevice.current.isIPad ? screen.width * 0.75 : screen.width + static func getMaxWidth( + for cellViewModel: MessageViewModel, + cellWidth: CGFloat, + includingOppositeGutter: Bool = true + ) -> CGFloat { + let horizontalPadding: CGFloat = (contactThreadHSpacing * 2) + let isGroupThread: Bool = ( + cellViewModel.threadVariant == .community || + cellViewModel.threadVariant == .legacyGroup || + cellViewModel.threadVariant == .group + ) + let profileSpace: CGFloat = { + guard + cellViewModel.variant.isIncoming, + isGroupThread, + cellViewModel.canHaveProfile + else { return 0 } + + return ProfilePictureView.Size.message.viewSize + groupThreadHSpacing + }() let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing) - switch cellViewModel.variant { - case .standardOutgoing, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: - return (width - contactThreadHSpacing - oppositeEdgePadding) - - case .standardIncoming, .standardIncomingDeleted, .standardIncomingDeletedLocally: - let isGroupThread = ( - cellViewModel.threadVariant == .community || - cellViewModel.threadVariant == .legacyGroup || - cellViewModel.threadVariant == .group - ) - let leftEdgeGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) - - return (width - leftEdgeGutterSize - oppositeEdgePadding) - - default: preconditionFailure() - } + return (cellWidth - horizontalPadding - profileSpace - oppositeEdgePadding) } // stringlint:ignore_contents diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 9456f8dc42..6c42a3751c 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -456,7 +456,12 @@ struct MessageBubble: View { var body: some View { ZStack { - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: messageViewModel, + cellWidth: UIScreen.main.bounds.width + ) - 2 * Self.inset + ) VStack( alignment: .leading, diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 33108de585..bd3eefe57b 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -115,10 +115,7 @@ final class PathStatusViewAccessory: UIView, SessionCell.Accessory.CustomView { /// We want the path status to have the same sizing as other list item icons so it needs to be wrapped in /// this contains view - public static let size: SessionCell.Accessory.Size = .fixed( - width: IconSize.medium.size, - height: IconSize.medium.size - ) + public static let size: SessionCell.Accessory.Size = .minWidth(height: IconSize.medium.size) static func create(maxContentWidth: CGFloat, using dependencies: Dependencies) -> PathStatusViewAccessory { return PathStatusViewAccessory(using: dependencies) diff --git a/Session/Settings/Views/ThemeMessagePreviewView.swift b/Session/Settings/Views/ThemeMessagePreviewView.swift index 8ff0d41212..e82f5b444a 100644 --- a/Session/Settings/Views/ThemeMessagePreviewView.swift +++ b/Session/Settings/Views/ThemeMessagePreviewView.swift @@ -13,8 +13,8 @@ final class ThemeMessagePreviewView: UIView { // MARK: - Components private lazy var incomingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell(isPreview: true) - result.update( + let cell: VisibleMessageCell = VisibleMessageCell() + cell.update( with: MessageViewModel( variant: .standardIncoming, body: "appearancePreview2".localized(), @@ -30,15 +30,17 @@ final class ThemeMessagePreviewView: UIView { showExpandedReactions: false, shouldExpanded: false, lastSearchText: nil, + tableSize: UIScreen.main.bounds.size, using: dependencies ) + cell.contentHStack.removeFromSuperview() - return result + return cell.contentHStack }() private lazy var outgoingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell(isPreview: true) - result.update( + let cell: VisibleMessageCell = VisibleMessageCell() + cell.update( with: MessageViewModel( variant: .standardOutgoing, body: "appearancePreview3".localized(), @@ -49,10 +51,12 @@ final class ThemeMessagePreviewView: UIView { showExpandedReactions: false, shouldExpanded: false, lastSearchText: nil, + tableSize: UIScreen.main.bounds.size, using: dependencies ) + cell.contentHStack.removeFromSuperview() - return result + return cell.contentHStack }() // MARK: - Initializtion diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index 8e0119bd9a..087091c28f 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -95,6 +95,8 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa result.dataSource = self result.delegate = self result.sectionHeaderTopPadding = 0 + result.rowHeight = UITableView.automaticDimension + result.estimatedRowHeight = 56 // Approximate size of an [{Icon} {Text}] SessionCell return result }() @@ -452,8 +454,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa UIView.setAnimationsEnabled(false) cell.setNeedsLayout() cell.layoutIfNeeded() - tableView.beginUpdates() - tableView.endUpdates() + tableView.performBatchUpdates(nil) // Only re-enable animations if the feature flag isn't disabled if dependencies[feature: .animationsEnabled] { UIView.setAnimationsEnabled(true) @@ -510,14 +511,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa return (section.model.footer == nil ? 0 : UITableView.automaticDimension) } - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { guard self.hasLoadedInitialTableData && self.viewHasAppeared && !self.isLoadingMore else { return } diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index 887a554092..63025785d7 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -580,6 +580,8 @@ public extension SessionCell.AccessoryConfig { public let additionalProfile: Profile? public let additionalProfileIcon: ProfilePictureView.ProfileIcon + override public var shouldFitToEdge: Bool { true } + fileprivate init( id: String, size: ProfilePictureView.Size, @@ -754,6 +756,7 @@ public extension SessionCell.AccessoryConfig { public extension SessionCell.Accessory { enum Size { case fixed(width: CGFloat, height: CGFloat) + case minWidth(height: CGFloat) case fillWidth(height: CGFloat) case fillWidthWrapHeight } @@ -779,19 +782,6 @@ public extension SessionCell.Accessory.CustomViewInfo { let view: View = View.create(maxContentWidth: maxContentWidth, using: dependencies) view.update(with: self) - switch View.size { - case .fixed(let width, let height): - view.set(.width, to: width) - view.set(.height, to: height) - - case .fillWidth(let height): - view.set(.height, to: height) - - case .fillWidthWrapHeight: - view.setContentHugging(.vertical, to: .required) - view.setCompressionResistance(.vertical, to: .required) - } - return view } } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index ef3e66cf9e..a3815587ae 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -21,7 +21,9 @@ extension SessionCell { private lazy var minWidthConstraint: NSLayoutConstraint = self.widthAnchor .constraint(greaterThanOrEqualToConstant: AccessoryView.minWidth) - private lazy var fixedWidthConstraint: NSLayoutConstraint = self.set(.width, to: AccessoryView.minWidth) + private lazy var fixedWidthConstraint: NSLayoutConstraint = self + .set(.width, to: AccessoryView.minWidth) + .setting(priority: .defaultHigh) // MARK: - Content @@ -302,17 +304,27 @@ extension SessionCell { imageView.set(.width, to: iconSize.size) imageView.set(.height, to: iconSize.size) imageView.pin(.top, to: .top, of: self) - imageView.pin(.bottom, to: .bottom, of: self) + imageView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) - let shouldInvertPadding: [UIView.HorizontalEdge] = [.left, .trailing] + let edgeSet: Set = Set(edges) + let hasLeadingPin = (edgeSet.contains(.leading) || edgeSet.contains(.left)) + let hasTrailingPin = (edgeSet.contains(.trailing) || edgeSet.contains(.right)) - for edge in edges { - let inset: CGFloat = ( - (shouldFill ? 0 : Values.smallSpacing) * - (shouldInvertPadding.contains(edge) ? -1 : 1) - ) + /// If we want to pin to both edges then we should actually center instead (otherwise this will cause constraint violations) + if hasLeadingPin && hasTrailingPin { + imageView.center(.horizontal, in: self) + } + else { + let shouldInvertPadding: [UIView.HorizontalEdge] = [.right, .trailing] - imageView.pin(edge, to: edge, of: self, withInset: inset) + for edge in edges { + let inset: CGFloat = ( + (shouldFill ? 0 : Values.smallSpacing) * + (shouldInvertPadding.contains(edge) ? -1 : 1) + ) + + imageView.pin(edge, to: edge, of: self, withInset: inset) + } } fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) @@ -483,6 +495,7 @@ extension SessionCell { radioBorderView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) radioBorderView.pin(.bottom, to: .bottom, of: self) + .setting(priority: .defaultHigh) } private func configureRadioView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Radio, isEnabled: Bool) { @@ -670,7 +683,11 @@ extension SessionCell { private func layoutDisplayPictureView(_ view: UIView?, size: ProfilePictureView.Size) { guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return } - profilePictureView.pin(to: self) + profilePictureView.size = size + profilePictureView.pin(.top, to: .top, of: self) + profilePictureView.pin(.leading, to: .leading, of: self) + profilePictureView.pin(.trailing, to: .trailing, of: self) + profilePictureView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) fixedWidthConstraint.constant = size.viewSize fixedWidthConstraint.isActive = true } @@ -682,12 +699,9 @@ extension SessionCell { ) { guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return } - // Note: We MUST set the 'size' property before triggering the 'update' - // function or the profile picture won't layout correctly profilePictureView.accessibilityIdentifier = accessory.accessibility?.identifier profilePictureView.accessibilityLabel = accessory.accessibility?.label profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) - profilePictureView.size = accessory.size profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) profilePictureView.update( publicKey: accessory.id, @@ -743,7 +757,10 @@ extension SessionCell { private func layoutButtonView(_ view: UIView?) { guard let button: SessionButton = view as? SessionButton else { return } - button.pin(to: self) + button.pin(.top, to: .top, of: self) + button.pin(.leading, to: .leading, of: self) + button.pin(.trailing, to: .trailing, of: self) + button.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) minWidthConstraint.isActive = true } @@ -769,6 +786,11 @@ extension SessionCell { view.set(.height, to: height) fixedWidthConstraint.isActive = (width <= fixedWidthConstraint.constant) minWidthConstraint.isActive = !fixedWidthConstraint.isActive + + case .minWidth(let height): + view.set(.width, to: .width, of: self) + view.set(.height, to: height) + fixedWidthConstraint.isActive = true case .fillWidth(let height): view.set(.width, to: .width, of: self) @@ -782,7 +804,10 @@ extension SessionCell { minWidthConstraint.isActive = true } - view.pin(to: self) + view.pin(.top, to: .top, of: self) + view.pin(.leading, to: .leading, of: self) + view.pin(.trailing, to: .trailing, of: self) + view.pin(.bottom, to: .bottom, of: self) } private func configureCustomView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.AnyCustom) { diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index f30fd2c3cf..768773ff44 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -34,7 +34,9 @@ public class SessionCell: UITableViewCell { private lazy var contentStackViewWidthConstraint: NSLayoutConstraint = contentStackView.set(.width, lessThanOrEqualTo: .width, of: cellBackgroundView) private lazy var leadingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leadingAccessoryView) private lazy var trailingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: trailingAccessoryView) - private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView.set(.width, to: .width, of: trailingAccessoryView) + private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView + .set(.width, to: .width, of: trailingAccessoryView) + .setting(priority: .defaultHigh) private let cellBackgroundView: UIView = { let result: UIView = UIView() @@ -84,10 +86,10 @@ public class SessionCell: UITableViewCell { let result: UIStackView = UIStackView() result.translatesAutoresizingMaskIntoConstraints = false result.axis = .vertical - result.distribution = .equalSpacing + result.distribution = .fill result.alignment = .fill - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -98,8 +100,8 @@ public class SessionCell: UITableViewCell { result.isUserInteractionEnabled = false result.themeTextColor = .textPrimary result.numberOfLines = 0 - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -112,8 +114,8 @@ public class SessionCell: UITableViewCell { result.themeTextColor = .textPrimary result.numberOfLines = 0 result.isHidden = true - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -126,8 +128,8 @@ public class SessionCell: UITableViewCell { result.numberOfLines = 0 result.maxNumberOfLines = 3 result.isHidden = true - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -173,11 +175,11 @@ public class SessionCell: UITableViewCell { contentStackView.addArrangedSubview(leadingAccessoryView) contentStackView.addArrangedSubview(titleStackView) - contentStackView.addArrangedSubview(expandableDescriptionLabel) contentStackView.addArrangedSubview(trailingAccessoryView) titleStackView.addArrangedSubview(titleLabel) titleStackView.addArrangedSubview(subtitleLabel) + titleStackView.addArrangedSubview(expandableDescriptionLabel) setupLayout() } @@ -186,7 +188,9 @@ public class SessionCell: UITableViewCell { cellBackgroundView.pin(.top, to: .top, of: contentView) backgroundLeftConstraint = cellBackgroundView.pin(.leading, to: .leading, of: contentView) backgroundRightConstraint = cellBackgroundView.pin(.trailing, to: .trailing, of: contentView) - cellBackgroundView.pin(.bottom, to: .bottom, of: contentView) + cellBackgroundView + .pin(.bottom, to: .bottom, of: contentView) + .setting(priority: .defaultHigh) cellSelectedBackgroundView.pin(to: cellBackgroundView) @@ -257,6 +261,7 @@ public class SessionCell: UITableViewCell { // Remove and re-add the 'subtitleExtraView' to clear any old constraints targetView.removeFromSuperview() contentView.addSubview(targetView) + targetView.layoutIfNeeded() targetView.pin( .top, @@ -268,7 +273,7 @@ public class SessionCell: UITableViewCell { .leading, to: .leading, of: label, - withInset: lastGlyphRect.maxX + withInset: lastGlyphRect.maxX + 2 // Padding ) } @@ -342,7 +347,7 @@ public class SessionCell: UITableViewCell { // Layout (do this before setting up the content so we can calculate the expected widths if needed) contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading) - contentStackViewTrailingConstraint.isActive = (info.styling.alignment == .leading) + contentStackViewTrailingConstraint.isActive = contentStackViewLeadingConstraint.isActive contentStackViewHorizontalCenterConstraint.constant = ((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) contentStackViewHorizontalCenterConstraint.isActive = (info.styling.alignment == .centerHugging) contentStackViewWidthConstraint.constant = -(abs((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) * 2) // Double the center offset to keep within bounds @@ -355,14 +360,6 @@ public class SessionCell: UITableViewCell { default: return false } }() - titleLabel.setContentHuggingPriority( - (info.trailingAccessory != nil ? .defaultLow : .required), - for: .horizontal - ) - titleLabel.setContentCompressionResistancePriority( - (info.trailingAccessory != nil ? .defaultLow : .required), - for: .horizontal - ) contentStackViewTopConstraint.constant = { if let customPadding: CGFloat = info.styling.customPadding?.top { return customPadding @@ -521,7 +518,7 @@ public class SessionCell: UITableViewCell { maxContentWidth: (tableSize.width - contentStackViewHorizontalInset), using: dependencies ) - titleStackView.isHidden = (info.title == nil && info.subtitle == nil) + titleStackView.isHidden = (info.title == nil && info.subtitle == nil && info.description == nil) titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) titleLabel.font = info.title?.font titleLabel.text = info.title?.text diff --git a/Session/Shared/Views/SessionFooterView.swift b/Session/Shared/Views/SessionFooterView.swift index ce4c09f9e7..a5c08c8449 100644 --- a/Session/Shared/Views/SessionFooterView.swift +++ b/Session/Shared/Views/SessionFooterView.swift @@ -4,11 +4,6 @@ import UIKit import SessionUIKit class SessionFooterView: UITableViewHeaderFooterView { - private lazy var emptyHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(equalToConstant: (Values.verySmallSpacing * 2)) - private lazy var filledHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(greaterThanOrEqualToConstant: Values.mediumSpacing) - // MARK: - UI private let stackView: UIStackView = { @@ -53,7 +48,12 @@ class SessionFooterView: UITableViewHeaderFooterView { } private func setupLayout() { - stackView.pin(to: self) + stackView.pin(.top, to: .top, of: self) + stackView.pin(.leading, to: .leading, of: self) + stackView.pin(.trailing, to: .trailing, of: self) + .setting(priority: .defaultHigh) + stackView.pin(.bottom, to: .bottom, of: self) + .setting(priority: .defaultHigh) } // MARK: - Content @@ -81,8 +81,6 @@ class SessionFooterView: UITableViewHeaderFooterView { bottom: (titleIsEmpty ? Values.verySmallSpacing : Values.mediumSpacing), right: edgePadding ) - emptyHeightConstraint.isActive = titleIsEmpty - filledHeightConstraint.isActive = !titleIsEmpty self.layoutIfNeeded() } diff --git a/Session/Shared/Views/SessionHeaderView.swift b/Session/Shared/Views/SessionHeaderView.swift index b845d027bb..0cf98554bf 100644 --- a/Session/Shared/Views/SessionHeaderView.swift +++ b/Session/Shared/Views/SessionHeaderView.swift @@ -61,14 +61,18 @@ class SessionHeaderView: UITableViewHeaderFooterView { private func setupLayout() { titleLabel.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing) titleLabelLeadingConstraint = titleLabel.pin(.leading, to: .leading, of: contentView) - titleLabelTrailingConstraint = titleLabel.pin(.trailing, to: .trailing, of: contentView) + titleLabelTrailingConstraint = titleLabel + .pin(.trailing, to: .trailing, of: contentView) + .setting(priority: .defaultHigh) titleLabel .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.mediumSpacing) .setting(priority: .defaultHigh) titleSeparator.center(.vertical, in: contentView) titleSeparatorLeadingConstraint = titleSeparator.pin(.leading, to: .leading, of: contentView) - titleSeparatorTrailingConstraint = titleSeparator.pin(.trailing, to: .trailing, of: contentView) + titleSeparatorTrailingConstraint = titleSeparator + .pin(.trailing, to: .trailing, of: contentView) + .setting(priority: .defaultHigh) loadingIndicator.center(in: contentView) } diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index f7da0d828c..34669ea476 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -303,6 +303,7 @@ public final class ProfilePictureView: UIView { widthConstraint = self.set(.width, to: self.size.viewSize) heightConstraint = self.set(.height, to: self.size.viewSize) + .setting(priority: .defaultHigh) imageViewTopConstraint = imageContainerView.pin(.top, to: .top, of: self) imageViewLeadingConstraint = imageContainerView.pin(.leading, to: .leading, of: self) diff --git a/SessionUIKit/Components/Separator.swift b/SessionUIKit/Components/Separator.swift index 3230665d81..a4ccd6296d 100644 --- a/SessionUIKit/Components/Separator.swift +++ b/SessionUIKit/Components/Separator.swift @@ -73,6 +73,8 @@ public final class Separator: UIView { titleLabel.pin(.trailing, to: .trailing, of: roundedLine, withInset: -10) titleLabel.pin(.bottom, to: .bottom, of: roundedLine, withInset: -6) + roundedLine.pin(.top, to: .top, of: self) + roundedLine.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) roundedLine.center(.horizontal, in: self) roundedLine.center(.vertical, in: self) roundedLine.setContentHugging(.horizontal, to: .required) From 237e4d77ee6215aff91a43e7a140ed0a840c639c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Sep 2025 14:03:53 +1000 Subject: [PATCH 237/244] Fixed an issue where theming was delayed until the next run loop breaking layouts --- SessionUIKit/Style Guide/ThemeManager.swift | 14 ++++++-------- .../Style Guide/Themes/UIKit+Theme.swift | 16 ++++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 66a926c2d9..d5faf2f774 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -221,7 +221,7 @@ public enum ThemeManager { SNUIKit.mainWindow?.backgroundColor = color(for: .backgroundPrimary, in: currentTheme, with: primaryColor) } - public static func onThemeChange(observer: AnyObject, callback: @escaping @MainActor (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { + @MainActor public static func onThemeChange(observer: AnyObject, callback: @escaping @MainActor (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { ThemeManager.uiRegistry.setObject( ThemeApplier( existingApplier: ThemeManager.get(for: observer), @@ -311,7 +311,7 @@ public enum ThemeManager { } } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemeValue? @@ -347,7 +347,7 @@ public enum ThemeManager { ThemeManager.uiRegistry.setObject(updatedApplier, forKey: view) } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemeValue? @@ -381,7 +381,7 @@ public enum ThemeManager { ) } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemedAttributedString? @@ -460,7 +460,7 @@ internal class ThemeApplier { private let info: [AnyHashable] private var otherAppliers: [ThemeApplier]? - init( + @MainActor init( existingApplier: ThemeApplier?, info: [AnyHashable], applyTheme: @escaping @MainActor (Theme) -> () @@ -478,9 +478,7 @@ internal class ThemeApplier { // Automatically apply the theme immediately (if the database has been setup) if SNUIKit.config?.isStorageValid == true || ThemeManager.hasLoadedTheme { - Task { @MainActor [weak self] in - self?.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) - } + apply(theme: ThemeManager.currentTheme, isInitialApplication: true) } } diff --git a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift index d956e82826..885320824d 100644 --- a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift +++ b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift @@ -389,7 +389,7 @@ public extension GradientView { } public extension CAShapeLayer { - var themeStrokeColor: ThemeValue? { + @MainActor var themeStrokeColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.strokeColor, to: newValue) } get { return nil } } @@ -413,7 +413,7 @@ public extension CAShapeLayer { get { return self.strokeColor.map { .color(UIColor(cgColor: $0)) } } } - var themeFillColor: ThemeValue? { + @MainActor var themeFillColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.fillColor, to: newValue) } get { return nil } } @@ -439,7 +439,7 @@ public extension CAShapeLayer { } public extension CALayer { - var themeBackgroundColor: ThemeValue? { + @MainActor var themeBackgroundColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.backgroundColor, to: newValue) } get { return nil } } @@ -463,19 +463,19 @@ public extension CALayer { get { return self.backgroundColor.map { .color(UIColor(cgColor: $0)) } } } - var themeBorderColor: ThemeValue? { + @MainActor var themeBorderColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.borderColor, to: newValue) } get { return nil } } - var themeShadowColor: ThemeValue? { + @MainActor var themeShadowColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.shadowColor, to: newValue) } get { return nil } } } public extension CATextLayer { - var themeForegroundColor: ThemeValue? { + @MainActor var themeForegroundColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.foregroundColor, to: newValue) } get { return nil } } @@ -522,7 +522,7 @@ extension AttributedTextAssignable { get { attributedTextValue.map { ThemedAttributedString(attributedString: $0) } } set { attributedTextValue = newValue?.value } } - public var themeAttributedText: ThemedAttributedString? { + @MainActor public var themeAttributedText: ThemedAttributedString? { set { ThemeManager.set(self, keyPath: \.themeAttributedTextValue, to: newValue) } get { return nil } } @@ -534,7 +534,7 @@ extension UITextField: DirectAttributedTextAssignable { get { attributedPlaceholder.map { ThemedAttributedString(attributedString: $0) } } set { attributedPlaceholder = newValue?.value } } - public var themeAttributedPlaceholder: ThemedAttributedString? { + @MainActor public var themeAttributedPlaceholder: ThemedAttributedString? { set { ThemeManager.set(self, keyPath: \.themeAttributedPlaceholderValue, to: newValue) } get { return nil } } From e0e95f69e7f9ed81d8920fe5124c8dae6cf6f302 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Sep 2025 14:05:44 +1000 Subject: [PATCH 238/244] Bumped version number --- Session.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 705fb6d0b5..76524cc17d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8320,7 +8320,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 636; + CURRENT_PROJECT_VERSION = 639; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8401,7 +8401,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 636; + CURRENT_PROJECT_VERSION = 639; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8882,7 +8882,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 636; + CURRENT_PROJECT_VERSION = 639; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9469,7 +9469,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 636; + CURRENT_PROJECT_VERSION = 639; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From bdd702b9772052862e9df245d6c53fb564b0cb7d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Sep 2025 14:36:39 +1000 Subject: [PATCH 239/244] Change to use child tmp dir instead of main one for proper cleanup --- SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift b/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift index 962970910e..4a9b41b448 100644 --- a/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift @@ -55,7 +55,7 @@ public extension AVURLAsset { finalExtension = fileExtension } - let tmpPath: String = URL(fileURLWithPath: NSTemporaryDirectory()) + let tmpPath: String = URL(fileURLWithPath: dependencies[singleton: .fileManager].temporaryDirectory) .appendingPathComponent(URL(fileURLWithPath: path).lastPathComponent) .appendingPathExtension(finalExtension) .path From 97d44c1bdf670ccd681992ff7fdb13fb433a89b3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Sep 2025 15:54:38 +1000 Subject: [PATCH 240/244] Tweaked accessibility --- Session/Conversations/Message Cells/VisibleMessageCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0e89a38882..20d64ebed9 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -813,6 +813,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let size = getSize(for: cellViewModel, tableSize: tableSize) albumView.set(.width, to: size.width) albumView.set(.height, to: size.height) + albumView.isAccessibilityElement = true albumView.accessibilityLabel = "contentDescriptionMediaMessage".localized() snContentView.addArrangedSubview(albumView) From b446bed3d7705b4cfe1b2227c668a45dac921df5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 1 Oct 2025 08:54:15 +1000 Subject: [PATCH 241/244] Bumped version number --- Session.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 76524cc17d..e16be1e309 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8320,7 +8320,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 639; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8401,7 +8401,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 639; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8882,7 +8882,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 639; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9469,7 +9469,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 639; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From 610133885271321e6e499eb9a45038959ccb1177 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 1 Oct 2025 09:10:45 +1000 Subject: [PATCH 242/244] bump up libsession version --- Session.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d06be6e08f..a9e5c4e467 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -10456,7 +10456,7 @@ repositoryURL = "https://github.com/session-foundation/libsession-util-spm"; requirement = { kind = exactVersion; - version = 1.5.5; + version = 1.5.6; }; }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4c3ec45ea4..36a72ad9fd 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/libsession-util-spm", "state" : { - "revision" : "05ba2d3194726058f801987775be81f61a52be7d", - "version" : "1.5.5" + "revision" : "a092eb8fa4bbc93756530e08b6c281d9eda06c61", + "version" : "1.5.6" } }, { From fef1194742ee8cee3ccf0ac10f70748f9ab6f3ef Mon Sep 17 00:00:00 2001 From: mpretty-cyro <15862619+mpretty-cyro@users.noreply.github.com> Date: Wed, 1 Oct 2025 03:15:23 +0000 Subject: [PATCH 243/244] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 3784 +++++++++++++++-- 1 file changed, 3539 insertions(+), 245 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 77b4fd6490..929defed6a 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -18335,6 +18335,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνεται προαγωγή διαχειριστή" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνονται προαγωγές διαχειριστή" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -18447,6 +18475,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administraatori edutamine" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administraatori edutamine on saatmisel" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -21016,6 +21072,12 @@ "value" : "Automatische nachtmodus" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczny tryb ciemny" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -31123,12 +31185,24 @@ "value" : "{app_pro} Badge" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {app_pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Badge" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaka {app_pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -65452,12 +65526,24 @@ "value" : "View and manage blocked contacts." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher et gérer les contacts bloqués." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bekijk en beheer geblokkeerde contacten." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądaj i zarządzaj zablokowanymi kontaktami." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -78898,18 +78984,36 @@ "value" : "Funkce hlasových hovorů, která je nyní ve vývojové fázi (beta), odhalí vaši IP adresu těm, se kterými si voláte a také {session_foundation} serveru." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine IP ist deinem Anrufpartner und einem {session_foundation} Server sichtbar während Beta Anrufe getätigt werden." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre adresse IP est visible par votre interlocuteur et un serveur {session_foundation} pendant que vous utilisez des appels bêta." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw IP is zichtbaar voor uw oproep partner en een {session_foundation} server tijdens het gebruik van bètagesprekken." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój adres IP jest widoczny dla twojego rozmówcy i serwera {session_foundation}, kiedy używasz rozmów beta." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -83765,12 +83869,24 @@ "value" : "Cancel Plan" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler l’abonnement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Abonnement annuleren" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anuluj plan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -83779,6 +83895,28 @@ } } }, + "cancelProPlatform" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel your plan on the {platform} website, using the {platform_account} you signed up for Pro with." + } + } + } + }, + "cancelProPlatformStore" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel your plan on the {platform_store} website, using the {platform_account} you signed up for Pro with." + } + } + } + }, "change" : { "extractionState" : "manual", "localizations" : { @@ -83806,12 +83944,24 @@ "value" : "Change" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifier" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wijzigen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -84332,18 +84482,36 @@ "value" : "Změňte své heslo pro {app_name}. Lokálně uložená data budou znovu zašifrována pomocí vašeho nového hesla." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändere dein Passwort für {app_name}. Lokal gespeicherte Dateien werden mit deinem neuen Passwort entschlüsselt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Changez votre mot de passe pour {app_name}. Les données stockées localement seront re-chiffrées avec votre nouveau mot de passe." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wijzig je wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met je nieuwe wachtwoord." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień hasło dla {app_name}. Dane przechowywane lokalnie zostaną ponownie zaszyfrowane nowym hasłem." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -84367,6 +84535,18 @@ "checkingProStatus" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusu yoxlanılır" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrola stavu {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -84378,6 +84558,18 @@ "checkingProStatusDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} məlumatlarınız yoxlanılır. Bu səhifədəki bəzi məlumatlar, yoxlama tamamlanana qədər qeyri-dəqiq ola bilər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrolují se vaše údaje {pro}. Některé informace na této stránce mohou být nepřesné, dokud kontrola nebude dokončena." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -84389,6 +84581,18 @@ "checkingProStatusUpgradeDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuz yoxlanılır. Bu yoxlama tamamlandıqdan sonra {pro} ya yüksəldə biləcəksiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontroluje se váš stav {pro}. Jakmile kontrola skončí, budete moci navýšit na {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -113769,12 +113973,24 @@ "value" : "Choose the content displayed in local notifications when an incoming message is received." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choisissez le contenu affiché dans les notifications locales lorsqu'un message entrant est reçu." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Kies de inhoud die wordt weergegeven in lokale meldingen wanneer een inkomend bericht wordt ontvangen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz treść wyświetlaną w lokalnych powiadomieniach, kiedy pojawia się nowa wiadomość." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -119636,12 +119852,24 @@ "value" : "Define how the Enter and Shift+Enter keys function in conversations." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez le fonctionnement des touches Entrée et Maj+Entrée dans les conversations." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Stel in hoe de toetsen Enter en Shift+Enter functioneren in gesprekken." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zdefiniuj jak klawisz Enter i kombinacja Shift+Enter działają w konwersacjach." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -119683,12 +119911,24 @@ "value" : "SHIFT + ENTER sends a message, ENTER starts a new line." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAJUSCULE + ENTRÉE envoie un message, ENTRÉE commence une nouvelle ligne." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "SHIFT + ENTER verzendt een bericht, ENTER begint een nieuwe regel." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER wysyła wiadomość, ENTER zaczyna nowy wiersz." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -119742,12 +119982,24 @@ "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTRÉE envoie un message, MAJ + ENTRÉE commence une nouvelle ligne." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "ENTER verzendt een bericht, SHIFT + ENTER begint een nieuwe regel." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER wysyła wiadomość, SHIFT + ENTER zaczyna nowy wiersz." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -121232,12 +121484,24 @@ "value" : "Auto-delete messages older than 6 months in communities with 2000+ messages." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer automatiquement les messages de plus de 6 mois dans les communautés ayant plus de 2000 messages." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Berichten ouder dan 6 maanden automatisch verwijderen in community's met meer dan 2000 berichten." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie usuwaj wiadomości starsze niż 6 miesięcy w społecznościach powyżej 2000 wiadomości." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -122255,12 +122519,24 @@ "value" : "Enter para enviar" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer avec Entrée" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Verzenden met Enter" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj przyciskiem Enter" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -122793,12 +123069,24 @@ "value" : "Send with Shift+Enter" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer avec Maj+Entrée" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Verzenden met Shift+Enter" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj za pomocą Shift+Enter" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -126384,18 +126672,36 @@ "value" : "Aktuální heslo" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelles Passwort" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Current Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mot de passe actuel" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Huidig wachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obecne hasło" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -126431,12 +126737,24 @@ "value" : "Current Plan" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfait actuel" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Huidig abonnement" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obecny plan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -126957,12 +127275,24 @@ "value" : "Dark Mode" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mode sombre" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Donkere modus" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tryb ciemny" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -127171,6 +127501,12 @@ "value" : "Ein Datenbankfehler ist aufgetreten.

    Exportiere deine App-Logs, um diese für eine Fehleranalyse zu teilen. Wenn dies nicht erfolgreich ist, installiere die {app_name} neu und stelle deinen Account wieder her." } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Παρουσιάστηκε σφάλμα βάσης δεδομένων.

    Εξαγάγετε τα αρχεία καταγραφής της εφαρμογής σας για κοινή χρήση στην αντιμετώπιση προβλημάτων. Αν αυτό δεν είναι επιτυχές, επανεγκαταστήστε το {app_name} και επαναφέρετε τον λογαριασμό σας." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -140311,6 +140647,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το μήνυμα;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτά τα μηνύματα;" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -140423,6 +140787,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Olete kindel, et soovite selle sõnumi kustutada?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Olete kindel, et soovite need sõnumid kustutada?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -144477,6 +144869,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το μήνυμα μόνο από αυτή τη συσκευή;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτά τα μηνύματα μόνο από αυτή τη συσκευή;" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -144589,6 +145009,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kas olete kindel, et soovite selle sõnumi ainult sellest seadmest kustutada?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kas olete kindel, et soovite need sõnumid ainult sellest seadmetest kustutada?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -149907,6 +150355,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seda sõnumit ei saa kõikidest teie seadmetest kustutada" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mõnda valitud sõnumit ei saa kõikidest teie seadmetest kustutada" + } + } + } + } + } + } + }, "fa" : { "stringUnit" : { "state" : "translated", @@ -151577,6 +152053,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seda sõnumit ei saa kõigi jaoks kustutada" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mõned sõnumid mille oled valinud ei saa kõigi jaoks kustutada" + } + } + } + } + } + } + }, "fa" : { "stringUnit" : { "state" : "translated", @@ -170205,6 +170709,12 @@ "value" : "Display" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affichage" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -188515,12 +189025,24 @@ "value" : "Show notifications when you receive new messages." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les notifications lorsque vous recevez de nouveaux messages." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon meldingen wanneer je nieuwe berichten ontvangt." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj powiadomienia o nowych wiadomościach." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -189080,6 +189602,12 @@ "value" : "Enter" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrer" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -189106,9 +189634,43 @@ } } }, + "enterPasswordDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the password you set for {app_name}" + } + } + } + }, + "enterPasswordTooltip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the password you use to unlock Session \r\non startup, not your Recovery Password" + } + } + } + }, "errorCheckingProStatus" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunu yoxlama xətası." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba kontroly stavu {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -190721,7 +191283,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Error loading {pro} plan." + "value" : "Error loading {pro} plan" } } } @@ -191890,12 +192452,24 @@ "value" : "Feedback" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donner votre avis" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Feedback" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -191943,12 +192517,24 @@ "value" : "Share your experience with {app_name} by completing a short survey." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partagez votre expérience avec {app_name} en répondant à un court sondage." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Deel je ervaring met {app_name} door een korte enquête in te vullen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podziel się wrażeniami o {app_name} wypełniając krótką ankietę." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -192954,12 +193540,24 @@ "value" : "Follow system settings." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Faire correspondre aux paramètres systèmes." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Systeeminstellingen volgen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dopasuj do ustawień systemu." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -197765,6 +198363,12 @@ "value" : "Bist du sicher, dass du {group_name} verlassen möchtest?

    Dadurch werden alle Mitglieder entfernt und alle Gruppendaten gelöscht." } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {group_name}?

    Αυτό θα αφαιρέσει όλα τα μέλη και θα διαγράψει όλο το περιεχόμενο της ομάδας." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -204857,6 +205461,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνεται πρόσκληση" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνονται προσκλήσεις" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -204969,6 +205601,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saadan kutse" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saadan kutsed" + } + } + } + } + } + } + }, "fa" : { "stringUnit" : { "state" : "translated", @@ -233975,12 +234635,24 @@ "value" : "Check the {app_name} FAQ for answers to common questions." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consultez la FAQ de {app_name} pour obtenir des réponses aux questions fréquentes." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bekijk de {app_name} FAQ voor antwoorden op veelgestelde vragen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź FAQ {app_name} by poznać odpowiedzi na często zadawane pytania." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -234501,12 +235173,24 @@ "value" : "Report a Bug" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signaler un bug" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Meld een bug" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zgłoś błąd" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -236494,12 +237178,30 @@ "value" : "Save this file, then share it with {app_name} developers." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistrez ce fichier, puis partagez-le avec les développeurs de {app_name}." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lagre denne filen, så del den med {app_name}-utviklerne." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Sla dit bestand op en deel het vervolgens met de {app_name} ontwikkelaars." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisz ten plik i wyślij go do deweloperów {app_name}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -237026,12 +237728,24 @@ "value" : "Help translate {app_name} into over 80 languages!" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aidez à traduire {app_name} en plus de 80 langues !" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Help met het vertalen van {app_name} in meer dan 80 talen!" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomóż przetłumaczyć {app_name} na ponad 80 języków!" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -238031,12 +238745,24 @@ "value" : "Toggle system menu bar visibility." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer/désactiver la visibilité de la barre de menu système." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Zichtbaarheid systeem-menubalk in-/uitschakelen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż/ukryj pasek menu systemowego." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -239358,12 +240084,24 @@ "value" : "Important" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Important" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Belangrijk" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ważne" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -241443,6 +242181,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η πρόσκληση απέτυχε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προσκλήσεις απέτυχαν" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -241555,6 +242321,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutse saatmine ebaõnnestus" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutsete saatmine ebaõnnestus" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -242334,6 +243128,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η πρόσκληση δεν μπορούσε να σταλθεί. Θέλετε να προσπαθήσετε ξανά;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προσκλήσεις δεν μπορούσαν να σταλθούν. Θέλετε να προσπαθήσετε ξανά;" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -242446,6 +243268,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutset ei õnnestunud saata. Kas soovite uuesti proovida?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutseid ei õnnestunud saata. Kas soovite uuesti proovida?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -244031,6 +244881,12 @@ "launchOnStartDescriptionDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kompüteriniz açıldığı zaman {app_name}-u avtomatik başlat." + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -244043,6 +244899,12 @@ "value" : "Launch {app_name} automatically when your computer starts up." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lancer automatiquement {app_name} au démarrage de votre ordinateur." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -244072,6 +244934,12 @@ "value" : "Launch on Startup" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lancer au démarrage" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -244083,6 +244951,12 @@ "launchOnStartupDisabledDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu ayar, Linux-dakı sisteminiz tərəfindən idarə olunur. Avtomatik açılışı fəallaşdırmaq üçün sistem ayarlarında {app_name} tətbiqini açılış tətbiqlərinizə əlavə edin." + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -244095,6 +244969,12 @@ "value" : "This setting is managed by your system on Linux. To enable automatic startup, add {app_name} to your startup applications in system settings." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce paramètre est géré par votre système sous Linux. Pour activer le démarrage automatique, ajoutez {app_name} à vos applications de démarrage dans les paramètres système." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -254072,18 +254952,36 @@ "value" : "Odkazy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verknüpfungen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Links" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liens" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Koppelingen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linki" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -259873,18 +260771,36 @@ "value" : "Logy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protokolle" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Logs" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Journaux" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Logboeken" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dzienniki" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -260081,12 +260997,24 @@ "value" : "Manage {pro}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} beheren" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzaj {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -270341,12 +271269,24 @@ "value" : "Menu Bar" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barre de menu" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Menubalk" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pasek Menu" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -271010,12 +271950,24 @@ "value" : "Copy Message" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier le message" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bericht kopiëren" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiuj wiadomość" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -279795,6 +280747,24 @@ } } }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Έχετε νέο μήνυμα στο {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Έχετε %lld νέα μηνύματα στο {group_name}." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -279867,6 +280837,24 @@ } } }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sul on uus sõnum {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sul on %lld uut sõnumit {group_name}." + } + } + } + } + }, "fr" : { "variations" : { "plural" : { @@ -294193,6 +295181,24 @@ } } }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Τα μηνύματα έχουν όριο {limit} χαρακτήρων. Σας απομένει %lld χαρακτήρας." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Τα μηνύματα έχουν όριο {limit} χαρακτήρων. Σας απομένουν %lld χαρακτήρες." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -294247,6 +295253,24 @@ } } }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumitel on tähemärgipiirang {limit} tähemärki. Teil on %lld tähemärki alles." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumitel on tähemärgipiirang {limit} tähemärki. Teil on %lld tähemärki alles." + } + } + } + } + }, "fr" : { "variations" : { "plural" : { @@ -295261,18 +296285,36 @@ "value" : "Nové heslo" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neues Passwort" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "New Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouveau mot de passe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Nieuw wachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nowe hasło" + } + }, "sv-SE" : { "stringUnit" : { "state" : "translated", @@ -295787,12 +296829,24 @@ "value" : "Next Steps" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Étapes suivantes" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Volgende stappen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następne kroki" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -301312,18 +302366,36 @@ "value" : "Zobrazení upozornění" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benachrichtigungsanzeige" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notification Display" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affichage des notifications" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Notificatie weergave" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlanie powiadomień" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -304257,12 +305329,24 @@ "value" : "Display the sender's name and a preview of the message content." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le nom de l'expéditeur et un aperçu du contenu du message." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon de naam van de afzender en een voorbeeld van de berichtinhoud." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj nazwę nadawcy i podgląd wiadomości." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -304298,18 +305382,36 @@ "value" : "Zobrazit pouze jméno odesílatele bez obsahu zprávy." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nur den Namen des Absenders ohne Nachrichteninhalt anzeigen." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Display only the sender's name without any message content." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher uniquement le nom de l'expéditeur sans aucun contenu du message." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon alleen de naam van de afzender zonder enige berichtinhoud." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj tylko nazwę nadawcy, bez podglądu wiadomości." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -305949,12 +307051,24 @@ "value" : "Display a generic {app_name} notification without the sender's name or message content." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher une notification générique de {app_name} sans le nom de l'expéditeur ni le contenu du message." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon een algemene {app_name} melding zonder de naam van de afzender of de inhoud van het bericht." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj tylko powiadomienie {app_name}, bez podglądu wiadomości ani nazwy nadawcy." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -309325,12 +310439,24 @@ "value" : "Play a sound when you receive receive new messages." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jouer un son lorsque vous recevez de nouveaux messages." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Speel een geluid af wanneer je nieuwe berichten ontvangt." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odtwórz dźwięk, kiedy otrzymasz nową wiadomość." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -326471,11 +327597,23 @@ "value" : "On your {device_type} device" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sur votre appareil {device_type}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Op je {device_type} apparaat" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "На вашому пристрої {device_type}" + } } } }, @@ -326500,6 +327638,12 @@ "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the {app_pro} settings." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrez ce compte {app_name} sur un appareil {device_type} connecté au compte {platform_account} avec lequel vous vous êtes inscrit à l'origine. Ensuite, modifiez votre abonnement via les paramètres {app_pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -329400,6 +330544,28 @@ } } }, + "onPlatformStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On the {platform_store} website" + } + } + } + }, + "onPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On the {platform} website" + } + } + } + }, "onsErrorNotRecognized" : { "extractionState" : "manual", "localizations" : { @@ -330837,31 +332003,24 @@ } } }, - "openStoreWebsite" : { + "openPlatformStoreWebsite" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_store} veb saytını aç" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Otevřít webovou stránku {platform_store}" - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open {platform_store} Website" } - }, - "nl" : { + } + } + }, + "openPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open de {platform_store} website" + "value" : "Open {platform} Website" } } } @@ -331497,12 +332656,24 @@ "value" : "Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mot de passe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasło" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -332053,12 +333224,24 @@ "value" : "Tu contraseña ha sido cambiada. Por favor, guárdala en un lugar seguro." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre mot de passe a été changé. Veuillez le conserver en sécurité." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw wachtwoord is gewijzigd. Hou het veilig." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasło zostało zmienione. Zapisz je w bezpiecznym miejscu." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -332118,6 +333301,12 @@ "value" : "Wijzig het wachtwoord dat nodig is om {app_name} te ontgrendelen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień hasło wymagane do odblokowania {app_name}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -332662,6 +333851,12 @@ "value" : "Wachtwoord aanmaken" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utwórz hasło" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -336672,12 +337867,24 @@ "value" : "Confirm New Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmer le nouveau mot de passe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bevestig nieuwe wachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potwierdź nowe hasło" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -337210,12 +338417,24 @@ "value" : "Your password has been removed." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre mot de passe a été supprimé." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw wachtwoord is verwijderd." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasło zostało usunięte." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -337257,18 +338476,36 @@ "value" : "Odebrat heslo pro odemykání {app_name}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entfernung des Passwortes erforderlich um {app_name} zu entsperren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remove the password required to unlock {app_name}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer le mot de passe requis pour déverrouiller {app_name}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Verwijder het wachtwoord dat nodig is om {app_name} te ontgrendelen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń hasło wymagane do odblokowania {app_name}" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -337316,12 +338553,24 @@ "value" : "Passwords" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mots de passe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wachtwoorden" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasła" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -337854,12 +339103,24 @@ "value" : "Your password has been set. Please keep it safe." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre mot de passe a été défini. Veuillez le conserver en sécurité." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw wachtwoord is ingesteld. Hou het veilig." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasło zostało utworzone. Zapisz je w bezpiecznym miejscu." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -337907,12 +339168,24 @@ "value" : "Require password to unlock {app_name} on startup." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mot de passe requis pour déverrouiller {app_name} au démarrage." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wachtwoord vereisen om {app_name} bij het opstarten te ontgrendelen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymagaj hasła do odblokowania {app_name} przy uruchomieniu." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -337960,12 +339233,24 @@ "value" : "Longer than 12 characters" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus de 12 caractères" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Langer dan 12 tekens" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dłuższe niż 12 znaków" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -338019,12 +339304,24 @@ "value" : "Includes a number" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inclut un chiffre" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bevat een cijfer" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawiera cyfrę" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -338078,12 +339375,24 @@ "value" : "Includes a lowercase letter" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprend une lettre minuscule" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bevat een kleine letter" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawiera małą literę" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -338131,11 +339440,29 @@ "value" : "Includes a symbol" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inclut un symbole" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bevat een symbool" } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawiera symbol" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Містить символ" + } } } }, @@ -338166,12 +339493,24 @@ "value" : "Includes a uppercase letter" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contient une lettre majuscule" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bevat een hoofdletter" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawiera wielką literę" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -338225,12 +339564,24 @@ "value" : "Password Strength Indicator" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indicateur de robustesse du mot de passe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Wachtwoordsterkte indicator" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wskaźnik siły hasła" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -338272,18 +339623,36 @@ "value" : "Nastavení silného hesla pomáhá chránit vaše zprávy a přílohy v případě ztráty nebo odcizení zařízení." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ein schwieriges Passwort hilft deine Nachrichten und Anlagen zu schützen, wenn dein Gerät jemals verloren geht oder gestohlen wird." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Setting a strong password helps protect your messages and attachments if your device is ever lost or stolen." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définir un mot de passe robuste permet de protéger vos messages et pièces jointes en cas de perte ou de vol de votre appareil." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Een sterk wachtwoord helpt je berichten en bijlagen te beschermen als je apparaat ooit verloren raakt of wordt gestolen." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienie silnego hasła pomaga chronić Twoje wiadomości i załączniki w przypadku utraty lub kradzieży urządzenia." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -342813,6 +344182,12 @@ "value" : "{app_name} läuft im Hintergrund weiter, wenn du das Fenster schließt." } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Το {app_name} συνεχίζει να εκτελείται στο παρασκήνιο όταν κλείνετε το παράθυρο." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -342837,6 +344212,12 @@ "value" : "{app_name} blijft op de achtergrond draaien wanneer je het venster sluit." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} nadal działa w tle po zamknięciu okna." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -352428,6 +353809,12 @@ "value" : "Plus Loads More..." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus de téléchargement..." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -352463,12 +353850,24 @@ "value" : "New features coming soon to {pro}. Discover what's next on the {pro} Roadmap {icon}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouvelles fonctionnalités {pro} à venir. Découvrez ce qui vous attend dans la feuille de route {pro} {icon}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Nieuwe functies komen binnenkort naar {pro}. Ontdek wat er komt op de {pro} Roadmap {icon}" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nowe funkcje niedługo pojawią się w {pro}. Odkryj, co nowego na {pro} Roadmap {icon}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -352516,12 +353915,24 @@ "value" : "Preferencias" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Préférences" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Voorkeuren" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencje" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -353054,12 +354465,24 @@ "value" : "Preview Notification" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aperçu de la notification" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Voorbeeldmelding" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podgląd powiadomień" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -353226,12 +354649,24 @@ "value" : "You're all set!" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tout est prêt !" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Alles is geregeld!" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wszystko gotowe!" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -353261,12 +354696,24 @@ "value" : "Your {app_pro} plan was updated! You will be billed when your current {pro} plan is automatically renewed on {date}." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} a été mis à jour ! Vous serez facturé lorsque votre forfait {pro} actuel sera automatiquement renouvelé le {date}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je {app_pro} abonnement is bijgewerkt! Je wordt gefactureerd wanneer je huidige {pro} abonnement automatisch wordt verlengd op {date}." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} został zaktualizowany! Opłata zostanie pobrana, kiedy Twój obecny plan {pro} odnowi się automatycznie {date}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -353951,12 +355398,24 @@ "value" : "Animated Display Pictures" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Photos de profil animées" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Geanimeerde profielfoto's" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Animowane obrazy profilu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -353986,12 +355445,24 @@ "value" : "Set animated GIFs and WebP images as your display picture." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez des GIF et des images WebP animées comme photo de profil." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Stel geanimeerde GIF's en WebP-afbeeldingen in als je profielfoto." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawiaj animowane obrazy GIF i WebP jako swój obraz profilu." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354146,18 +355617,36 @@ "value" : "{pro} se automaticky obnoví za {time}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatische {pro} Erneuerung in {time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} auto-renewing in {time}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} se renouvelle automatiquement dans {time}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} wordt automatisch verlengd over {time}" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczne odnowienie subskrypcji {pro}: {time}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354187,12 +355676,24 @@ "value" : "{pro} Badge" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} badge" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaka {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354222,12 +355723,24 @@ "value" : "Badges" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badges" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Badges" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaki" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354257,12 +355770,24 @@ "value" : "Show your support for {app_name} with an exclusive badge next to your display name." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affichez votre soutien à {app_name} avec un badge exclusif, à côté de votre nom d'affichage." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon je steun voor {app_name} met een exclusieve badge naast je schermnaam." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż swoje wsparcie dla {app_name} ekskluzywną odznaką obok swojej nazwy wyświetlanej." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354342,6 +355867,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Σήμα στάλθηκε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Εμβλήματα στάλθηκαν" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -354370,6 +355923,62 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Tunnusmärk Saadetud" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Tunnusmärki Saadetud" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {total} {pro} envoyé" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {total} {pro} envoyé" + } + } + } + } + } + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -354397,6 +356006,46 @@ } } } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznaki {pro}" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznak {pro}" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznakę {pro}" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznak {pro}" + } + } + } + } + } + } } } }, @@ -354421,12 +356070,24 @@ "value" : "Show {app_pro} badge to other users" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher l’insigne {app_pro} aux autres utilisateurs" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Toon het {app_pro} badge aan andere gebruikers" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż odznakę {app_pro} innym użytkownikom" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354438,11 +356099,47 @@ "proBetaFeatures" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Beta Xüsusiyyətləri" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkce beta verze {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} Beta Features" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fonctionnalités {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} functies" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkcje {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Можливості {pro}" + } } } }, @@ -354467,12 +356164,24 @@ "value" : "{price} Billed Annually" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} facturé annuellement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{price} Jaarlijks gefactureerd" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opłata roczna: {price}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354502,12 +356211,24 @@ "value" : "{price} Billed Monthly" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} facturé mensuellement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{price} Maandelijks gefactureerd" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opłata miesięczna: {price}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354537,12 +356258,24 @@ "value" : "{price} Billed Quarterly" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{price} facturé trimestriellement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{price} per kwartaal gefactureerd" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opłata kwartalna: {price}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -354950,31 +356683,92 @@ } } }, - "processingRefundRequest" : { + "proCancellation" : { "extractionState" : "manual", "localizations" : { - "az" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "{platform_account} geri ödəmə tələbinizi emal edir" + "value" : "Zrušit" } }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellation" + } + } + } + }, + "proCancellationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canceling your {app_pro} plan will prevent your plan from automatically renewing before your {pro} plan expires. Canceling {pro} does not result in a refund. You will continue to be able to use {app_pro} features until your plan expires.

    Because you originally signed up for {app_pro} using your {platform_account}, you'll need to use the same {platform_account} to cancel your plan." + } + } + } + }, + "proCancellationOptions" : { + "extractionState" : "manual", + "localizations" : { "cs" : { "stringUnit" : { "state" : "translated", - "value" : "{platform_account} zpracovává vaši žádost o vrácení peněz" + "value" : "Dva způsoby, jak zrušit váš tarif:" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{platform_account} is processing your refund request" + "value" : "Two ways to cancel your plan:" + } + } + } + }, + "processingRefundRequest" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform} is processing your refund request" + } + } + } + }, + "proClearAllDataDevice" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat svá data z tohoto zařízení?

    {app_pro} nelze převést na jiný účet. Uložte si své heslo pro obnovení, abyste mohli svůj tarif {pro} později obnovit." } }, - "nl" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete your data from this device?

    {app_pro} cannot be transferred to another account. Please save your Recovery Password to ensure you can restore your {pro} plan later." + } + } + } + }, + "proClearAllDataNetwork" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat svá data ze sítě? Pokud budete pokračovat, nebudete moci obnovit vaše zprávy ani kontakty.

    {app_pro} nelze převést na jiný účet. Uložte si své heslo pro obnovení, abyste mohli svůj tarif {pro} později obnovit." + } + }, + "en" : { "stringUnit" : { "state" : "translated", - "value" : "{platform_account} verwerkt je restitutieverzoek" + "value" : "Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.

    {app_pro} cannot be transferred to another account. Please save your Recovery Password to ensure you can restore your {pro} plan later." } } } @@ -355000,12 +356794,24 @@ "value" : "Your current plan is already discounted by {percent}% of the full {app_pro} price." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre abonnement actuel bénéficie déjà d'une remise de {percent}% sur le prix de {app_pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je huidige abonnement is al met {percent}% korting ten opzichte van de volledige {app_pro} prijs." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cena Twojego obecnego planu jest obniżona o {percent}% pełnej ceny {app_pro}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -355017,6 +356823,18 @@ "proErrorRefreshingStatus" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunu təzələmə xətası" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba obnovování stavu {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -355040,12 +356858,24 @@ "value" : "Platnost vypršela" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abgelaufen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Expired" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expiré" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -355081,12 +356911,24 @@ "value" : "Unfortunately, your {pro} plan has expired. Renew to keep accessing the exclusive perks and features of {app_pro}." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malheureusement, votre formule {pro} a expiré. Renouvelez-la pour continuer à profiter des avantages et fonctionnalités exclusifs de {app_pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Helaas is je {pro} abonnement verlopen. Verleng om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niestety, Twój plan {pro} wygasł. Odnów go, by odzyskać dostęp do ekskluzywnych korzyści i funkcji {app_pro}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -355116,12 +356958,24 @@ "value" : "Expiring Soon" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expiration imminente" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Verloopt binnenkort" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niedługo wygaśnie" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -355136,31 +356990,43 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{pro} planınızın müddəti {time} vaxtında bitir. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün planınızı güncəlləyin." + "value" : "{pro} planınızın müddəti {time} vaxtında bitir. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün planınızı güncəlləyin." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Váš tarif {pro} vyprší za {time}. Aktualizujte si tarif, abyste i nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." + "value" : "Váš tarif {pro} vyprší za {time}. Aktualizujte si tarif, abyste i nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your {pro} plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of {app_pro}." + "value" : "Your {pro} plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of {app_pro}." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre formule {pro} expire dans {time}. Mettez à jour votre formule pour continuer à profiter des avantages et fonctionnalités exclusifs de {app_pro}." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je {pro} abonnement verloopt over {time}. Werk je abonnement bij om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." + "value" : "Je {pro} abonnement verloopt over {time}. Werk je abonnement bij om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {pro} wygasa za {time}. Zaktualizuj swój plan, aby zachować dostęp do ekskluzywnych korzyści i funkcji {app_pro}." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Підписка {pro} спливе {time}. Онови підписку задля збереження переваг і можливостей {app_pro}." + "value" : "Підписка {pro} спливе {time}. Онови підписку задля збереження переваг і можливостей {app_pro}." } } } @@ -355186,6 +357052,12 @@ "value" : "{pro} expiring in {time}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} expire dans {time}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -355209,18 +357081,36 @@ "value" : "{pro} FAQ" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} FAQ" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{pro} FAQ" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "FAQ {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} FAQ" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "FAQ {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -355232,11 +357122,47 @@ "proFaqDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} TVS-da tez-tez verilən suallara cavab tapın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Najděte odpovědi na časté dotazy v nápovědě {app_pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Find answers to common questions in the {app_pro} FAQ." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trouvez des réponses aux questions fréquentes dans la FAQ de {app_pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vind antwoorden op veelgestelde vragen in de {app_pro} FAQ." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Znajdź odpowiedzi na często zadawane pytania w sekcji FAQ {app_pro}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відповіді на загальні запитання знайдеш у ЧаПи {app_pro}." + } } } }, @@ -359108,6 +361034,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αναβαθμίστηκε η {total} ομάδα" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αναβαθμίστηκαν {total} ομάδες" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -359136,6 +361090,34 @@ } } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} groupe mis à niveau" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} groupes mis à niveau" + } + } + } + } + } + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -359163,6 +361145,46 @@ } } } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grupy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grup" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grupę" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grup" + } + } + } + } + } + } } } }, @@ -359187,12 +361209,24 @@ "value" : "Requesting a refund is final. If approved, your {pro} plan will be canceled immediately and you will lose access to all {pro} features." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La demande de remboursement est définitive. Si elle est approuvée, votre formule {pro} sera immédiatement annulée et vous perdrez l'accès à toutes les fonctionnalités {pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Het aanvragen van een terugbetaling is definitief. Indien goedgekeurd, wordt je {pro} abonnement onmiddellijk geannuleerd en verlies je de toegang tot alle {pro} functies." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wniosek o zwrot jest ostateczny. Jeżeli zostanie zatwierdzony, Twój plan {pro} zostanie natychmiast anulowany i utracisz dostęp do wszystkich funkcji {pro}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -359472,12 +361506,24 @@ "value" : "Larger Groups" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groupes plus grands" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Grotere groepen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Większe grupy" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -359507,6 +361553,12 @@ "value" : "Groups you are an admin in are automatically upgraded to support 300 members." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les groupes dont vous êtes administrateur sont automatiquement mis à niveau pour prendre en charge 300 membres." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -359524,6 +361576,12 @@ "proLargerGroupsTooltip" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Větší skupinové chaty (až pro 300 členů) brzy budou k dispozici pro všechny uživatele Pro Beta!" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -359553,12 +361611,24 @@ "value" : "Longer Messages" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messages plus longs" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Langere berichten" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dłuższe wiadomości" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -359588,12 +361658,24 @@ "value" : "You can send messages up to 10,000 characters in all conversations." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous pouvez envoyer des messages jusqu'à 10000 caractères dans toutes les conversations." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je kunt berichten tot 10.000 tekens verzenden in alle gesprekken." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możesz wysyłać wiadomości aż do 10 000 znaków we wszystkich konwersacjach." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -359673,6 +361755,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Μεγαλύτερο Μήνυμα εστάλη" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Μεγαλύτερο Μήνυμα εστάλησαν" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -359701,6 +361811,34 @@ } } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message plus long {total} envoyé" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Messages plus longs envoyés" + } + } + } + } + } + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -359728,6 +361866,46 @@ } } } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższe wiadomości" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższych wiadomości" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższą wiadomość" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższych wiadomości" + } + } + } + } + } + } } } }, @@ -360490,6 +362668,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η προώθηση απέτυχε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προωθήσεις απέτυχαν" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -360602,6 +362808,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamine ebaõnnestus" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamised ebaõnnestusid" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -361325,6 +363559,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η προώθηση δεν ήταν δυνατό να εφαρμοστεί. Θέλετε να προσπαθήσετε ξανά;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προωθήσεις δεν ήταν δυνατό να εφαρμοστούν. Θέλετε να προσπαθήσετε ξανά;" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -361437,6 +363699,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamist ei õnnestunud rakendada. Kas soovite uuesti proovida?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamisi ei õnnestunud rakendada. Kas soovite uuesti proovida?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -362005,6 +364295,45 @@ } } }, + "proNewInstallation" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "With a new installation" + } + } + } + }, + "proNewInstallationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinstall {app_name} on this device via the {platform_store}, restore your account with your Recovery Password, and renew your plan in the {app_pro} settings." + } + } + } + }, + "proOptionsRenewalSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyní jsou k dispozici tři způsoby obnovy:" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currently, there are three ways to renew:" + } + } + } + }, "proPercentOff" : { "extractionState" : "manual", "localizations" : { @@ -362026,11 +364355,23 @@ "value" : "{percent}% Off" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% de réduction" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{percent}% korting" } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% Знижки" + } } } }, @@ -362105,6 +364446,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Καρφιτσωμένη Συνομιλία" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Καρφιτσωμένες Συνομιλίες" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362133,6 +364502,34 @@ } } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Conversation épinglée" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Conversations épinglées" + } + } + } + } + } + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -362160,6 +364557,46 @@ } } } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypięte konwersacje" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypiętych konwersacji" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypięta konwersacja" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypiętych konwersacji" + } + } + } + } + } + } } } }, @@ -362184,12 +364621,24 @@ "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when {pro} is next renewed." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre formule {app_pro} est active !

    Votre formule sera automatiquement renouvelée pour une autre {current_plan} le {date}. Les modifications apportées à votre formule prendront effet lors du prochain renouvellement de {pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je {app_pro} abonnement is actief!

    Je abonnement wordt automatisch verlengd voor een nieuw {current_plan} op {date}. Wijzigingen aan je abonnement gaan in wanneer {pro} de volgende keer wordt verlengd." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} jest aktywny!

    Zostanie on automatycznie odnowiony na kolejny {current_plan}, dnia {date}. Zmiany w Twoim planie wejdą w życie przy następnym odnowieniu subskrypcji {pro}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362219,12 +364668,24 @@ "value" : "Your {app_pro} plan is active!

    Your plan will automatically renew for another {current_plan} on {date}." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} est actif

    Votre abonnement se renouvellera automatiquement pour un autre {current_plan} le {date}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je {app_pro} abonnement is actief!

    Je abonnement wordt automatisch verlengd met een {current_plan} op {date}." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} jest aktywny!

    Zostanie on automatycznie odnowiony na kolejny {current_plan}, dnia {date}." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362239,31 +364700,43 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} planınızın müddəti {date} tarixində bitir.

    Eksklüziv Pro özəlliklərinə kəsintisiz erişimi təmin etmək üçün planınızı indi güncəlləyin." + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir.

    Eksklüziv Pro özəlliklərinə kəsintisiz erişimi təmin etmək üçün planınızı indi güncəlləyin." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Váš tarif {app_pro} vyprší dne {date}.

    Aktualizujte si tarif nyní, abyste měli i nadále nepřerušený přístup k exkluzivním funkcím Pro." + "value" : "Váš tarif {app_pro} vyprší dne {date}.

    Aktualizujte si tarif nyní, abyste měli i nadále nepřerušený přístup k exkluzivním funkcím Pro." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your {app_pro} plan will expire on {date}.

    Update your plan now to ensure uninterrupted access to exclusive Pro features." + "value" : "Your {app_pro} plan will expire on {date}.

    Update your plan now to ensure uninterrupted access to exclusive Pro features." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre offre {app_pro} expirera le {date}.

    Mettez votre offre à jour maintenant pour garantir un accès ininterrompu aux fonctionnalités exclusives Pro." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je {app_pro} abonnement verloopt op {date}.

    Werk je abonnement nu bij om ononderbroken toegang te behouden tot exclusieve Pro functies." + "value" : "Je {app_pro} abonnement verloopt op {date}.

    Werk je abonnement nu bij om ononderbroken toegang te behouden tot exclusieve Pro functies." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} wygasa {date}.

    Zaktualizuj swój plan już teraz, by zapewnić sobie nieprzerwany dostęp do ekskluzywnych funkcji Pro." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Твоя підписка {app_pro} спливе {date}.

    Для збереження особливих можливостей подовж свою підписку." + "value" : "Твоя підписка {app_pro} спливе {date}.

    Для збереження особливих можливостей подовж свою підписку." } } } @@ -362271,6 +364744,18 @@ "proPlanError" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı xətası" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba tarifu {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362285,31 +364770,43 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_pro} planınızın müddəti {date} tarixində bitir." + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Váš tarif {app_pro} vyprší dne {date}." + "value" : "Váš tarif {app_pro} vyprší dne {date}." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your {app_pro} plan will expire on {date}." + "value" : "Your {app_pro} plan will expire on {date}." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} expirera le {date}." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je {app_pro} abonnement verloopt op {date}." + "value" : "Je {app_pro} abonnement verloopt op {date}." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} wygasa {date}." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Підписка {app_pro} спливе {date}." + "value" : "Підписка {app_pro} спливе {date}." } } } @@ -362317,6 +364814,18 @@ "proPlanLoading" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání tarifu {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362328,6 +364837,18 @@ "proPlanLoadingDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınız barədə məlumatlar hələ də yüklənir. Bu proses tamamlanana qədər planınızı güncəlləyə bilməzsiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {pro} se stále načítá. Dokud nebude načítání dokončeno, nemůžete tarif změnit." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362339,6 +364860,18 @@ "proPlanLoadingEllipsis" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı yüklənir..." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání tarifu {pro}..." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362350,6 +364883,18 @@ "proPlanNetworkLoadError" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı planınızı yükləmək üçün şəbəkəyə bağlana bilmir. Bağlantı bərpa olunana qədər {app_name} ilə planınızı güncəlləmək sıradan çıxarılacaq.

    Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti a načíst váš aktuální tarif. Aktualizace tarifu prostřednictvím {app_name} bude deaktivována, dokud nebude obnoveno připojení.

    Zkontrolujte připojení k síti a zkuste to znovu." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -362379,12 +364924,24 @@ "value" : "{pro} Plan Not Found" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfait {pro} introuvable" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} abonnement niet gevonden" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie znaleziono planu {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362414,39 +364971,33 @@ "value" : "No active plan was found for your account. If you believe this is a mistake, please reach out to {app_name} support for assistance." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun forfait actif n’a été trouvé pour votre compte. Si vous pensez qu’il s’agit d’une erreur, veuillez contacter l’assistance de {app_name} pour obtenir de l’aide." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Er is geen actief abonnement gevonden voor je account. Als je denkt dat dit een vergissing is, neem dan contact op met de ondersteuning van {app_name} voor hulp." } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie znaleziono aktywnych planów dla Twojego konta. Jeżeli uważasz, że to błąd, prosimy o kontakt z Supportem {app_name}." + } } } }, "proPlanPlatformRefund" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri ödəmə tələbini göndərmək üçün eyni {platform_account} hesabını istifadə etməlisiniz." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Protože jste se původně zaregistrovali do {app_pro} přes obchod {platform_store}, budete muset pro žádost o vrácení peněz použít stejný účet {platform_account}." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use the same {platform_account} to request a refund." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, moet je hetzelfde {platform_account} gebruiken om een terugbetaling aan te vragen." + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, you'll need to use the same {platform_account} to request a refund." } } } @@ -362454,28 +365005,10 @@ "proPlanPlatformRefundLong" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə, geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

    Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəmə tələbinizi göndərin.

    {app_name} Dəstək komandası, geri ödəmə tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Protože jste si původně zakoupili {app_pro} přes obchod {platform_store}, váš požadavek na vrácení peněz bude zpracován podporou {app_name}.

    Požádejte o vrácení peněz kliknutím na tlačítko níže a vyplněním formuláře žádosti o vrácení peněz.

    Ačkoliv se podpora {app_name} snaží zpracovat žádosti o vrácení peněz během 24–72 hodin, může zpracování trvat i déle, pokud dochází k vysokému počtu žádostí." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, your refund request will be processed by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, wordt je restitutieverzoek afgehandeld door {app_name} Support.

    Vraag een restitutie aan door op de knop hieronder te drukken en het restitutieformulier in te vullen.

    Hoewel {app_name} Support ernaar streeft om restitutieverzoeken binnen 24-72 uur te verwerken, kan het tijdens drukte langer duren" + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, your refund request will be processed by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." } } } @@ -362501,12 +365034,24 @@ "value" : "Recover {pro} Plan" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Récupérer le forfait {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} abonnement herstellen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odzyskaj plan {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362536,12 +365081,24 @@ "value" : "Renew {pro} Plan" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouveler l’abonnement {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} abonnement verlengen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odnów plan {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362553,28 +365110,10 @@ "proPlanRenewDesktop" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hazırda, {pro} planları, yalnızca {platform_store} və {platform_store} Mağazaları vasitəsilə satın alına və yenilənə bilər. {app_name} Masaüstü istifadə etdiyinizə görə planınızı burada yeniləyə bilməzsiniz.

    {app_pro} gəlişdiriciləri, istifadəçilərin {pro} planlarını {platform_store} və {platform_store} Mağazalarından kənarda almağına imkan verəcək alternativ ödəniş variantları üzərində ciddi şəkildə çalışırlar. {pro} Yol Xəritəsi" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "V současnosti lze tarify {pro} zakoupit a obnovit pouze prostřednictvím obchodů {platform_store} nebo {platform_store}. Protože používáte {app_name} Desktop, nemůžete zde svůj plán obnovit.

    Vývojáři {app_pro} intenzivně pracují na alternativních platebních možnostech, které by uživatelům umožnily zakoupit tarify {pro} mimo obchody {platform_store} a {platform_store}. Plán vývoje {pro}" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store} Stores. Because you are using {app_name} Desktop, you're not able to renew your plan here.

    {app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store} Stores. {pro} Roadmap" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momenteel kunnen {pro} abonnementen alleen worden gekocht en verlengd via de {platform_store}- of {platform_store} winkels. Omdat je {app_name} Desktop gebruikt, kun je je abonnement hier niet verlengen.

    De ontwikkelaars van {app_pro} werken hard aan alternatieve betaalmogelijkheden, zodat gebruikers {pro} abonnementen buiten de {platform_store}- en {platform_store} winkels kunnen aanschaffen. {pro} Routekaart" + "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store}. Because you are using {app_name} Desktop, you're not able to renew your plan here.

    {app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store}. {pro} Roadmap {icon}" } } } @@ -362582,68 +365121,73 @@ "proPlanRenewDesktopLinked" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_store} və ya {platform_store} Mağazaları vasitəsilə planınızı {app_name} quraşdırılmış və əlaqələndirilmiş cihazda {app_pro} ayarlarında yeniləyin." - } - }, - "cs" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Obnovte svůj tarif v nastavení {app_pro} na propojeném zařízení s nainstalovanou aplikací {app_name} prostřednictvím obchodu {platform_store} nebo {platform_store}." + "value" : "Renew your plan in the {app_pro} settings on a linked device with {app_name} installed via the {platform_store} or {platform_store}." } - }, + } + } + }, + "proPlanRenewPlatformStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Renew your plan in the {app_pro} settings on a linked device with {app_name} installed via the {platform_store} or {platform_store} Store." + "value" : "Renew your plan on the {platform_store} website using the {platform_account} you signed up for {pro} with." } - }, - "nl" : { + } + } + }, + "proPlanRenewPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Verleng je abonnement in de {app_pro} instellingen op een gekoppeld apparaat met {app_name} geïnstalleerd via de {platform_store} of {platform_store} winkel." + "value" : "Renew your plan on the {platform} website using the {platform_account} you signed up for {pro} with." } } } }, - "proPlanRenewDesktopStore" : { + "proPlanRenewStart" : { "extractionState" : "manual", "localizations" : { "az" : { "stringUnit" : { "state" : "translated", - "value" : "{pro} üçün qeydiyyatdan keçdiyiniz {platform_account} hesabınızla {platform_store} veb saytında planınızı yeniləyin." + "value" : "Güclü {app_pro} özəlliklərini yenidən istifadə etməyə başlamaq üçün {app_pro} planınızı yeniləyin." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Obnovte svůj tarif na webu {platform_store} pomocí účtu {platform_account}, se kterým jste si pořídili {pro}." + "value" : "Obnovte váš tarif {app_pro}, abyste mohli znovu využívat užitečné funkce {app_pro}." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Renew your plan on the {platform_store} website using the {platform_account} you signed up for {pro} with." + "value" : "Renew your {app_pro} plan to start using powerful {app_pro} Beta features again." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouvelez votre abonnement {app_pro} pour recommencer à utiliser les puissantes fonctionnalités bêta de {app_pro}." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verleng je abonnement op de {platform_store} website met het {platform_account} waarmee je je voor {pro} hebt aangemeld." + "value" : "Verleng je {app_pro} abonnement om opnieuw gebruik te maken van krachtige {app_pro} functies." } - } - } - }, - "proPlanRenewStart" : { - "extractionState" : "manual", - "localizations" : { - "en" : { + }, + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Renew your {app_pro} plan to start using powerful {app_pro} Beta features again." + "value" : "Odnów swój plan {app_pro}, aby znów używać potężnych funkcji {app_pro} Beta." } } } @@ -362669,11 +365213,23 @@ "value" : "Your {app_pro} plan has been renewed! Thank you for supporting the {network_name}." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} a été renouvelé ! Merci de soutenir le {network_name}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je {app_pro} abonnement is verlengd! Bedankt voor je steun aan de {network_name}." } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} został odnowiony! Dziękujemy za wspieranie {network_name}." + } } } }, @@ -362698,12 +365254,24 @@ "value" : "{pro} Plan Restored" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Forfait rétabli" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} abonnement hersteld" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plan {pro} został odzyskany" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362733,39 +365301,39 @@ "value" : "A valid plan for {app_pro} was detected and your {pro} status has been restored!" } }, - "nl" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Een geldig abonnement voor {app_pro} is gedetecteerd en je {pro} status is hersteld!" + "value" : "Un abonnement valide pour {app_pro} a été détecté et votre statut {pro} a été restauré !" } - } - } - }, - "proPlanSignUp" : { - "extractionState" : "manual", - "localizations" : { - "az" : { + }, + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Başda {platform_store} Mağazası üzərindən {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." + "value" : "Een geldig abonnement voor {app_pro} is gedetecteerd en je {pro} status is hersteld!" } }, - "cs" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Protože jste se původně zaregistrovali do {app_pro} přes {platform_store}, je třeba abyste pro aktualizaci vašeho tarifu použili svůj {platform_account}." + "value" : "Wykryto ważny plan {app_pro} oraz przywrócono Twój status {pro}!" } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Because you originally signed up for {app_pro} via the {platform_store} Store, you'll need to use your {platform_account} to update your plan." + "value" : "Виявлено дійсний план {app_pro}, та ваш статус {pro} було відновлено!" } - }, - "nl" : { + } + } + }, + "proPlanSignUp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via de {platform_store} winkel, moet je je {platform_account} gebruiken om je abonnement bij te werken." + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, you'll need to use your {platform_account} to update your plan." } } } @@ -362791,12 +365359,24 @@ "value" : "1 Month - {monthly_price} / Month" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 mois – {monthly_price} / mois" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "1 maand - {monthly_price} / maand" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 miesiąc - {monthly_price} / miesiąc" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362826,12 +365406,24 @@ "value" : "3 Months - {monthly_price} / Month" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 mois - {monthly_price} / mois" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "3 maanden - {monthly_price} / maand" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 miesiące - {monthly_price} / miesiąc" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362861,12 +365453,24 @@ "value" : "12 Months - {monthly_price} / Month" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 mois - {monthly_price} / mois" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "12 maanden - {monthly_price} / maand" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 miesięcy - {monthly_price} / miesiąc" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362875,6 +365479,17 @@ } } }, + "proRefundAccountDevice" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, request a refund via the {app_pro} settings." + } + } + } + }, "proRefundDescription" : { "extractionState" : "manual", "localizations" : { @@ -362896,12 +365511,24 @@ "value" : "We’re sorry to see you go. Here's what you need to know before requesting a refund." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes désolés de vous voir partir. Voici ce que vous devez savoir avant de demander un remboursement." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Het spijt ons dat je vertrekt. Dit moet je weten voordat je een terugbetaling aanvraagt." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przykro nam, że odchodzisz. Tutaj znajdziesz wszystko, co powinieneś wiedzieć przed złożeniem wniosku o zwrot." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362931,12 +365558,24 @@ "value" : "Refunding {pro}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remboursement de {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Terugbetalen {pro}" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwrot {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -362948,28 +365587,10 @@ "proRefundingDescription" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_pro} planları üçün geri ödəmələr yalnız {platform_store} Mağazası vasitəsilə {platform_account} tərəfindən həyata keçirilir.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vrácení peněz za tarify {app_pro} je vyřizováno výhradně prostřednictvím {platform_account} v obchodě {platform_store}.

    Vzhledem k pravidlům vracení peněz služby {platform_account} nemají vývojáři {app_name} žádný vliv na výsledek žádostí o vrácení peněz. To zahrnuje i rozhodnutí, zda bude žádost schválena nebo zamítnuta, a také, zda bude vrácena část peněz, nebo všechny peníze." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Refunds for {app_pro} plans are handled exclusively by {platform_account} through the {platform_store} Store.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terugbetalingen voor {app_pro} abonnementen worden uitsluitend afgehandeld door {platform_account} via de {platform_store} Store.

    Vanwege het restitutiebeleid van {platform_account} hebben ontwikkelaars van {app_name} geen invloed op de uitkomst van terugbetalingsverzoeken. Dit geldt zowel voor de goedkeuring of afwijzing van het verzoek als voor het wel of niet toekennen van een volledige of gedeeltelijke terugbetaling." + "value" : "Refunds for {app_pro} plans are handled exclusively by {platform} through the {platform_store}.

    Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." } } } @@ -362977,28 +365598,10 @@ "proRefundNextSteps" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_account} hazırda geri ödəniş tələbinizi emal edir. Bu, adətən 24-48 saat çəkir. Onların qərarından asılı olaraq, {app_name} tətbiqində {pro} statusunuzun dəyişdiyini görə bilərsiniz." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_account} nyní zpracovává vaši žádost o vrácení peněz. Obvykle to trvá 24–48 hodin. V závislosti na jejich rozhodnutí se může váš stav {pro} v aplikaci {app_name} změnit." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{platform_account} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your {pro} status change in {app_name}." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_account} verwerkt nu je terugbetalingsverzoek. Dit duurt meestal 24-48 uur. Afhankelijk van hun beslissing kan je {pro} status wijzigen in {app_name}." + "value" : "{platform} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your {pro} status change in {app_name}." } } } @@ -363024,6 +365627,12 @@ "value" : "Your refund request will be handled by {app_name} Support.

    Request a refund by hitting the button below and completing the refund request form.

    While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre demande de remboursement sera traitée par le service de {app_name}.

    Demandez un remboursement en appuyant sur le bouton ci-dessous et en remplissant le formulaire de demande de remboursement.

    Bien que le service de {app_name} fait au mieux afin de traiter les demandes de remboursement dans un délai de 24-72 heures, le traitement peut être plus long en période de forte demande." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -363035,28 +365644,10 @@ "proRefundRequestStorePolicies" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geri ödəniş tələbiniz yalnız {platform_account} veb saytında {platform_account} hesabı üzərindən icra olunacaq.

    {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz. Bu, tələbin qəbul olunub-olunmaması ilə yanaşı, tam və ya qismən geri ödənişin verilib-verilməməsini də əhatə edir." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaši žádost o vrácení peněz bude vyřizovat výhradně {platform_account} prostřednictvím webových stránek {platform_account}.

    Vzhledem k pravidlům vracení peněz {platform_account} nemají vývojáři {app_name} žádný vliv na výsledek žádostí o vrácení peněz. To zahrnuje i rozhodnutí, zda bude žádost schválena nebo zamítnuta, a také, zda bude vrácena část peněz, nebo všechny peníze." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your refund request will be handled exclusively by {platform_account} through the {platform_account} website.

    Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je terugbetalingsverzoek wordt uitsluitend afgehandeld door {platform_account} via de website van {platform_account}.

    Vanwege het restitutiebeleid van {platform_account} hebben ontwikkelaars van {app_name} geen invloed op de uitkomst van terugbetalingsverzoeken. Dit geldt zowel voor de goedkeuring of afwijzing van het verzoek als voor het wel of niet toekennen van een volledige of gedeeltelijke terugbetaling." + "value" : "Your refund request will be handled exclusively by {platform} through the {platform} website.

    Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." } } } @@ -363064,39 +365655,38 @@ "proRefundSupport" : { "extractionState" : "manual", "localizations" : { - "az" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Geri ödəmə tələbinizlə bağlı daha çox güncəlləmə üçün lütfən {platform_account} ilə əlaqə saxlayın. {platform_account} geri ödəniş siyasətlərinə əsasən, {app_name} gəlişdiriciləri, geri ödəniş tələblərinin nəticəsinə təsir edə bilməz.

    {platform_store} Geri ödəmə dəstəyi" + "value" : "Please contact {platform} for further updates on your refund request. Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests.

    {platform} Refund Support" } - }, + } + } + }, + "proRenewBeta" : { + "extractionState" : "manual", + "localizations" : { "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pro další informace o vaší žádosti o vrácení peněz kontaktujte prosím {platform_account}. Vzhledem k zásadám pro vrácení peněz {platform_account} nemají vývojáři aplikace {app_name} žádnou možnost ovlivnit výsledek žádosti o vrácení.

    Podpora vrácení peněz {platform_store}" + "value" : "Obnovit {pro} Beta" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Please contact {platform_account} for further updates on your refund request. Due to {platform_account} refund policies, {app_name} developers have no ability to influence the outcome of refund requests.

    {platform_store} Refund Support" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neem contact op met {platform_account} voor verdere updates over je restitutieverzoek. Vanwege het restitutiebeleid van {platform_account} hebben de ontwikkelaars van {app_name} geen invloed op de uitkomst van restitutieverzoeken.

    {platform_store} Terugbetalingsondersteuning" + "value" : "Renew {pro} Beta" } } } }, - "proRenewBeta" : { + "proRenewingNoAccessBilling" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Renew {pro} Beta" + "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store}. Because you installed {app_name} using the {buildVariant}, you're not able to renew your plan here.

    {app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store}. {pro} Roadmap {icon}" } } } @@ -363122,12 +365712,24 @@ "value" : "Refund Requested" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remboursement demandé" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Terugbetaling aangevraagd" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wniosek o zwrot wysłany" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363294,12 +365896,24 @@ "value" : "{pro} Settings" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} instellingen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363323,18 +365937,36 @@ "value" : "Vaše statistiky {pro}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine {pro} Statistik" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your {pro} Stats" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos statistiques {pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je {pro} statistieken" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje Statystyki {pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363346,6 +365978,18 @@ "proStatsLoading" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikaları yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání statistik {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363357,6 +366001,18 @@ "proStatsLoadingDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikalarınız yüklənir, lütfən gözləyin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše statistiky {pro} se načítají, počkejte prosím." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363386,12 +366042,24 @@ "value" : "{pro} stats reflect usage on this device and may appear differently on linked devices" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les statistiques {pro} reflètent l'utilisation sur cet appareil et peuvent apparaître différemment sur les appareils connectés" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{pro} statistieken weerspiegelen het gebruik op dit apparaat en kunnen anders weergegeven worden op gekoppelde apparaten" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statystyki {pro} pokazują użycie na tym urządzeniu i mogą wyglądać różnie na połączonych urządzeniach" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363403,6 +366071,18 @@ "proStatusError" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} status xətası" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba stavu {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363414,6 +366094,18 @@ "proStatusInfoInaccurateNetworkError" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuzu yoxlamaq üçün şəbəkəyə bağlana bilmir. Bu səhifədə nümayiş olan məlumatlar, bağlantı bərpa olunana qədər qeyri-dəqiq ola bilər.

    Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti, aby bylo možné zkontrolovat váš stav {pro}. Informace zobrazené na této stránce mohou být nepřesné, dokud nebude připojení obnoveno.

    Zkontrolujte připojení k síti a zkuste to znovu." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363425,6 +366117,18 @@ "proStatusLoading" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusu yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav načítání {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363436,6 +366140,18 @@ "proStatusLoadingDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} məlumatlarınız yüklənir. Bu səhifədəki bəzi əməliyyatlar yükləmə tamamlanana qədər əlçatan olmaya bilər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítají se vaše informace {pro}. Některé akce na této stránce nemusí být dostupné, dokud nebude načítání dokončeno." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363447,6 +366163,18 @@ "proStatusLoadingSubtitle" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} status yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "stav načítání {pro}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363458,6 +366186,18 @@ "proStatusNetworkErrorDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuzu yoxlamaq üçün şəbəkəyə bağlana bilmir. Bağlantı bərpa olunana qədər {pro} ya yüksəldə bilməyəcəksiniz.

    Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti, aby bylo možné zkontrolovat váš stav {pro}. Dokud nebude obnoveno připojení, nelze provést navýšení na {pro}.

    Zkontrolujte připojení k síti a zkuste to znovu." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363469,6 +366209,18 @@ "proStatusRefreshNetworkError" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuzu təzələmək üçün şəbəkəyə bağlana bilmir. Bu səhifədəki bəzi əməliyyatlar, bağlantı bərpa olunana qədər sıradan çıxarılacaq.

    Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti, aby se obnovil váš stav {pro}. Některé akce na této stránce budou deaktivovány, dokud nebude obnoveno připojení.

    Zkontrolujte připojení k síti a zkuste to znovu." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -363498,12 +366250,24 @@ "value" : "Need help with your {pro} plan? Submit a request to the support team." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Besoin d'aide avec votre forfait {pro} ? Envoyez une demande à l'équipe d'assistance." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Hulp nodig met je {pro} abonnement? Dien een verzoek in bij het ondersteuningsteam." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potrzebujesz pomocy z planem {pro}? Wyślij zgłoszenie zespołowi wsparcia." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363527,18 +366291,36 @@ "value" : "Aktualizací souhlasíte s Podmínkami služby {icon} a Zásadami ochrany osobních údajů {icon} {app_pro}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durch die Aktualisierung stimmst du den Nutzungsbedingungen {icon} und der Datenschutzerklärung {icon} von {app_pro} zu" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "By updating, you agree to the {app_pro} Terms of Service {icon} and Privacy Policy {icon}" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En mettant à jour, vous acceptez les Conditions d'utilisation {icon} et la Politique de confidentialité {icon} de {app_pro}" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Door bij te werken ga je akkoord met de {app_pro} Gebruiksvoorwaarden {icon} en het Privacybeleid {icon}" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dokonując zmian wyrażasz zgodę na Warunki Świadczenia Usług {app_pro} {icon} oraz Politykę Prywatności {icon}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363568,12 +366350,24 @@ "value" : "Unlimited Pins" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Épingles illimitées" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Onbeperkte Pins" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nielimitowane przypięcia" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363603,12 +366397,24 @@ "value" : "Organize all your chats with unlimited pinned conversations." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organisez toutes vos discussions avec un nombre illimité de conversations épinglées." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Organiseer al je chats met onbeperkt vastgezette gesprekken." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organizuj swoje czaty z nielimitowaną możliwością przypinania konwersacji." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -363638,6 +366444,12 @@ "value" : "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of {pro} access." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous êtes actuellement sur le forfait {current_plan}. Voulez-vous vraiment passer au forfait {selected_plan}?

    En mettant à jour, votre forfait sera automatiquement renouvelé le {date} pour {selected_plan} supplémentaire de l'accès {pro}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -363667,11 +366479,23 @@ "value" : "Your plan will expire on {date}.

    By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait expirera le {date}.

    En le mettant à jour, votre forfait sera automatiquement renouvelé le {date} pour {selected_plan} supplémentaires d’accès Pro." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je abonnement verloopt op {date}.

    Door bij te werken wordt je abonnement automatisch verlengd op {date} voor een extra {selected_plan} aan Pro-toegang." } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш план завершиться {date}.

    Після оновлення налаштувань автоматичної оплати, ваш план автоматично подовжиться {date} на додатковий {selected_plan} доступу до Pro." + } } } }, @@ -371930,12 +374754,24 @@ "value" : "Use your recovery password to load your account on new devices.

    Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utilisez votre mot de passe de récupération pour charger votre compte sur de nouveaux appareils.

    Votre compte ne peut pas être récupéré sans votre mot de passe de récupération. Assurez-vous qu'il soit stocké en lieu sûr et sécurisé ;— et ne le partagez avec personne." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Gebruik uw herstelwachtwoord om uw account op nieuwe apparaten te laden.

    Uw account kan niet worden hersteld zonder uw herstelwachtwoord. Zorg ervoor dat het ergens veilig is opgeslagen – en deel het met niemand." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Użyj swojego hasła odzyskiwania, by załadować swoje konto na nowych urządzeniach.

    Twoje konto nie może być odzyskane bez tego hasła. Upewnij się, że przechowujesz je w bezpiecznym miejscu – i nie ujawniaj go nikomu." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -372546,6 +375382,12 @@ "value" : "복구 비밀번호를 불러오는 도중 오류가 발생했습니다.

    문제를 해결하기 위해 로그를 내보낸 후 {app_name} 고객 지원 센터에 첨부하여 문의 해주세요." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En feil oppstod når ditt gjenopprettingspassord forsøkte å laste.

    Vennligst eksporter loggene dine, så opplast filen gjennom Hjelpesenteret til {app_name}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -376012,6 +378854,12 @@ "value" : "Weet u zeker dat u uw herstelwachtwoord permanent wilt verbergen op dit apparaat?

    Dit kan niet ongedaan gemaakt worden." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czy na pewno chcesz na stałe ukryć swoje hasło odzyskiwania na tym urządzeniu?

    Nie można tego cofnąć." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -377502,6 +380350,12 @@ "value" : "View Recovery Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le mot de passe de récupération" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -377514,6 +380368,12 @@ "value" : "Bekijk Herstelwachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż hasło odzyskiwania" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -377561,6 +380421,12 @@ "value" : "Recovery Password Visibility" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visibilité du mot de passe de récupération" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -377573,6 +380439,12 @@ "value" : "Zichtbaarheid herstelwachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Widoczność hasła odzyskiwania" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -378733,28 +381605,21 @@ "refundPlanNonOriginatorApple" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Başda fərqli {platform_account} vasitəsilə {app_pro} üçün qeydiyyatdan keçdiyinizə görə planınızı həmin {platform_account} vasitəsilə güncəlləməlisiniz." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Protože jste se původně zaregistrovali do {app_pro} přes jiný {platform_account}, je třeba použít ten {platform_account}, abyste aktualizovali váš tarif." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Because you originally signed up for {app_pro} via a different {platform_account}, you'll need to use that {platform_account} to update your plan." + "value" : "Because you originally signed up for {app_pro} via a different {platform_account}, you'll need to use that {platform_account} to update your plan." } - }, - "nl" : { + } + } + }, + "refundRequestOptions" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Omdat je je oorspronkelijk hebt aangemeld voor {app_pro} via een ander {platform_account}, moet je datzelfde {platform_account} gebruiken om je abonnement bij te werken." + "value" : "Two ways to request a refund:" } } } @@ -378983,6 +381848,24 @@ } } }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld χαρακτήρας απομένει" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld χαρακτήρες απομένουν" + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -379037,6 +381920,24 @@ } } }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tähemärk alles" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tähemärki alles" + } + } + } + } + }, "fr" : { "variations" : { "plural" : { @@ -380342,12 +383243,24 @@ "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimez votre mot de passe actuel pour {app_name}. Les données stockées localement seront à nouveau chiffrées à l'aide d'une clé générée aléatoirement, stockée sur votre appareil." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Verwijder je huidige wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met een willekeurig gegenereerde sleutel, opgeslagen op je apparaat." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń swoje obecne hasło dla {app_name}. Dane przechowywane lokalnie zostaną ponownie zaszyfrowane losowo wygenerowanym kluczem, przechowywanym na Twoim urządzeniu." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -380389,6 +383302,12 @@ "value" : "Renew" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouveler" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -380403,6 +383322,23 @@ } } }, + "renewingPro" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovení Pro" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renewing Pro" + } + } + } + }, "reply" : { "extractionState" : "manual", "localizations" : { @@ -380897,18 +383833,36 @@ "value" : "Požádat o vrácení platby" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rückerstattung anfordern" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Request Refund" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demander un remboursement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Terugbetaling aanvragen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawnioskuj o zwrot" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -380917,6 +383871,17 @@ } } }, + "requestRefundPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request a refund on the {platform} website, using the {platform_account} you signed up for {pro} with." + } + } + } + }, "resend" : { "extractionState" : "manual", "localizations" : { @@ -387397,22 +390362,70 @@ "screenshotProtectionDescriptionDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazda çəkilən ekran şəkillərində {app_name} pəncərəsini gizlət." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrývat okno {app_name} na snímcích obrazovky pořízených na tomto zařízení." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Conceal the {app_name} window in screenshots taken on this device." } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masquer la fenêtre de {app_name} dans les captures d’écran prises sur cet appareil." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приховувати вікно {app_name} на знімках екрана, зроблених на цьому пристрої." + } } } }, "screenshotProtectionDesktop" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran şəkli qoruması" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrana proti pořizování snímků obrazovky" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Screenshot Protection" } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre les captures d’écran" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захист від знімків екрана" + } } } }, @@ -401998,18 +405011,36 @@ "value" : "{app_pro} Beta" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Beta" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Bêta" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "{app_pro} Bèta" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beta {app_pro}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -403631,18 +406662,36 @@ "value" : "Nastavte heslo pro {app_name}. Lokálně uložená data budou šifrována tímto heslem. Při každém spuštění {app_name} budete vyzváni k zadání tohoto hesla." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setze ein Passwort für {app_name}. Lokal gespeicherte Dateien werden mit diesem Passwort verschlüsselt. Du wirst jedes Mal nach diesem Passwort gefragt, wenn du {app_name} startest." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez un mot de passe pour {app_name}. Les données stockées localement seront chiffrées avec ce mot de passe. Il vous sera demandé de saisir ce mot de passe chaque fois que {app_name} sera lancé." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Stel een wachtwoord in voor {app_name}. Lokaal opgeslagen gegevens worden versleuteld met dit wachtwoord. Je wordt gevraagd dit wachtwoord in te voeren telkens wanneer {app_name} wordt gestart." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustaw hasło dla {app_name}. Dane przechowywane lokalnie będą zaszyfrowane tym hasłem. Będziesz musiał je podać za każdym razem, kiedy uruchamiasz {app_name}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -403684,6 +406733,12 @@ "value" : "Cannot Update Setting" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de mettre à jour les paramètres" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -404671,6 +407726,12 @@ "value" : "Startup" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Démarrage" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -409003,12 +412064,24 @@ "value" : "Spell Checker" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Correcteur d'orthographe" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Spellingcontrole" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdzanie pisowni" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -409535,12 +412608,24 @@ "value" : "Strength" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solidité" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Sterkte" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siła" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -409594,12 +412679,24 @@ "value" : "Having issues? Explore help articles or open a ticket with {app_name} Support." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez des problèmes ? Explorez des articles d'aide ou ouvrez un ticket avec le support {app_name}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Problemen? Bekijk de hulpartikelen of open een ticket bij {app_name} Support." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masz problem? Przejrzyj artykuły pomocy lub utwórz zgłoszenie dla Supportu {app_name}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -412227,12 +415324,24 @@ "value" : "Theme Preview" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aperçu du thème" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Thema voorbeeld" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podgląd motywu" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -412274,12 +415383,24 @@ "value" : "Return" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retour" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Terug" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Powrót" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -412577,12 +415698,24 @@ "value" : "Translate" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Traduction" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Vertalen" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przetłumacz" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -412624,6 +415757,12 @@ "value" : "Tray" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barre de tâches" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -415197,6 +418336,18 @@ "unsupportedCpu" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dəstəklənməyən CPU" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepodporovaný procesor" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -419274,6 +422425,12 @@ "value" : "Eine neue Version ({version}) von {app_name} ist verfügbar." } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μια νέα έκδοση ({version}) της εφαρμογής {app_name} είναι διαθέσιμη." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -419447,12 +422604,24 @@ "value" : "Update Plan" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour le forfait" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Abonnement bijwerken" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj plan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -419482,12 +422651,24 @@ "value" : "Two ways to update your plan:" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deux façons de mettre à jour votre abonnement :" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Twee manieren om je abonnement bij te werken:" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dwa sposoby na aktualizację planu:" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -419535,12 +422716,24 @@ "value" : "Actualizar información de perfil" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour les informations du profil" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Profielinformatie bijwerken" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj informacje w profilu" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -419594,12 +422787,24 @@ "value" : "Your display name and display picture are visible in all conversations." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre nom d'affichage et votre photo de profil sont visibles dans toutes les conversations." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Je weergavenaam en profielfoto zijn zichtbaar in alle gesprekken." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoja nazwa wyświetlana i obraz profilu są widoczne we wszystkich konwersacjach." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -420132,12 +423337,24 @@ "value" : "Updates" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mises à jour" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Updates" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizacje" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -421149,12 +424366,24 @@ "value" : "Updating..." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mise à jour..." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Bijwerken..." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizowanie..." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -421178,6 +424407,12 @@ "upgradeSession" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšit {app_name}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -423739,12 +426974,24 @@ "value" : "Links will open in your browser." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les liens s'ouvriront dans votre navigateur." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Links worden in uw browser geopend." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linki będą otwierane w Twojej przeglądarce." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -424235,28 +427482,10 @@ "viaStoreWebsite" : { "extractionState" : "manual", "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{platform_store} veb saytı vasitəsilə" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přes webové stránky {platform_store}" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Via the {platform_store} website" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via de {platform_store} website" + "value" : "Via the {platform} website" } } } @@ -424282,6 +427511,12 @@ "value" : "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifiez votre abonnement en utilisant le compte {platform_account} avec lequel vous vous êtes inscrit, via le site web de {platform_store}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -427486,6 +430721,17 @@ } } }, + "warningIosVersionEndingSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Support for iOS 15 has ended. Update to iOS 16 or later to continue receiving app updates." + } + } + } + }, "window" : { "extractionState" : "manual", "localizations" : { @@ -428926,10 +432172,22 @@ "yourCpuIsUnsupportedSSE42" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "CPU-nuz Linux x64 əməliyyat sistemlərində {app_name}-un təsvirləri emal etməsi üçün tələb olunan SSE 4.2 təlimatlarını dəstəkləmir. Lütfən uyumlu bir CPU-ya keçin və ya fərqli bir əməliyyat sistemi istifadə edin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš procesor nepodporuje instrukce SSE 4.2, které jsou vyžadovány aplikací {app_name} v operačních systémech Linux x64 pro zpracování obrázků. Proveďte upgrade na kompatibilní procesor nebo použijte jiný operační systém." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your CPU does not support SSE 4.2 instructions, which are required by Session on Linux x64 operating systems to process images. Please upgrade to a compatible CPU or use a different operating system." + "value" : "Your CPU does not support SSE 4.2 instructions, which are required by {app_name} on Linux x64 operating systems to process images. Please upgrade to a compatible CPU or use a different operating system." } } } @@ -428961,6 +432219,12 @@ "value" : "Your Recovery Password" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre mot de passe de récupération" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -428973,6 +432237,12 @@ "value" : "Je herstelwachtwoord" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasło odzyskiwania" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -429020,12 +432290,24 @@ "value" : "Zoom Factor" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niveau de Zoom" + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Zoomfactor" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Współczynnik powiększenia" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -429073,12 +432355,24 @@ "value" : "Adjust the size of text and visual elements." } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajuster la taille du texte et des éléments visuels." + } + }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Pas de grootte van tekst en visuele elementen aan." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostosuj wielkość tekstu i elementów wizualnych." + } + }, "ru" : { "stringUnit" : { "state" : "translated", From 6040c74306f5d3de99235cd8bf026c4461e96afb Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 1 Oct 2025 13:46:51 +1000 Subject: [PATCH 244/244] quick fix for unit test --- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index a6dee58c50..d60698f19d 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -690,7 +690,7 @@ class OnboardingSpec: AsyncSpec { timestampMs: 1234567890000 ) )) - expect(resultDataString).to(contain(["TestCompleteName1"])) + expect(resultDataString).to(contain(["TestCompleteNamee1"])) } // MARK: -- updates the onboarding state to 'completed'