From dce321c77a1a36b462efad1f6ba9403bb8126370 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 17:23:58 +0200 Subject: [PATCH 01/16] Fix build --- .../Sources/JoinLinkPreviewController.swift | 4 ++-- .../JoinSubjectScreen/Sources/JoinSubjectScreen.swift | 4 ++-- submodules/TelegramUI/Sources/ChatController.swift | 5 +++-- .../PeerChannelMemberCategoriesContextsManager.swift | 11 +++++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 5b23c18ed4..3eed8b32db 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -139,12 +139,12 @@ public final class LegacyJoinLinkPreviewController: ViewController { } private func join() { - self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peer in + self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { if strongSelf.isRequest { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .inviteRequestSent(title: strongSelf.presentationData.strings.MemberRequests_RequestToJoinSent, text: strongSelf.isGroup ? strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } else { - if let peer = peer { + if case let .joined(peer) = result, let peer { strongSelf.navigateToPeer(peer, nil) } } diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index 8153b4a429..c3b13d0b63 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -350,7 +350,7 @@ private final class JoinSubjectScreenComponent: Component { } self.joinDisposable = (component.context.engine.peers.joinChatInteractively(with: group.link) - |> deliverOnMainQueue).start(next: { [weak self] peer in + |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, let component = self.component else { return } @@ -358,7 +358,7 @@ private final class JoinSubjectScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: group.isGroup ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } else { - if let peer { + if case let .joined(peer) = result, let peer { self.navigateToPeer(peer: peer) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 072e45985b..b2fe984711 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7743,11 +7743,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.peekTimerDisposable.set( (strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData) - |> deliverOnMainQueue).startStrict(next: { peerId in + |> deliverOnMainQueue).startStrict(next: { result in guard let strongSelf = self else { return } - if peerId == nil { + if case let .joined(peer) = result, peer != nil { + } else { strongSelf.dismiss() } }, error: { _ in diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 5df3fb288a..e4502acd6d 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -451,11 +451,18 @@ public final class PeerChannelMemberCategoriesContextsManager { return engine.peers.joinChannel(peerId: peerId, hash: hash) |> deliverOnMainQueue |> beforeNext { [weak self] result in - if let strongSelf = self, let updated = result { + if let strongSelf = self { strongSelf.impl.with { impl in for (contextPeerId, context) in impl.contexts { if peerId == contextPeerId { - context.replayUpdates([(nil, updated, nil)]) + switch result { + case let .joined(participant): + if let participant { + context.replayUpdates([(nil, participant, nil)]) + } + case .webView: + break + } } } } From 7b11a88231b029a8ded96598e5e90db7b72c735c Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 17:25:20 +0200 Subject: [PATCH 02/16] Update CI --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e30b0e9545..1a268ae4e1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ internal: - python3 -u build-system/Make/DeployBuild.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/deploy-configurations/internal-configuration.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" - rm -rf build-input/configuration-repository-workdir - rm -rf build-input/configuration-repository - - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 + - python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64 --embedWatchApp --watchApiId="$TELEGRAM_WATCHOS_APP_ID" --watchApiHash="$TELEGRAM_WATCHOS_APP_HASH" - python3 -u build-system/Make/DeployBuild.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/deploy-configurations/enterprise-configuration.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip" environment: name: internal From 8f4f46326586761db07700489e3df9da4ffefd54 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 18:17:02 +0200 Subject: [PATCH 03/16] Fix layout --- .../InstantPageUI/Sources/InstantPageV2Layout.swift | 11 ++++++++++- .../ChatMessageRichDataBubbleContentNode.swift | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift index 5d548a2b80..dfb798709e 100644 --- a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift @@ -437,7 +437,16 @@ public func lastTextLineFrameIfLastItemIsText(in layout: InstantPageV2Layout) -> else { return nil } - let lineFrame = last.frame.offsetBy(dx: text.frame.minX, dy: text.frame.minY) + // The stored line frame always has minX = 0 — alignment (center / right / natural-RTL) is + // applied at render time by `v2FrameForLine`. Apply the same correction here so the returned + // frame's `maxX` reflects the line's actual on-screen right edge, not just its width anchored + // at the textItem's left. Without this, a right-aligned or RTL last line — whose visible right + // edge sits at `textItem.width`, all the way at the right text inset — would feed the status + // node a `contentWidth` equal to just `lineWidth`. The trail/wrap decision would then think + // the date fits trailing the line, and place it directly on top of the line at the right text + // inset where the line itself ends. The width is unchanged; only `origin.x` shifts. + let displayedLineFrame = v2FrameForLine(last, boundingWidth: text.textItem.frame.width, alignment: text.textItem.alignment) + let lineFrame = displayedLineFrame.offsetBy(dx: text.frame.minX, dy: text.frame.minY) var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 var leading: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift index bfc42cdbe2..f2640d0146 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift @@ -576,7 +576,15 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode isReplyThread = true } - let trailingWidthToMeasure: CGFloat = lastTextLineFrame?.width ?? 10000.0 + // Measure trailing extent from the line's actual visible RIGHT EDGE (after + // alignment, in page coords) — not just its intrinsic width. A right-aligned + // or RTL last line has `lineWidth` worth of glyphs but sits all the way at + // the right text inset (lineFrame.maxX == text.frame.minX + textItem.width). + // Feeding the status node just `lineWidth` would let the trail/wrap decision + // place the date inline with the line — on top of it. `pageHorizontalInset` + // is the offset between page-coords and status-node-local coords (the status + // node sits at x=pageHorizontalInset in self, and pageView sits at self-x 0). + let trailingWidthToMeasure: CGFloat = lastTextLineFrame.map { $0.maxX - pageHorizontalInset } ?? 10000.0 let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: EngineMessage(item.message), isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)) From 045ba1038611f0526c64c4887c5b83f139c078e0 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 19:00:35 +0200 Subject: [PATCH 04/16] Update tgwatch --- .../ChatBG.imageset/ChatBG.png | Bin 0 -> 70575 bytes .../ChatBG.imageset/Contents.json | 13 ++++ .../Messages/BubbleStyle.swift | 15 +++-- .../Messages/ComposeSheet.swift | 58 ------------------ .../Messages/MessageListView.swift | 25 ++++---- .../tgwatch Watch App/Messages/ReplyBar.swift | 20 +++++- .../tgwatch.xcodeproj/project.pbxproj | 4 -- 7 files changed, 48 insertions(+), 87 deletions(-) create mode 100644 Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png create mode 100644 Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json delete mode 100644 Telegram/WatchApp/tgwatch Watch App/Messages/ComposeSheet.swift diff --git a/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png b/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png new file mode 100644 index 0000000000000000000000000000000000000000..92c90ecdf9d39397bac87d7a0267593dfd8bf275 GIT binary patch literal 70575 zcmV)kMJs4pv}nt)RJw7#SJyCMG7l>FH^&MT-_KipIFt;$C5Nbktj0Tk}?ymrdY&-re=Se*NmT zXwjlYQD_CtJ;H+r54=B~Klgs#-g@speDLPy=e@9eH2AeCY_4TW8t0 zUs^%aqD6~B(F&Rd0|BwLv}7)^lamwg?CdP|b?fUJ-u3m32^kYEq4(c90n-YZ7A;y7 zi8kx3!63%6xw&cCW6xdeg`WxbhcPoV<1H-Ad%G^^9~LqIciPXNKkt>GVSQMysi`Rw zv@Ke+C?2gv(_p|Xudc4V4=%8e(0KCXiN#&Ex3}%@Fw*7aWpkNbTwMI}UjBv7eE867(V|7MXf2usmG#=9UT6hPgF)~g0fS}2zW||fcz9qgB3weJ z?ln=@5Q1rSb=BM7-#0;YA22;N?VX;Ub=C>o1}WYMA%Fmi6Ut42T%}mF-;a;Iy}dn) z%b`>d!`t2c>@3)0uSJWp(F&S|15Fj?LcBqmI?qx4k2ZUJ{3~OD8Lhj&!BC@r8)jhn z-Tb`yMd(gC0b_|2&z^gCo}Yb3JPTK2G;yK{AUOnAAThhMkb;3*mHSy+TX%bY(#E7( zl#W)=G#m&OQk=;+Bd`syQi#2*tgQT^p(A7vK(kK6g&8%GnWn|q*q9ZivPP$;r@#E| zn{%0+IAP0r!F-9?UpFB^d9WGMt4`C!L7R7`Iba2|~&bCZUhMYD!+B6MMt zSSV4zU}3Ci|6-&Xoj?jS--*>%cW}M9N7b*6qf7?~gam7g1&fg7_oRZB&&2BGy#S>J z>$pm{8u!LMvablwvzP>zDy}Jv*6r9Et)Lk~AXvym!;0Zx!fdK`g%|^K%wlnA(f$0O zBTy7000YlY0C9K`m}h6F?s496>F8c`xHanDw@Cfoyx0RjfA3+o-aE)$$7gg4($7T=`Nk6(r;1xc<)_JlQS zh0nhzM9{z_*bscgEHJ(DCkXx-nWkKoV9Xg4B_hP+A*eg_I(H<7&PCZ4%R%! z`+?HM>l&L0Gk>lV1wwAIZeYz|)Vzjdk+iNb)_*;Sb7gt|pkQzQ^Pm6NcVk(Z;B;0S zsoG^g$s}a+`Sf?|T{uhFtYIg>Z-4r0&-y4X#_QM@G8kLF=OY(EqrfYNpN)Cs+z&U7 zAg#%x`vzP&91MyH??a|jT|nR$X8QSaIfpU~XhOZ%ut@gzzjTC4*3X0Kph+UU6x^1D zg9gdxt7Zhtgy-mtfa^$#56lV%-84Fckzt{7A980Uy$9ZlOOW-$C79MSQR|)uj)A^? z_pa2nVo4&jQx=H9TxvaBKRr5E0{Q}{k* zP&~}#Xx4_O)C(ZlWJ1h!ZL1nhH;hs?v*hl^?GWMZ_5Zq-56_QDmGy?s=?~>9I z>tyuq)Oox-mu}7KsOUfs2vIEM1LtNp_uu$Lu5qw|nrd2Vq&o-@K)=mbz!vSSMR_PY zAc<>dV`IHjtAoISIdN!JP+A0_FWel9PAj9iD2D_ENNK%I1a1t349Xd6H>eg5bA*{} zZfs^chrFNbSYKN=hFm7t!=e=h?g?vn)_G^3p(tNd%mq42ng-AN@$~Gub*I*iJ*^wK zMg%Fsl?UFbv-5lv)PV$zWeEj-bD%+7Y!;glCPp+MBs`ie3m%=D(xUnBV*!uMRfh%+ z(c*%@)lsx1i<=P3i5S8#)PDF=QJ%Q6;j?A&I1C&F{E@g# zAZDxSaKFP~keDzYE}p-U=BjM87Q=&;8@#?x-7NA@lk)2^jzj zpA0_Mt!~N?v;eQ>-ixSH%G?Mcw7I!q zrfCzP3Tma>h7XTYayf7a)5{?Bk)Q8V|0vdCd1cu&Bi0Ic89~%IA)^ek>6vM7%4v}N z%)!H4zew5sMBmkGlyzMo8Umt^IAASIIG6N0=e{W-=q(o-iU4Od+|fD-{5CZgRvf(j zaohG2uf|l+q?Dhz&(V=li%eg9MeFnp~q64!Qmkgi#rB0nF7DNpSPAf z&!K=ZN*jcMDD{@t`}cQigr(xGVb7(^0Z>|$Yy|c6Z8(r{s?&)2@EAWDA7&8t%z$b`-a9w3<`v@8KhjmCoLtHB{NACZ~ zSpjLzHus3N3ccT#Sx_Lepn(a5g$Nsj*~MW(L*c+M|08OeTk4?G2GVE(Wdk9~0VKvC zmmiEwS#`Z2ZGDRAmWvCv9uycJ9vm8x4QsPPn2r3pXnR;T(L)Pkp~eLZm+J250fhmv z_Gt8T3v=Gb4$;2mQc(u|^0GASNAF)>JD4t(~72n=Qz9?(gqe90<=s zQmck*Bn_5z=V!VrU>!-gq=D!KrVGD7;PAP`Grp%};8_6;$=#nOBa+vXMaFykSwRx6 z@IKaEblwV@KCoa`jO5n=fuX=u5jfIhq`|;M&{&F0bIl4Y5^JOH7y{P^Q$SFr(Ug_J zVfW~Wad&sm1Qv$~28`CExYaO$y@f$w{rMXEuZ&U^>Kw{$S&Aqx&LbH00Ufp)T5EZoNi+NxT@bzvW#NS!ZNQ z27n zN(3zjn>891>qhZ84i5jS@nQJyFjRy#%1A$`=3{<-&PEc)HFab^_)M(mvin|T0kouc zE@~LcefjC&8{UsEI)-D!`M2J9o2*pz!e3If(B`zzt4HZ)(|*+&WE0UvnyaJe&N z@4g_DMI+5q?I8G_g$_@73yk7h~hlf+!L3 znFuf5i`9g-4%1CigH(5fdz5kLqQZp3NWImzTf4aTvi4Ee4QsN!y<_c1IJnWo5v7+l z<*@qqvu&C>*4odXbXoppT00gr43BmA?0)YBr9H#M@~BJV%%U79AwvQEnVD%z^;N

vKKqT*x3RHlmN4I)w=0eNqPW)r^lNLKhM4_;phtOzF8$=7ad)oAEhecl zj&Ai^KL5La%{DVn`1LqiqZL8?S?K>L_F&V*0XCVA#tI!1Z(J`Vl*y zAq_DO%J}gQ(MD#oy-tu@$oJWg{khiuTDR^ZK*0dBLc9DPyy5XzB-NHw;IBMCv1u! zApjPd1wUi4gW*zjbeXA}E!UPbr7=(NeK1ah?}rZ@qGxuvn^IR4g(X1T`2OlBNLm1>CWMJBwy1M(h`r&51!2M#u z$@X<#lYX_-4`byc}J(6wPH$4&ze3mFCQ%3$_ktA}irot;k|VN>^_!*1ZZVT6j;bcT|7e(WX* z6JrmSmsiYP5*AF+@xvVaDinmGpep))?i3WaK`mdt3sa#qA9EJv2RD;yubhcyKi>gmoNLKWW)b@Su4aAIrHZYJ?B}Gd&4mnSuV9Y3K^Ib z1cde?B102@RqR;LtRw1XrP}aL0eQcfZ^h>#tG4VVFfU^7~wS zn4`3PC0h*BxCv#E^~yrD_?KJXL2571(Es@J4{z4F9pAipV^+ejrP-%}nyxD0LFvfQ zAwr)u+S}brl)|mJ78zyTpz#@T*}6qRanq`in?=(EC7l`>T@I|YI%_p@d2ZUcp5y1Z?!Rc*9LDe=@g@@7vjiy~A6l@X~4i3J=3MZ6YzN4t~ zt`sBB7BA2q`3nF~~gwfHz_&IOwv*S!^@C&)+| zwKS}7Q^LA;%2j6wxwgVMK0dMMbPvQE}rR~rRU0($w z#G{asL~N#rL;$W;Q&h>M33ocXsO*8rw%5c^X=PF z@cz|KeMMdf*8o)o1*3a00%_k#nZ-zujPvT{OJg{UPf#IblYjz8##vFxGMqWq3cesK zL`omQ{WNYT1lQr$!%oMFFyIe7c6UD);;tlXkb5CRPZLn#H=IZHkc)FymxX&HXgG8? z?x5yf3&4iLG?Nq#a#(t`R8h4sJ<=qYc>Vi;VCo*j@%v}(cRB^=u$gP}(&~E^Dizg%M>#hIN{y^@Ru==6tRL9^7jtt!C~_xZmM=R$y9(^;3blL3)o~FTUCW{6 z5ORMId|ZQ4)hxhVV5tAReQTc~trmfvm!hDyI2=xdI|ApDo?8_$WbP4R2aR@%L#TyO zx8i5+N$s9kM--JNcAh{`qG+h)4H|k;_aK*~QkVtxrF-;f%Q`qD{eE6I7jz*P?Q67o zTqD`M0B!Ic>UN;mjJ}Wc09*$we4Q)st?K*nBO}hZ7=#ge1|NMDV4cy< zNk>JnOE4lV=>UK)`NfMDotE+(0NQv^kO+f>Ir162j{}SQP^qVRL4~9+V}uu$){Oi8 zq@sh!H6TO~Hr-N~St~T+Cy$?aAGbgLqOqd=Bi!d5lOuz%iLn^&LqqHyxHHS%l+k<_ zdcy*=5yl}!)W&;U-*ipT@e7XqEVb$BDcgh5(J|{9z;jse=rj>-D}(z1OXl|WXYAO4LBK)en#5pO`zn(RG%T-QjSvEc zHGTD;SKd(&yF!CTP*XJcw-e+|YRdz-2WWx4;H;{{cUgiGnuHlYdi2s;f@n1r(7e#LcQit z3W*zXUJ;7yhf=TAX#k;#GDVYFl%%5F-OvJaz7UM!XF79SKUQ*;B>`~Fp|9A&*9)P9 zT@sq~n555H7z%Rd<%YBQ@J{oG5I;w*g=>UJmKK-1F9%;5>2GJP)b50z3Fh0-D0cv& zsmw6iS;zDj-GLu>40PYcs`70)Qg0yiUcP)~mK6oX!v+Z#EHDHMeeNzVuZ*sH#iq&v zmI~d%i3x?3Qm%DtYtx<48&d*!^tx8=6D5|ZyxYNO`=Wu>#W}OHg0OT#TOI=V9JJNC z&I*JWSX5h;;7QB4VUTDwY+$cmTgse*X>|Jzgbdm~hcDcjRyq6{F{{qGO|0UU2ClSc z4lTkHtxt&qbp9Tn81HnoWv}`j=3xZ1b@ctnd$|0tYtdiRp!25@)zdT6-p`w#c7Cu3 zikvl*7OX$kGqEmck88t~3B7WO>b%_xI@6O*h<^U`xfCHY7Q}=YfByCGtF5=@9C7WO zCz{Z5ZB;34_aa6U7oP!H2pr9eAZ0&4IG>G%!RxvOG&y)^8*ksf?J!lP21DQoLWCKD z4UL6@vZiYHtiOukRD_BKDM891=P?Y-H7jh4fU}etfCdd@QH1IO1@=+478glrAMuB# zZSR!Yf+a1~LHqIJN7lF%Abeoh{c4!78aS)GuHW3MJfZV+a(vR6?u(E_Fmm0SHjc!f z{8?2a+MyRTFfxcfPwhDAW_{{0KZ65Eb`3-v{=A1y_#mJVDiUV6=?Lm^FT>zQ3Ll0R zzXD;)enTI)8JiZ^*XRLj=|cR->ZLx_!Znr@rcA#7V1ayPmO-mnDqkYgftB&ce!!l4tePe$7cepC8UX{nV!2vl-G*AIXHNOU(|~ZCX&lWw0DY#J%CCaf5kdf8T}1WmVMZe=u9*#V<+L4(N$k;X~nyb!m*{hC}B0r_uX zAXqZdbXo>4#SMugczS((*|@rh7TJJcfj+QesUNCyZ#T7e!rk{X=#qiUXc%0en(&Hc ziI;&Cc*|fN8@2Hl+#~yg)es(pRwk68sX>nLP^B_!4=heLK2a(Es8rBkl9HVPV6fqO zi#)++M}-ZV7g=51zK;Wx=f)t^@ZaLMhKa-#A3`vvK}!t5I`1w$G-AIm*;g(60#4jC zxL09jb#pT+o6w)2>$$-GUZ+>zumOh6`QxFH4uE^DLktZeLxvdw4`B#zo$9M4L(l4a{B(kff<#7Wj{WsL*>c3X81#Em zYkw@W*0L!F8ks;{1hiE4WK8xutZ0y|&}IzQ2k=l3#dAb#CX8b;8lV(@9tMOU9cHL0q2_p4n!Izv z-dGLLd1ufYf~PDC12-Cb0==Qc-~q@}K~p6^`%ADNL90G+ZPsIbfno|D(!4A7QT0r{ zW?+?tdr?OtCSapf-J!H;g-+ny1d|QJuZhc01M1U+n~NSsv$Hea^prm=iO)pa)3Br{ zUs41&&c;$__c^Zn0% z{$u-$RTph|I_VyB^K;((+`KpGtR5IL>FLKoN9v*(v}WqSV4xih$kV{NaO2Xj0;L)< z?!oTEex7izz3T|g`_#;|$*}32tE-L<=Z$Be$U+nBHx`YQh03qf8)8AjCj9l+UzWMU zCP=uL0C9(u1IGjAtDpTmd3$#!qeF#R96T}Tq*^OX`;R~W@LvDjI~d0`&^Z>yUBrMk zvV|Cpd=s?cN9d{DeDpd!Pd)+N>(-INaw;)Uv1XVBHM z2_25q#U+Et1jj%IFSwL@r0R)E^zD@%-c%E`&G zjXWrFouDrs--2L03KQ3&0>iLt_-EiUZnFlP53CjvG~#HB=gu$AO#!Mpsc5JNpP`-M zOGnt?-pZpkCJfEEpP%E>w_#m&gI;rL^qEv}K*W4*T*W%;%=aS9qQ$aJ1N(AzdS(g*`>ObbMo`B=*w{}i^-m<3#LZSgiNKnD;0`}73+|m+A(px3loHls$;JkcngAXqZ5~>K{-Gn*OAHVC z@|BtidtOsD6~hXT7eD}MBt|QAdP6Q#tY*$KJcn8J6GcD}QwcK(xhHx7_X&i627-&KjFuvrrx6E;6DtM=5gv7&1nPF6F4+8x zcEx9D8X@b!{i3nfp;y-+;C@LL#R^3W@0R-OugR3nZx7Q0Yt5dsAC{cqlnofGl2wL* zsd6r&f>Xo%RG7RKbOVtqPw}nJIB$0r5jMmXNnQz0Ty0%!zeDRMuugJoldMyR^yZ)~D^m8B(CYLd;4GsYr9x&Co1Jw)ac8idr;O~7&#h8}<=q#tx z(;r=h4GdJ11=Bz&l)R67VJ*2wS$X6oI}cI=u};2zJ+uwtxy_ArGe;q64h8WBG+8zetu_szOF*Fo zV>Rr#16LLSAXpKk)Wq~+Vp$I^){Kf5(d5yf!VFdd77R*VPd2xI%AFwrJNy1aMwZ)y*vO)zvjGhE?FBsOSC4T-=hAwSAxd0 zBj?u9uqFtWMwVf%6vFpoEvFs7GOt5e;0j4opa_%aU?9-Y-q7^R?!L?6*m}&Hw?)FoB@Oi z?m<~FtQS@e)?Ctwl&l5xBNyYs!lJk0ybmM6-`ZGCy>YJu4aA5`hl4y8#1{6uSQipF zFlbyPXr5OIn2A!J2^u3{ZXAQ%-u7KdRkZTJB{MxeRanPgm_`ow)c>w)V7#yo6k z(A*PyjWRK4nm4q8yN%C4*sLtCbW+iqthJ-q;fkh}QTRTvRH!pbYZ3Apy!Ytnn+apq zYIEb?c1UfzoO70+TEx-s;M#xjFcpw8A3nx*JCKh7=cB zHtI9h3#FK8p7T@2Z{2H`I~hbxOAl9)0}5B%ePWrcI3+|Wx>>;b^Ioh$K2PUy zWOVpa$D53MEohWlsnN6?X!SkQsAoN3Bxs?$hPchDvs#EfOgKi03BMN%q=MM%V!kV_ z6h@e&l|1){1`4SEpbgE@wV)m)y9l5oeRn#p)?@j6;if+?zU@ zZcy8GwSfP(ucpVC|fx z2vj4WuSz)R+$)+a#gKdxt{MW+WyOV94{BssLxi+vpjlUNVLd(l59jkXxs6d?9v+W)|dTecg z$PE19zKc%ASaaOBTA`Eyn4H{bxW2F?HKGAaW_E7Y`?T{ZRR$ikiKeb>Nwxi{EB&FM zQ3}8{_e*K_d$Qdao#fdeL#=Q4ek@G{4ro)Nt3fMhh8P?$G(`kWKg)~EorA-JzHTwv zrXakM%*#Q8hk=mUS7agpn)AknKM54Z&OM+kV0pgu}F47ILxNtQL|XoS*%Bo;rkfu)9Gly^@qxi$G*8%yr4jh~BlZ=($G zeVdybb_Su*`a`eJpjjsA7p2%n5@CRK5Oyi{0*6g=%Zr4v;-ZIG1IrLRSQF|d zTr{|r-OsxwU=eh4&WgcOq?mP3_YG;ihX;q|7Sf1m%~mQqHWdf11?w0OKvLxb8X+dg z`w*Z-00o4?Y=k|oO@vxeK$tP}ZokTN3v9fv}9;FMq>7&jBYmjr$X(R&;EQ^^qk_eL7tN;OwdzE`4zSWDqx{X>7|E%A` z|MI{RN9o`?ikRE5|38G@>yYE(_|fMzfCI!~hN&0r6Sw|YmfE^t1ID6|JB#&z z$#B?}+)yO=UKkq;gzqJ#nXwV)=ke|#Gm!9k?g$87>dyZT0 zd&Zh61T%Z1R!^c|QzC*)(0`}kj{G7}c+H6Sudvp?njst&PDzxiwBY+tW;Dt>4E6;p zPZ@*=B7U~^Jf;xd*flW;mHkUvj?fg88l!Y5Ud#POMT^{V;jl>_lL6bsY~5VKl}_jHTw;2XN-nWlAt>Y7&X;YU6E$3oCOId ztn+b@Qq4L*58>n<9uio2w?wjz(~btW-JW#2f2OoDLV|0+^1<>$=y081zxo!IrrZt_ z?<-Bvu(9|pk>YW)>H!1>vx7kaX@f1YuQ0IeV8R z2<>NG-4)`Vhy&MeL!aEgmp$SjN`ED(frHNb%IKmkAvZHkHr$#qO_-$CnDYU1Ns?p% zG?D>^#@<+uv|x@4pDBf|EP>o(G_gdbJ93jj4_i`I@7TGrhDKWd~S>m45#CI6FoeQP8&cbFdix|g%I4}qq zB)k>1^$;=&0Jo`%z2UoWzPZDBZdM}9i1m$gkwMt3Ihismj%l9-jBwido#}kd0ZLNq zpfucQf2$$^V>xt8jxGkoD$HNf1-={Oz%eL)v;nlKF3Ls1vd%udx2cDIM+$m?eKuw( z#+ejy{XC>7F<`Q{ZVgZhP!=>o8HQR$pFQ7OF_KtNRs|bt5u}~x4e0)|k2_Z5wKbb3 z4xB@HBzrt8igsy&hAa;bGR%rY7M*{hPRF=f)QvTI@cE1dCyZttv&5w4wZrDiEDC6e zZ{EByZ3-c-dLa&6c%32sckkYJTo?Gh&m9xy^+{qHs@+l7g)(vMXbM3?6VW7$M8R53 zBrKy%jvqtWwPvfFgFpZL(*$zdq|RQzMaDVNd!=+KgOH94)^O4Z9kqce(sDzAse2%s zwXxLv!{Ua_^~)#YdvSO6+6xI61lO<#n$lcn91JU{bHah2qcH=741|FHA7-Vj5Dt&p zpP*@vkBysq1V-4p*3yBslplL)+Ak_p)6H`H8| zpQ?McpHBkar@0oL@^YFW(IF2grm$e0nwL#DuwF@0(OGNOnb-0j?m_#bG;a;Fvvw@b zOP~p-MF|3CH!-@p&h8%8K$dn9f<`f=ehqBDrm->kU%&n@T{Pv z0%K%baA-8nmA|ubWTIe}fBwAN$=uM;s21q~@%}jZ(b0s}zilqC6vG)g(ezL>|Ix8g z$5bA4rbF5=D#tAOJFXh`v8r|-YLk;Dt(wl*n z1AyX9GVRy{399J(M(5>k=?fd&7I zj*4$GN=J=r|Cn3bw_)^Rt|YEASf4Qz1{C*Aqx<<1>Rn8D*7Ry|xZo)rv(rXT@)+KkvIU9n?z=rA)$}mixQApMT4y;=Hjx2vKEj6>%R7zQglC z(vKR8$||l}GiTkPZ`Q3S$RgzoWff-Pi&2+$(@~nBVbP==!;BCdXwe)N4houZm`TyX zV{3EE0@(y^^*0XGsS`+LWD4CEDAPw-PwbOs(I8m-s&kVyo!FT&ed;Le5uC)Kps_Tr zRbKn?%*x^%!8ADsVP>WlDYYMbVXolQ-3>N6xrqM4hV6-zJ(T+#2H z!1Ozi`?W1)S`qL91dIaOaB^UQ&;nZi0rzRf;1A0;2L=0dwxaGcfDQ2r3*X;~?SFm8mcAchz)+fwbq~6p;hp zVz>akX^>r(3UC<2+3A^C-OA#s3M5)^CZwd@-2C)b98x2oFWE3qM( z{ikspJTIYx#fC6=_>iQAiC;1x2ZhZ5di1DNzQH}IU^&jJQBn;qO+TA<(_Exj&%;`- zkz3Z8f_|9BiwlePvqlKxj)0IRg7imODG(F>XkHWDkhsI4+KkqkY0(d|2+$IBG+UcP?jIs-f?Y3kJ6|bxu!Qm6cGOG z?3`&Q8qcaW5sOQU-o^R9Gu8QKU^K+I4 zyJ|Os^>920u_>{$?#dQe)?UyJ^@zPk3d9&L5H0cQb<5DX$3fG-P*XW;(f zz#Gju25@tIV?l0&P1JIj3M?L&Y!#Lm8FN?HSDD;Z@B;)C*Y3xhT#($kzosUq>|T=I z5BE>3x6D>ZVQ}vV8+l^(zkD&vMIAZn%=e(VmoHzK(U3>Sj6w=Fl`gCJiwI-bCd_f* zY5hJsJ7cM^YzX%(ORYuqfZp)a589-V{bX-dA5tbbKUfGbP0kep55ePSCtZ5$8|zNk zjCOj{0Zrj7V@yLFre~(j5*cKKx7?p6CqHcOOyF#7Iw3l3`_N0^@ZF?)BblY6dv zbHV!rgoJ|US%doQEzFIClyJ;POdtPyQFTSqh;fB!G&R?-x4-8!f4??n)!GU9r2;UH zhfXk|LFe5HKPg~f2**LrK4(lBcACDak_pxUt4$r7qgW<3;{CPVEkIMmc!gzpVTqe9e{!0Q3_1Q*()M~?@Z_z68@ zRlwl*91f|5j}eo^%}`Vv2O7hl&!OjExigg6gw~Fm26r_*$G*DPM2n!y1lPN=vf6i) zceu3?mM3egxh|~jf)k$fH)OrZX64>Bsg&2KS6|p$!e=~w^4MBdwVs7|Xw6ZazJK>? ztVprO5*>A}Slo>WbJkilkb}@tFn_K%XuR&Q#4K2FX_XYvMg9H|JvBwt0JWW^>j4!C zbEvR9Xf=~n3E=(OU(!-hG&DnmZUTI6nMIyaXdX=+lpHJ&b;efqm>AYCz4L+xklX#7 zVPp&vov`W z6Zn|MXd~Py{6%^B>#x7OpYAo@i2r{0846C8SZCoaXoju3GFl@W0)vTLaCOvWQ(Hk( zEtI)LEf)ox&>{?p!5!=jAZm3#4nv6m5PpklfyKc- z;X<-mMlP5vw{w+2NnecAGfAJ-gijx662SkPt)Cly|WJC4|(6A zxp|`R4Sz?Ae}UkjEfL8g&}K9-BCHTW&&0H%e}nIlMaYte|DFb|ph*Gz!4ne?%r!!$ z55hzvVP&~-C}3u-ps5xd4&0l#xRjF2!C`-}I0kX$^4{(3j~#bU7zh_-A9AoYPe~g4 zeTOpkI3N84G3$m!O3d^8{LFj(`c2zRHzicF!&xAC>G->!L=yJKsIv=ug5|Z_HiC=d6_4|NZZO?O?Wwz+T|S z(uj#N0S%BIihB-M16CF7ulJlbm`AE;)1j%KxEf(((Mh|0_570cOFTXO;VhUhnXS(j z{{m|gYkOn_>&9=;3jZQok)q_Q%d1~z2_fhaJ|ts&{r1)PURGtP6*LKf7L3~r|72LJ zQnn16qP%}LgFsrGsXFXXkT*OM0#p&4X1~zTs%W_my^zIoadqi!yBGqD6)T58>wVYC zIWw#UTDG|`IU~X3P#(Vf`OKPuMg`HZLcx0Y2tu_e>z66Vj~_pAv6%_?9kvUGKC1x*5|J2f$xq&`YCT1?ieg2H7%J)B5M^+(x0Fpa4r zY{-DRyX_1~(>#(g+(;Z;{L?50W%rT7H_iaCgM%-Q4t&=gnmlV%)=;&&0|gC>Z+v3h zxRAO$*KtB;kx(Wp%8BMZbxSrH^n0vsW_8uOxV-T8_VyEZCifExHq4Fo&p-d9zP47- zBmhhl|0bqm(%J;2b>bdqA|xod*KT8l)gTb&Nw;J_z<%DDN5X&ZQ;eZVTZQ>^kPtZP z-dp57un#b>tU=5{pn^gPRRl5$$gsA8Z;=foN5I5YE**$qRm3_=;to;Yvr?i<}4a=_1Ofv!NMS6Trl5m%*)SHpYWOj?h%3~um1Al(u4~Hi5AWpaNh_M803cY z*HTRP`t|G7{#p&^;=y-5b_a^j$M;)A;)HbvKRfUA2CEwQ!_|4Y&H-*^)U__Yd?8n} z3fW=qtZ~@FRW=@HF^`mOybi<&aUt`0{LESBcL^1~i|3=6fLv=h&dmF1=1POpA2-R{ z8?}O_4;19Ty!2E3@K35m1_4b3jb=_*d-k9|6OI4I#zqHj-B2{RKC~T(Or2%p7(NGt z7y|+avpEmf4_40KPFSJ6p(PdJm*x=f?(JHMC2HhIPrIOk!>Q{knCG_J&Rk7&o#@@t!)v3a!q>0jX-7`hqg*6 z@LHGgy=b#s52?n|co8ztH7Uq`a>(dVHOMU+tgk!+8e`pUE#;EuY$BL&v8kJK8q0=r z&K_oQfpQ+>rUqk;qa>vfdTop*1t1L4WZ3`?9%iZEmc|eIv+z9B(YLO^@<1Spi2J=3 z0>_f+XQ{9*AS)x{Fpz$U7FI<1FZatqee&e76;76+t#eS7A}aR>57OT2#;~Ib2m7GD zaL_e$0A178r){T`9_s83&%k=r==**^nUz~UEugWuSWh%|jFQN6;TR^$H2WUDM!!|f zWNQA_3YrYykRj2l8Y(p?q@_aiy_lU+9wDwI_lg##SXCA#&ijv!zS%)6s?G!}Q|;?v z_C;h>Ay`;nzs6{)v&Iy%`txH>ZE9p4<}|Ta2o$avK_hnvE*>;j(qYAXG*K3do;24M z*&z3FFV_<^iZGIpkpi360@j%fCwrLZ#qHs?JO6^FA%_JG)=*Rt#f^6vw5myiR?yr- zsAw{(!6I0!%71>&TbN%kAu~E+!zjEbkDs{M#+g|J5MvyJ*63F>Uo5|{5X1W8c^FqO z;l}67N-V;yBx?eO`S{UeZ|76Kvtw8@EGE|Y>$fBGjh_c2mboYH4fh?E8^T!06;%lw z7?8ACS)@QI=;5{DXRa3pn8zIX`v`;#-UJc?!h6mhgk5Ru2aFzNkn`5d&DIMxqbG=+ z{;M#!--A8ZXQD`|`K*AeJghJx@JNcH_gq@EF`9b_v=WFcoIPe5)aIsXHWT9$opv!w zaPT#wansUyzh;R)8VsTWFfUq+tJ`(I_VD}-CVY8$QCQaiO{ILe{Lt?EET>F2 zECaImNc&{G_p#Fk!wf=NEYFd-!#!GOT(?%NcNM}$);ZRdCWz|!xHa+j=b>r3Sp*Gq zhtk1AmhXLv`JilZ&x)C-Eex?ux!!@F>AGUC8^jII9z{$9;?H(p^fouQjM-qG_v(Knw-5q}gMii(Hn$<(cP{?H-ylr1 zCy4ss%YnD`Xsf56nZNn%^!)*4Q4K3k$Q8i>CjhuWN53h8xWgbZ8-y=ed{|5U+>8?N z8ylMzD`G9F^C88&%7&W=-KTCn*gy7JnU!Jqb!FKKInkhMWNZ4q7v&3b>U=8^`6d=V}RUrpl)a_ntKTR z?;6dzwz_6Q0Y(c!_H$P$dyDs~7!2YK3mc^?qxDh=kH8?C35`GM+gHb07!H3&I6=rf zUvx`-^;|#A_m9W^LYrGf75tZjG?hMiY*AkAb1eA+%xwGtzTVTn;VVu zg5XslFYC1&WMB^JLgxK^C+iuf1chCuao~N=+}?35Y4^SJ9bPMF?j2y_SPT%*(!(Xw zq|;WZODnRju0bHNmqmbZQm)f?mZHni?NspOrq2(iM)+sVx)TmAQAGkDDf%1*|EtHMzmVfbLZr7zO(P zeRIu&lxtMxI*ZOLX;B_H%p71eDBMSbxN9T~NEUeeu6xQb0whkDK?VpQhLk}QSOsHa zW_n9O&1pyz5Or0G6MTbz=8o^jJ&xk#M z@u2l5u`Kfj*AyOUApsnQ5(bjQ4Ah{N`<`-&&7#xP;}}(V?73=7c<8%X8x$iR(9mX( zA*XnkVqUCY6H>0Z$8L%Q*Z(n?U4+7sH4YXB&b%68l8_VL!^sDev1`ZvL9?uJmbgq9 zl?WOZk{cLt{`*fmpDJ?~+*g>N_U_jEviBv{S4+b@dKo1(lVMQvbMvM#aZus_!-&&@ zVH)f|nzVwHxDPvH^M}wjdog{ko!4^hVFA17VqOTI@N=>d5Mr_#)YHlu znFj7iUdLP@qJSmUB%lXB5Y*yucd-2V96z%v`%=VFcsr+x`MNl?0=fe7&hqd{7) zEvE^8xc!jWx7}iJa5#V7{OlS7LBm{LSxziyAVL~fLckpQZk9llp-AiHd!p9PomY*u z3xm?AZkQ#)q}xm(1md{+d-&Qd71aq14r(+W#p{eIMi6GYxTWeWXkdbCYpYJf-TOt5 zhHHm7*IYjgALRink=LRXLnF8gW!+>^CfFk$eEz}cL8l`!77w%SRTrcdQS6CQgzs}_ z1MVBXt1LGJH)l`#s6lk>A?qiXdLHXls-S_W)`K<*Vsa1%2f8XjQ??7Q>Tb>)N)_C7 z)Aq15(_W)`zAbfektp~bzJIwyqE?l)y{JeNmBvT!x3Jb1g|xogV0?EN`)+NYJbBt_ zTO^PW!N>xE!;Ihwzn_B*W6pCHNuI!^sBYVuhRHq1{l&SC)&;36g65foEkw1o$qIE^ zkWlds{m;}jVL@R@sV)Dd4d*+ves^-W-6z4ry`luLURW#$eMV`s0-QzG4rNCkhP+U! zpfQuz-OMK!5ELLh#_jgfgo}Xk)HS0#Ec~M2pp1@`RmD;eJcKX* zvg%XHCUS_0Yv?A}xn8W!ECFe>qpi)Y*eRqCF8d5&bMMds3I=N)W#-C4s{-tkO-B9r zvFF~frz(7n#*<)V`(O6#Amctda_eFP-<(VGOQ_fl+T%XMjF+Dk3#p@^fp*b~H8FJ} zaEZd4y2VM<#|}j4jy24?T@&9YD+cifyD4Go%^o$2JYCmju)7q}BJ^FTJ zdrd5I$x?#7UkdVo=7L14@AbF3G~A3r8HY)0F}UZrX|oy!Ui4UNu2*ZXeNr=Aqjz@= zoKccc(u`|wI-sxbJPNE=6ABYFssLVbjF!MMT>0+y&KOAeU|E&+=?~Lx{Q|$yPMd6` zZV@v(Ha9k%duY^#nCY`2z~|5Zu%FR(CMOY!Hzqi~G28y?(!0L8{%vlAT6iFkV4~bN z8an|rgqOyrh8ZXKb$D=S?g!1bDGTZv9MzWP3Su8V?|#lSAWUuu4scv18*7X(mQ@$G zujck}>seJVEj8Yxcy4E>cP}*lhN41DATD5PgWdlA+h`!pij3QL0_R|u+ViufBu#Km z!gK+HXI-ioMi(CQ0gq8%KMH5dK9lzWg$Wvb^)TMAPFp$p?)z*t6f6oP>W%w5!bPqK z7&nYst+Mk3y}Y97dcMYU;ja6k&Ftno`?>O@@$z1nlQcA3axa{l2sM^*sd1VypRM`v zWfJu$E}59?VZG5h(-a+cgSuJA;Vveo5eCet+qAzj0R6EBxcP`hKqqn6tBfYqJ|Jx3 z_AzZ0Hl1-Pa?F-jMqiM^<#zN_98n6H)3=OB729m3!PJh`1tV?qt!tfCv+42u3-R4 z1gs}6W6nClSs8wH0&YB%O{|+bjoNNH3hNBOK|y2DjDs`>2j)xR8V9xre<@Z+oL@c9 z{z|>#Ac6);744%AgMc)ysW|O>(0jM6LbvDKPnP(CdZ;BWmMjuV5Wzabvc}SiR!?h^ zDzUbU3%~vbK_TTKt^14j(R1$b@SxJ3XGK8GJ6TWGNaH6omo=jqrAHIW zR?y^y!UT<~dvOiXq6G94I1n3g0>v%}9v9(4;IqjHiUw!Pg8ac;15Bz+H<7x<7BR(A zy_&E9(rWd-azU@HZ?*D|t7jVu0oE4x2eZ`7bX;aIOB5mmENjX-K*#e=Yfn>rM?7<8 zW_sW%?79M)z)FDj{5qQ-E^g~t?F;M0`tmR9#~R0tIW7Z!c3QDbUGEzT?RBO!UNk%U zSwUdE(ts)6Tv+r2s~onXe5AyV5Nf?@Ns5AcYLVrlq=7QQ!R0`!dOA;N{6Ja@jclLi z&!5@#kcdz>CM%byXc1qPnolk*cB~%N;}lIFoYBe2$u=P%M--mk%prwHFhl!x26q1uATNsfLa0~~To3Vx zDgv&!4PZWR-uzQZW|7)?Jakq~URA$9bx8!$(||*51(MG7RLAkgVX$^EpD+W`Xk+3J zT)Y0pYq=j3iogE)k9*xkr}1bsjssI8+ezB~eFSGwwJgd4QwlQYMT7I?l9WJK(oUWV zBfY=snp)me6fS7|c_&L|p@gB-M#V|gTAtSyC1}i@;TSzLK*|y_ni;Q3ZwOfFy;XsO zY%Ss49^{qLr~YI~KTm=Q>kj7HZ>)5kfkuw8E?|U@nsHL$e(GMU!qH4~Q)s0#H(R== zVc1c)py6Oe8!AeRMF?S8=x?MKhpXrFgkO}kZvA`8&{nty4#E7wycO=<-7#Zjsv~%+ zY(;}6h_Jy`Lw41m>){V@+OQeqy$(0`&1-Rskm`CFw0WtbEq|vm(t8R(EkpR)A)Aoj9JYm@$eVoCLvBlm#(ZfjvdJuyplce>D$%Sb%ko zB7@MS4oC3^7@gJ^C5tn_L*ofxYON)c4dHPCSjKAKBdbSEBIR;ZU7q^1g%b`qpX^-~ zvD;xs;ev+EWSlRXZJY0y(4Gr;D(i^{Oc|{XCUkT2GuKU4T!zmqqs5JeLqYiX?!ycO z#!IkQGfb+i{^q+8Fq`gRQCzy~G-?iD(zRD{DzOO;I~qCns?>C44eIlhNkpZ*b@Po> zi4O~4UcL{A=-a1!Fn8CoHV9H{?S!Ysr112@vEz<-@_-LS-Tw+QzP)6J0;zFnQUTaW;!)Ws z=Q4nnjr-!;(Kqka%a`7G(1Qy`uc1GAyDlVPG)YF(7Te;0a6iOrW`f!q1^?4P z^WEEekru;_#RYecmLD3e6Px6$L;n-bs}fghrsWSTM8}4elO*RS{>hvWfi7 z=G-4W$Tb5>ZljmZUDs_yz=FHJs>eD<5uO(_H#chqh(H-+gN(gaVnKA!S6NWom?Fkb zv}h3qZt=DCH4_%fcva6lMiAg;j`Ad)pKwZ;JS>!ekeE<)bJ@I5kBD^6fT@wfh!)YS zPD8CMB$V=mM)AhQX&|17pkr;_pvhRyP3P#PDsiMUSu0IqCXo92#EZG(< z@&@M_tA!dWT=XbaSTxYQnzDL9s&y2f11ldFH9!!pZ)|wOf}s*LY#>a9OsV<#xkSw! z$vQ#P%^Uupx6#<_G8nl!1k@E=qaRC}^zr})@I0u{E)yDEfEGy?3M@@xPc2&H3Du7w zOmHjz^UqteNYsUcxTN|3!lR-^>F^8O)F^f+z9`9ajxW_EJ9SVr-gU-?u`z7QOdv3P zvvee}JFEy-4h;4D{LBL9`$2Y=p!Di_r&*(0t1~TaMJZUt3OlQSDTg6*e`aRZX=H=7 zRZ<_giXh98A+zab3(3+(%!}`G6;} zufsGCr*Rey%o@hUA!E}JCd%x>4@&#{`MG)bx9dbRKop;W5h57!%ys9pXc~#ocb|hQ_jGf>$n$3$BFFNf?$_9rAD*%mc5Y3sGNL+V;vVdhv zrv|JV73wt+&;;}^feoFb#N6iwkPKD)=3*d44Dw6#{Z4`5d(+I%V!zlo_yT7;eBGM2 zGb}DMp;3YchKwdau^^oW!rB~yLOsUtLM0IxY$|?Km?@e&M5F0{c`!wof?PIS)Y<8o zckBcP))T~Jg^x}nBW3^-!L=m8!}rsRjO)TISDJB|U=3l)2=J_eW+G_wffZ3DSkI={ zFZd%SMms)!_3BCrn3sl!)fMWY2hGu?;^&RuG_#v!|HXMQC}RkZUMLGFVC=!X8xfMJQ^TC0gi_yBBM~JH9Y?)9a z!mMUIDb2jU?IATd{Oa->qZRdwOFCi!WrrC=&?7%f*{Ksyx^P43{4!qS!Q=yX9v@n- z#JDrbd0;I-+3jRd5bxY|9%N|3$^B8h%)jC5R%~?Gx(GBv(7<>hz~kfZ#;~Krh8u0n zX+oONsFA{HQfZaei`9UZ0TFSC2hl9h<}s=JeU9#j5w>KfC>8kR_@pC@_$)QJER#Vf zWwX~9?ru=$Yy|0j>TOmepam2tl&Un06@>ws#?`D5)6ivIgbim7Wl;sz8n58@LHyB~ zd88?&VVFBT$kZweHhSLiB-$`#6$%uKbKlXkF^ru9YV8PQGtu6*wl>t-V8|5qhYX2`_)=7eWlq6Ul{1rs_#afzNu zXp|(6GUFO6%PXB(I9WlwwhrqrU`aBv1rK}4m76hr>XM=}S5iMlb>^&(nULnpfr1zy z-mC#aWwhbb&a=xi=0683nnSM<26Ed$tRw)0ub=B!cA6AcLYZw;a7o~5!CjM7lV<&K z^|6NSaIfki<>~RG$F`r$hQdpLRfFP_rFUIkuy~}qrm#zl^MIA3!eJgLTR%>Jn9>vH zHZ1Gr!iqFh}UnK8robD^^rXi$;?P zKL;I``vGf*;zy+n>pg>ZNR}0qSLXm2FOTr#NddDJ;qvt96FXc8ndo8i=bm48hK!X3 z+R8L1q_+O8w5Vf`x{N_?$t5*($=sLW>Fz z4howKQ{LO(^DfTgQ;C%&SN4bj)|2l}TC-tp&($@%pZh$6qmDph#fnkK3MCidLHKg; z#bOR+2BUqK(};H!Q7ECcXaWkajf?rol~yFvMrne_Ae6sED^5MY*u?-Oh@E>2b!CPS5lB z=wU7}$g&C`{WSmE->hZm98UuDrt9E3p{*!yfQk2G`h!5J;ha?pu%&nes(U$3-c2qVkE!&2b@ zqaj2O2n38t%>m-?N#aRZGiY^#wuMOxlmPA}NxTZJ2!>Fl-@iq7KxgbBS(PwPnvD{O z7fqoQiFub{k)k+p&ca>;G=3Dvg~bJL+R;40_l1RdtCI`6(-p+VIxC`Jlk1{82WJ;A z%OLBatUqew;B!(KtLSK6Hfkeil$Q>{XH6Nz&nv$u!UZ9tzCl^F2bsNxDT}hW9|He; zKSCrb?67jUU$iz{&WnpnCK$PXuC-eVD?;XhBmSe3Q---7uCHISPwtzH6?EG>oQ4J#1wI?064vt7gwn;={b6B0POZElT1AhW9p+y|14@)D z(tTMIbzaT`s@Fqlu9pYrLCFXpDb1^yE}xZ047l#7ji74Ci4eBxhk{6eb3a# z3c(Yt+*UkCZEMN~_a`@EHy|YGPlZL(qTYa>bnc?`ZDTGRjafEikgBdhfq6q`lb9;Z zo1eQ04wV3)NHPU>PP(JuwJ^V6XFW}?GS(SuP8~po z6}3qQVB_+1wLpBt2=<-t7UIQ>k5JsJ>=gBZnB_)GhBG;zt9jiiyLFHa9oCsp%=ZzbLpq_K~E9CPPE%q@}7a z-LP3`YMP1=i5o9`>Sz{={OOaY-p^ZqyfEj1GnIE{8sGiq%^PFxuU@?}YaT71vnoZ5 z?`E_HbEo**>(_5`wX!&aieF*9bJn3ozz3_ewUX&Ho4}2a9wA!yX)xCn_ZzcXk_~yI`}(uk;MhZdss{M07ZBhQ5!)+Eaj%a z{Cy4;nl=QCmLfNuRY}JLJ2m4QjV}B*7!sNb<}%g@#D#E)OE}PcnK1Nz9vs?fXB~{% zL0{_Wo@0#`X{zanvZ~q?`IWKUc6axF)9N@VS(P`UCD99bd;NUz`s#bgXj<5ho`6s{NfnIm#QH8J!!AHV`u} z3hb|Sj1KO9)F~KTLtMJAEZu0qWg2?m{PTUX*bp!%!tkVFv}$VOIxC~&k46W;&@~Q# zm;aI~mf)o;yzZTAWjU=Lvk zO5bjJr(MR4a~8O7;*tAZrqZfPpJGo0Ex%*se00j&S%DU8SxO7aM3}+jDx4gh{3QVZ z`X>1RLF)@Zgp2?ByIUu;udg~i<>dbEMj!ALQntT+{boW2-Xqs|D`*M?1T|JtQ#>{w zA;LrMFBk|+3I-zo{OZbzJtsi~Gg31`e5wlM3$j)aNYg=}kmqh(aGV4HzE8p|jrPN} zJbvtJJ7o`dih@JH05=XwSkRym^LtfpJ=PkHn>FHJTp<0{$^yL3YQ?RZR50;61QJDe z#%LY$g=YAEVtOhNW}UPib!HqWJ6P@r50tB3L;qM$EPU?WVu|kFp1HNgvT1786#Ips zWslUt3Lr3S$f8pae>&cD8SLYO$qDb{hmZENVn^&hw7?qh8JrE)RIyq3Ml?2yaN&94 zIkFCwI5do?g`iQeHSgBcNuV|}Jk)_TOmGVPMKvq5W;9#5;gssgXGkz9urFVDR`?wb z#!D~Kj05+2^!=#AoYY*9!&Q}knLXpc+Hg7Nx|0RkOVA(~X_NEeD!*q{XxMCeP0 zh7Z*EQ5G@E3qf`OZE;;`6^t^VL42k_IGRPV>a~wBV5}b28|KJ^pFy4G+a!fXDw85j zJP1bbr+1&*{cxt)Yt{+6g4UxkTh=MLFOJAtxJD4YZc-cFKb;@D-G2a zDKM=Gt0)``P6*Arb%ZWpj;aM3bBu*}SK9r8At9)sQ|?zyTdAa{XIGf!{9yZ)y@sH> z+$nXdmrx=Hf<`XDesx;b@(K5_b1fk}Vb~j+8->-{6a^UlNswRz{Xi#D82FnMN-RCb zmOPkv;N67gL@9F=I_u_kRCfggH0y(~;d?GGuWaU&ea@&)ufsa>yf%V{4O&`y*l{&w z!7M_?X&+>96_M*-7A(We5iaI5T6Y~MRBFUC3<=E^t+py{3_m*8BZ~?_z(cc4xGvQ? z!R)a@$*{^Iyp>`rcPRhL&C<&nhS9*lSSzL6*9F`Y>!R_=o10rsi1zO7XUx+vTPx6Y z_v{PPE2kK3d6$6;u_;iyCl#(n?^{iJxkqSeeQmuH%SAx51_w?+^t0$SIhW6w4Ei$O zxXnEvVkVw%X-1^5GWj2)(vJ3gXf{3tYRfF2n~CRx>ylh2n0S;DX9IRIAbr! z0_L;JYFL>BtRIs%(bQp<)Db1IY802?eze}CNAr7;Eo-U*_lux_zrb&x6V{FyQ40%; zMpM>{&)h@hzwWSUVALyxW`oe9uJ(L@n8ydDOk@!^Ys8w3!j{rK@i zt}TC7R+g<{C0Vs*ptI7h@>IENHAbA3sKA%ca#HyL=oXJX0Lmd_gIwkl!x2_Vx! zM}a9@LkSwQnE3A&R(oefz^V`8p+Sdj3!0JuV3SuFk)m7c*Ts1sN#(oPhMBIG{mZy$>KDz+0Vmo|}tQfRZm`0w0 zhQV0~8upvL4re+hv5blU776#tb*L7EwfUSNjzwGnMGsf1X4$YOn!z-zZB@eXM*y7K zLjpwms%%u^Fe9OgLAhSDBrY`#wVFo*ltzelAI&|FLbVe|LF*R|28_CjhLc%OlkNw%8B;n{sHmZ8++P#Zo(F}J2~_ZF zsqdFp9apYgr+N3#laQ+$YA0ya=0pdN4T&RkG$s{E;kKCzGK+BI)S0N24Fz+-UBT~Y zggly<+E}$vWY1Yk{M%VIXH7tqph649!eoD-r#u3*Xtyw4K9_X$IVVh11J$3PvuZ~`hJA<&Y(13)z(&PDTEX=&p$~8j55&|647CJU(m33CkX5I9;a@(F^E=3-+ZNpcpV90b_6hoe zuEv6%jiBm8gW3ujL9Mr0xQmNRN2q=-IxYV&KeSU;WO~*nQ7ZpQe<(B zsnDw~pg4?cgD}y0U;<5H00+5(UM;v=8ZZpU{oQ0F*6wtpN1K zA1-EAPUIfAFKCSYDl*vxI)!OT;3;uoV`H-ud+7(YC7OIN*%9n8Jg%SAUfh5!@&i^l zd??Q9WIy3QxM)!z*wef_m?S}C1WhkkYT?O`PSBErq6+W_E+`hBX^{otR7HZbSS1pzOs~P zkxmN^4!_2`HU)}#J#g!SfT>$-D-P^~xyu|V=uHzp(Tw@%5W6tgUo53V$Hc#Vt!HCM z=#PSCQm~6mdSNYIzI^#hG6ZLc^+CWV{cdOscNuj_;aCmy#~$9dl;G} z2Mu)8So^X(8AB;(qCimCtPuBj;H3EjXVQ3`=p9 z1ZvUl#XP`uA}BZs(-{l=z}MG0=x=Cg>C(`oQ|-L83$w z#fmfc>eVZ|RwZcYIh30^_V8;Oplhvh()Hv$2pcRRtm85)_*!)aC<>i^RqoMd^ZStR zy$PW1WM+EC`}_6ZnS{Ko&~C5>hwJOH?zp;v_za0AgSdi|;Hz{0E;oYnXv>*J@1pA$tTI?ybUu4Uxazw$8uUdVTAzcvh8PYSbsekN zn*`B97NwCrz&gb}ic3~4n2JCvXj;@3e#-JA@5nJLv|n6jFupuLUn9Be-ccfq(UL}< zBP3XJ1cHE7f&gM10I`-N@H=5s==5c8uZ%V+5M41Rbmp9}ZMJTf8e?0%AG>fnd;96!M@^C97_=%sf*I5;~ZfFHfi`rsx ziuU}!R->yX;LYpTowV6BfXUzy4g@eW34!?eo)>rG}7LOJXzgRvp>u`ZKn z!0ZzlgW+ORO|D!GSvq+G%U5%D$S8!ShRI!rMME#Tyr3R@#NqNpD`;BO7QXiW>vJ%> z%d5*w8XVdm+AWOct+QmB*ijgUeQ|ziZlJQD-dU_&SRlZVVBokglLYp8ebmK6Eyt7- z21VVE(U3Rt%Z!*|Di+1^xW3NM?aSH_o5O{cCt5+%qP9T0nsqUhu&V>@nz6$aZ&Ik1 z)WnC8QE-Sw0CQ||TJ6#IBX4nGu{0MN%o>5f_eBK??o8Y*xTUf%Cj<}41*#9q8(bfq zq{*uK>py=vCVDh9BMa05EARLs`^`+GJT1SWXhG9rIH9yy7=wC`aj4L)`B!azdI8r7 znmA05pnt#NXc|dTXQ*{hnca{z=bp6;?+4N6lJ%{1Ws2(Y4>yAtR|ONVZ%o(7vI ztnkuVliXjPE?_KF&VdHl<$>0sX;E9iP|12?a$`6?fEJ99i8faiG+b_DbHgkjK8JdN zeuoCvT2Y#Hp0E~5jtOwRWdZ?$pk|GT4TQmdD3F}i4W^nM7+g_nukIeh1nc2AI+Ck( zPPY^?G1=!llQasoXfW#tDxs~Qf$=j81Ln|_bkY{p0%1ZEK(u9;P+a3o`QBlESS~O^ z=nWd9MgT1~iRRB|P}_m_Tlbn8n1PyCmI?M_c6Khdns-wLr~{qhzV`= z-vLW4TEv6+0PY$Eo#kQ?O^uIFSeI_JG5XYTI5;S2-3e;4iYAw2d~uZ+4Mt&<4ZqlO z*IA0sO`u0x(PR}8>?QFN>S|h^kPIkB;fBD2Lhds5)F#26otkmL`5P3u3mOh7x%FsT zRGY@NqsG!f*x>J_Q|=chaN4%vExJL|R?(KB9qY)A+bAerM0q(l_|lnmKgjps!XYjM z{nk~pmTT~!qSS1d(f0O7b4w!3^NM@0_H>O#QEghrb@QP^s7Og(v+T4~xkXyE=mibG!?m>^Of2pkJ71 z-bHIbt?~!?Y=jK&uTl^q2q@a55He}svwo24t;n8R!vu{j$%5sT<-UchoGIumkL@$5PUBDQKc9gGk%!+r z58!H1F{hel4gxGC7#vnji{5~SMn}i`c8T!gnBzt}v~MCIiwwaeiVEHX=xxyRm7Cgt z)Wen0?cz11GOjNVH-kH2ra`moyFvY&*qop4z|eew4eszk z3L<4uB9Qsqme#uiy1%;iYmjhbh7lg^KoSk>sdhpb14$JftX8b>L3wj7f<{858-byf zaj^>Wu_$5xvY+8_)@VV)#&Z2=pWF{?L1vKOVR+Qt@3oD8J`csM&@!wOv0InXKnU|b z&Fc^`NO)1Bz`x$k5R-*%kFBG()O2jGIjGUa}#y-vG)TS1=r7PlPpkOgkm!)NYti=ef%6^hzRDDRJ&EBHV<{; z6;a}HBGvK82|?UCb#FF_w!E>i?wHi6_s`q69VLguz+rMouC6h2 zJqMOyQ<`9My|}_@1gn-wH@L%{qJ4IHW<0V;p~+_}12fO>;j{37kg1l{ZJS#%fPA!3 zppkK4dI1fUjj77*F(}YzEijkTD&Tzt^3+pEfIfrQ4)nj(Bu*Hsi$( zyl59jTFAO&{dIp9_gh=D`)>056zCO85{;j;9?qtO?oWbF0IRDjHoz3M)i%>a-tTM_l5<9ZH^usMnnz|M8erWLQE2~ud;4=}26f3Qy$%>G){4#52{I*B^TF%b z6OLmu> z$jh?9#V#u`Z^#AB-t|+cqbPlpvr=t`EjM0SXJ~66PD6$Wn46*uJzmS};#&8p8vq-6 z`}5ZP^=K&&BU(^VV{fD2`_Pim`kP+hsWklc^$nvz)*po@?$B_UPzqprdHd0O0hcM) zb$NM_IOf+ha8Fp1DjJ5qNS&qd7~#%yoQZAgk;g;6t+vep2a zP;m5z+Yp)@bUcFQH5JuS!QA|Azm8I72$wEsh8Y^h1*4dpd|;Y$xK#|qd35yM4jHa8 z%ut9`=0C52p_3*l|E=bMbC@_>{LE`+=Vra*@5Q$w(V^xb@ZC+H6CyVU_XAk}IOZf~ zsNz_)?*DP63K7Ji9r{oJ*npj#PsUha6lf}b=G0Axpg0i3WdOv1;DErQerbU$ zwaF=8Xz_PU*fQI5Ak=VgP^r!|%O-=h=O7};3Fxx+-@bjdZUb6-gacNFrPw+F!#eYt za2N7^nltZbZ}|-R(CI&OwRpa}<@)B@s>T(g(|+>#spiN)YP>5?0plE&<;W-4@E%z3*i}Adp?ejjTfFM1f42*De-lAEeeAb z4?4jyJ~8fMEbA6`fe6rsN4$UQiqPg@dJ;e|sA$~aF4Ft7?7nbZ_wL@V1wonZUiVft z-M}8eh&ad`m^W|Um_nfR@^I&CtHp791H5*tapg#-=_mB_`0*q6-GiS8J7Z3nptx9d zV`(rF^w=v}8DWhzB{u1IwSuMu++5%I-I@ai#m_%O{fK0Os)9OhsFhN4K~>WSVa(hE zS{w|oi9n!W%}B_z2OOMm@HDOwr?g`_D(?lY`r|Z5_DKSZ)K{fYqd-UlOdCT$;9>DB z2QD@ZK5KfsOn|0{X{PZ!!Q^D=DI1M7M;NnzwDBndC?$OU(a|@z2M2+LJMQ)(AJ-;l z40k#-leQVFl%%3lzM*LqmbZeYW*CJ4!i?$ecZMT#IWE-}=AyqNjFPmOP%9e*ljhM3 zQdzCRNh-2!TC9e(AghZuHoexMTXFyb21ejFzFhGxgUmnvCe50)MhM};(IJEhV%@}b zH%|lBUW2HfJ$r7L^5zU|XaR{0;q)vfW)-Y?Jl=W64QtQ$?J7fNO?IX|g z_vqnMGf$Y$kBpf~f?C33A#JR0*nZY+5L#Mj^aX+<3F8tYkIwihI^|y@k=2(F$bG~$ zaZWXAo7Y`kT{`6;y_=JJU{4j;PpTM>IV)q3E*eRp??^M=xfL`e!GudF@Trv6eh146 z(>B(f@Alt0^8}E`M#o}ZJP`b&tw-ME^pv;rX{VD>r9*-M!Xn|buu9T)gXXow2vo$` z6yRIu-f8DQC{*R1RA^WtfnzvI_{ih?S^K=I$*YDxuj!k0ul}@?EC{ZS*WEG(i-6yY zaQqkHCZYH8p9X#P3xh)A!6J&rYIr>^g$K^x{oVzwRbYv*5)mR?n3VS;C=&bQkLSjK(PEt--5Q_un7XZ*0Z z3t)b6#9{4f9zXU*-2o%hDNMBLOpLF7Cx|z2KzTh$1`s_55RFKgZmM2ZdLpU19PSM# zY@R%QYMrymcuArq`#q+9nt(#+kyVFLcZT-fVRp75k0*>?;m=U^(% z`?9M7_e>+#y}jMauLZ_I)>_&i?|lJu9JbO?%s4j${Mox_j^3d&1U2V}EKOPQJALL9 z<$?()a9^{k_l7xiv%YvC%>O{ zf%QNRzpNw`tT8AJ0(kxU^>0MS@2cnEJ!tw|4|Xu=%6t|YC0(`;z8>00-fo18aMak? zFt`Vp1NQ|pcD4)a{gejm3)W^`2Fle9C`pf;g@PgkeX1{_T)=y~ejOJv zNV+MIWrej5&13b*5*ijjFWf)pG6_?s+jkkTerk^2q)Do2ThJ5?7%y=a1PRO>CXAM% zDsZeD1s5-aaR5A$#u6Yzxefd#kfWyY^<(bp<;C}-G2!n%bIh0&Q)`#=&>v@@_8xKX z@6{trDXU3FClrl3wQm{5hO&79I$l`t=c!Q9jN(Mgqtk$X)bSiwt&|wv!~Wuu)ZC$> zpedPH{beZ&csK5Xp=tfV^>Jp4Ea=4=-wM1iY2ux&peYbAaN5t1cDS>>)9Iassjuu6 z)we)M{4Sw^MyHB(ga81^KsLWj8>>h&Jfa{T!)w!O%(^BXXo>TVu&Eb8yM(AXz?#>R zHaiVr!*$Kf&bR<+b28LgR0FkXvi2s9nIlwSnqftOGs0O=)*ax?XdHH$DXeLs9)oJ+ zSq4bDnV5W#D21If$9at#Cp{>97xbDb%_>+<^?t7vG`T`Wk`N@bPLTTH4$TM_s~d2e zwm`O*=lgt>?yOn&3^-d@n0L>QcC3m?ckt-!%y)z}90VFJ1zZP7tOsTBaFBt77uhmA zulNua7zc`b;hNNKH>pOh+8=}s|2DO4O^fn?5V^d#G}=>EW|-EYRpDKb*EjW6ii3E@)`~ynSnCt`$}~p~6e(9&mN-r!s%~^f`96J)4b{q^uMG zF^oCl=vRTvdm&uL;X=F!7r7q*hnNG7P=K)1coM?mzBptYW+fdY(HuFHe1^tCw`kD| z&^|6;b+?Xdb%cO{j;RMzq5!mvJLl<>r}p;~cSS_n7%c?cQ=4);fPS`y8P z?K&xsj+wB~2{-G(wGzDqiMWv9#z&RHr@{|F13koS`C+YntEu3~s zlt+Yx2j`WwiF2*7rmUS3F<46}m4l*j7LB^%!raiZiAhu;Y_yqN3!4#kk7WVE1OKN+ zUgHDp_IWUC%{k%T`Ip9>Q9?A$5bMevSXn9}uzp&SdV1lH)#s}I2MYyP8lR8V#b@`L zzNwk-2<2ALwCIOulhZhGHaPpw|Mwo`T(P+vYC z>cxftdG_>~S%BU6CEpV*Mn%ZoJ3*uDD70({3lkA$AJ)3d0^h5aCWE%Yu%Tj3b3q`$ zlo^$Z<-i8>T$DM>gog+_lOJOYQ)yk{DW7!7otmDpfm?iTx8Z4gZnw%`zMuCY4A~FO zPbY92mmQ;6jbcJATI2!C(sWAR*5;P?;iKPNlgIAvXA6o$lX+){LEp>nY^G_EKM2ya zpoxR--_G(#y73|i8qRkfbFUiJf<`Ug)wob|Lhw752`nijz1qMJYNCq~L%cBXcTRZW zZ|1%zqU6wo8`x>|s~9&wBZQ-Z;Wjji0Pvb;PoLY^*^A4Io|z*eE>&V{K$wDtz(3Tx zR?9%sP}~Zb+i}hIAo!-VtZC64SZ9sfpQfk@C}%f8!a&{ymYmbu;7k+^Uoo6al-k@6 z;!$bVgL{Q%q-mbn4UIYZN-acd)}o=Ys0gyGie10S<%YS-^2|KpzATk72*^QPSR|Ob z_^e^#{7v}bVk~n`*Z~QtesgLN49w|3Kpq|(_KYJT#H2|fBoHtwPN zyt=yXT*shjjG%##C`JsY?;1h5_yB(^tGOP;3**hZHM`tce*|+V02T#AM(_^CPh3Pe zVV1qSZ?#tEcdo4$l%Cv=*!+ZxS(~IOAjm$uI26K!Jy>2@_FlbwWpO30jnCu&;!5Q2 z1Zs5{)d+D|26eTl7ocerKdf#sMHDqE7<} zL#%nY09p(NXa#pOZI6^JG7LcRL(xIlh^ZocW{5}Ttb{_xd`BKs8>)-y4g@H0;^AuEmI?6k{Sw@)p(nP7@T1XsH6Dh5@{P|G|6n`b}qaFif5M;?U?h zf%IZ^3_$bXcba7mLKx$Yz!4A#Zz~dYpUv-lwl7qk8L=vj8}}YD8m{rhi$Be}1SB$G ziE&@J+whsU7ES(8Mp05fIvG=F`TKkO-ss5aZ!X!ULYY?3E6N#wevcfZ=6>^3LiyZp z-;O#;UJ*dKr5+Xu&Bau*{rC2X8;Anltvt9M%u*3nOTc*pk<&5EG;d% z%~~=>&GQmMgP5UKrRyFr;XLCF%3%BW;iCoV<>Er1p{c>N;~H!#c7n#H>@*G(pO>W) zpZBf?t_HQ#M@S%0a2Jr}cYbmCTcb=h>(sqy(xFC2BMcGHm+s!~yRcC=-a##5rPpKi z!tkJZ%_HfR4TUm;mTFmpC&zYPI3LhY(_?6?Dff?JWC;N`zUzne(FlvYz~>^sG!`RI zaHi=hvbVRNTRDIl=B6E$0ZqZ1k``!pLoEf3#-eg~*i8QACcpHU%fMz+oOBs<6+J({ zu;rU3du za4S=i{mOE9wO7!N3Km&wtVhO6$GMmV>%8msL~hJ8w`UtpV6orBP~*h;=ZvD!w;$-dl%o%4@YV>ZeLeyafbc2xL zphL^75ABBN>y;Ml1^dN*a5hv+#XiDR@c?uaVklhfi3%cbgFbgKO!j6NI!klzmxH7o z_yFre#^mJyN@g7RZ1w`al2vI4gY(Efz!#F7wKSM^XOHvOt!}OtpsR}k*RD1Nn=nKC z#oWy2gp(fAKvO6OZK0i)PgX%$m@z~;2#9~sN?j8*CAdgq#&zhs5|;v(+?7qol#oG# zQhbMN@ry^-Z6?jmryV19Tr~KdSzGp643MdxS-?K? zekn}sF+vR2#@oiCed%_daBlN<$I%Nhi$%iy@V-G40%{{@U}isloSJJUZ)g)?aD!MQ z98$EGEZG}Tz;Iy@(f(vvpms43=+@RFi=~kH1#{O302tk*bFY~T%W2ni5@0m0e9Y=0 z$LG)``d!fRz%+07_cX!~;zVbgxh-7gHwdg^pGi7D3|WnT5&mlU zseR)6*%y^9s31Gd{Fp2l89+Ja+{*QvRRO7upyAMLJF^q>6Xt}blB9~g7hseeDEz4B z=YtmuD%OB?G>D*K9t`i#Gu;x#^XH#`nqa|g#(E++RA)g}8XB-H3^E6XAun91d!7ZEY)>>Z6B7t-*O=NCE1WOi(q9egaplx==iR^4Qzk z-Rmet2pQ|%?Cw*_oC%(>(NdVYP0>C1VGj)u(~K7h`?AjgXEW=)TZ(vSEJ+sBt}}o- z0;|P6Ipt})GowYra5-3c)w~FDGlct&r2*O(W`CZ-PvF{EHx=?FsfJdzpWlu8Lq~Y3 zz%`B41@oFGTP+Am^I41~@Y`Q_EpmV{DECMj&qNq)J9jEVU%lr{<925%&V{M|fmp^5 zN}ME_6wnp%zop<_u)b*!yXE2|WPcHt!pdQNphGcqTw26z%1*84dar}77@UW+vt{xE zBjdAvuf1=;ZY#BIL4!3zP)!Ug4XgpDi#-%EI@pR9cRzOuO4EV^etmsY*aS}QnM^h` zIQm&3bPyWqb)@(VLIGjSYc4J>?LFaZLRhC6-OA_tDW0D@pTi!iV0IYlGwJ}_%ki;P zi8KCa@J|-3#+qhdm2DOWKdx|Yv{OgG7secYtJMt3a7B#;O9*q=11_?@%O^atTv;xco3Ukay-E z1}G-1D~FP7nYcFOq&Q!u5b%5#%qEymO0T)r2Q%a-Yfovm)>eDf%sZh;IxcsA?@}y2Ti* z`J?k8Yuqu1CCbQbM$7Suy`~63qZCs%0nLxYsN%$KklU)v#*$p8np`G@Tx>}-Sq?QW zow$+O5^OC7hZGgo8TwC~1*G^3){t37eUb`L9G?I2++0<8SE}a$B|C7&5h!>t#0QjJ z$r)95iu;U#8e#@X0D~xtwH7pf-1ApS>KB!5S&$h&EWTHQe;fw2_Yh7`sRrIEY`(V+ zE*d7+M6eajsh=Pr&2gBZ^{tzWr74g?)sGok&pL0!Z=4$Rs!L1Pd){vlz4Q1olV{E_AeM%B)=2xeVn9F-vV>CjpIwrFrz`L6%|^_RD{zGg8CTzZ)8+91uD$zwEXkqFmnByl?$>nWiiE>Cdi zxZWyVLeMpCSGo#AzgWdKIkig+&x-Dx^5v(@`u)>aK6W#!jDDg*?7=uK5_Zp4)5$QrfzpD!{!YB}|$`it*IZmmh{Y z9^lVM5r`{VEDNe-`QYLVQk)PrWCF&~c(~|}#+QS(LgG8$zkk>1i$~pATBS_A1luv) z^#*(=+)Onoux3a&8+3nbEodNmQZdooq-E6|FfX(_w7~!SfB&xqDtX`@L1?0_`kPm8 z?VsyRS|_A^Q5Ilbp=ON4C9sOi%KqyH_1)6?;ckT)APBql!Bd(kjAPJ7hk*SedkdE@ z4_wk|+)rlJj1^O})*?SZFPiR}G?uC1LAaRQ2Ux8cmpY|q!pGXZ!lG?s_eUwc8*L||qf z93EKW0?bz(kYOV4-o5qC-Op+VQdhvpPEXHFyY2_;;dFQFG~4=;lLo+{1k}aqvhm}g zgAGtdpqIE7M>m0sQtwrkRaPL-WVKpT+wG63;w*AQ7ST?rMZQqN25!!7%88UQXad^W z*jTp&l|1I-w1TD}C<_PwcQ_`a`WrU0YzqK}mbAi`>y#^}A2YN1pwTh!_GdkCDrJ8tv?)# z5xCuFqQe}y9~gNZT_{*HFjF;ZtrJ*Zm^Fff^n7(3S3O9WKA;ygQWYSgX$}(S^G60; zS^`4aqF$h0XecBt>JMlInqj~7bM)_=dH5A{8SWQ1C|W^NFmNkSg}t)6Vw(r!P;nz0 ziZCjpJ&O$^Ti>wnN|UJsF+O~_WX$?1#utx}fpEF6ygOy*0VM!%eRZ7b$>D|$SYOr_ zAwztOwdT697Q%;p!O_wc%0i!_U}7yOIsJ-i#i=RprLHakt1Y76%(wmIOHr0kez)aBUp+sAX{Y_0Zeg*tCGGv`dZ09_=%l7r}9L zb!oI#glo$txLV(|NlR>L-Dk~$lwEj};x&Ud&g_NOqA3Bl!9+kzQw?Jya1L{^NtTXr zprt110G$OA5D?bU(RXus%+Agkvxw%@Lv%2*$B!Qw6WMWsMDt6yey)#3hU&)9Yi@lV zfYk`I#agJVF!U9GrC+*800|)Z)43OB$`A|dXMyNFSaa-EQG0ZGby@g#v?vxu3K*dk zG$jGI4dy9K9|q0uV%9K?=^1~;4yyJBWDip2Mo&ZTXg$N%tbwoaE~ES z5J<{uLP+8!TL`9SHmP~0S!T|?gFB$EqYAh#Ro6$?mcQXoM+mVu2%DsB``J5a4)O0gAt7%Z$yW07*!m=2&f~F*>@D%M2%~}VagNks$njwJ8-(ikw44M@j zAO=3o%uG8AWZSvRjys1)K6wcl37Ady-2|CoWNM-ZV_94Eg%6fY9y#@8LY5@1zq%F` zB23tO);h|pxn_jCAKzOu%PdW%BCj1ZJ1q>>6N@-ZKM2Pyr_B5a&4XzINMs=sQ*p8} zcW-TCLosLtO^JY@IiMU=%@7xEgBT94fia@tqP_BaQcZk3=*GLbxe>TxzS(HyxcCGN zLxK$kc5`!Wt>C|fg2%>GoU3NueI}LTO%pN#>DjD7-LId0kG7tXu*a&QD0XLiC-n@P zJhgN_3)gBmNde(9?r0S^Uz3VASyKct0dVMJjieDL$Xn!(R?w6Tv_FIl!X~b!1wkY? z4+odSfJ<%C9c%@*RS~X&IZS`ENSs!Qmd5)Tjume0&HI$b$b6A?cc^fAP#?3q>&K14 z+9;nxXJL(GsC%=_Zs7izUBvy=xv)^RRS3hV`%G5Nm;L$_oN_(k)NAM# zB?lHSofxmQ$?7np%{r?9XnnY+5F!wIbao9tL+~(vgaZOWdG1&(V`HOE%N-5=9kCH1 zHc`}lE{uxjtc}FD6G-DuATh?7t^n?{slh-KJq*%)qwcxR^ioDMJAqZ_AbL_DcI^SiYf;k{`62tPg>i>Rn9xGF&3Io2X>+;m;w$JL&C|NGznI(x*vy!i8l zWp`E4*#N-nj#2P;X5dlZHBcp zkB0jFy9vEv>&8n;Fjh^~GmClw+Cw3LR%N{){3oX-z0bSVFm4nWtQ4+`HB@2&*GET) za6J#tBghdpoVg|h*41_;E5u_=aSbVYondh$D{7h##J@*1N;w}|L8ELV7F~h%&+ZUN ztJemIP|)IGL`vmF2v8_j^kLz8h>@I}{OEK}Mc@>jB_#`HWqHLq29Pnt#Korg*V6(9 z$-2^pho9>xNTEwyQP6+{4HgBoSk+hq)(n>(=jg?Y7nYLGbI^$_>u{0<^o+7ZypYit z!`d+bF9Hey$v2!Mjc*W?O{x|>Ww{c$uGyJc@5*^e>NN9eC|zf4KGpWbBP1;PEjS0} zKmYlU_vXzTr;XfLrV@wC1e9|FlyP8WeW|J4>FEy_UpUIug#{t9J}@(WhOyPPJxkt@ zYX?^e2@Z9vtAPgo```Zt^I@*NmoHyA_tb}4SYezIXj3CDSWnJNG@DQT$hXi1=A zX4ZgzB}k;W@Q`~O8sjzUehv>xQfNCUDCkw~6oS4kNjyU(XcWjsOW*&pZwxdkgNCuc zLeRW^-8ndF7|CbgA1_M_hFDcdrfHoJD$EALQ9P-a@Z&SWL@)Ot_XGnYK#r!`q~gmw zz*=CrvW_HV)KS>LEVxeg2Bsd4+0+4IqN2-guc1aovA(7QAxoXv-2A*1Ka(hvBwd~} z2;(=`w0kdtiKK}l6gvdkt5>f~NqO@4ai=|09WXS41|h=%MPTqVafURD%qC&cAW*{D zJ!;Cx-wAw{$(ST4Ak^uZX}<#dfj_&%;x&#DT|42|aRrX>|41h}Km(36V?xDyO&N<_ z6j+}pPo7%V9&}QNX`5k)VX%KNB|0=T5eP|=OPZV>&l$v(&cm`b1GiyX4@0=~215E< zkkOfE7m#Pqo|+4u&wcgsrS+n#OQzyb)X}hsSRQB{2m^#eIG~yY5Dr>D8x}4)gCV1} zacCfrByPAo!S{1W`QGF2$L1r4;Lpy_EUPR`s9e_1P^SU8#rT)s)!yd5x7lD#sUMOX zB+D$8BEj{+lz8BJDpI)_WE6Biu%=4QFZSZ88;e0E)wsG$%v5CU}{?*qR!3(ZERah(AOK@9?bW#D&qP!MLC zyZ0eJ(O#)2w}A#o1~a}>BbO*ical-6fYpP2^%i|oe$9S`Kj7N9efzwrs*TY^A~ zt7%IDlo8^N0P9?z!B`SbeO)sD8ZT(%bI0EvJ}me|qXMHJ5K_t{;ebSoC^;+(iwnk} z+U9%dfo4k@H~*q>v0qikh%BIca%}s~tRNUd7S~9gfc{|4STjYh&x$)IKLe}12q=Bn z<`&gpzmtG-ZW*Y~wfgSeyMf~=#9DD7vKCTO`B_S0I6GF#VLw2pOG}GZ&%~NWPB~LdV@6 z)h*)-hK!p84HO0q1K<#s?cy`d)-fa2Y0&SMb`H~wvnEs-pC^dRL_0>~0)$C&u23}U z=n-HoP=>h9*RNmaw(oon3Kowz_^x@Zn|GZIHLfGttwiy(i_43~b?#`iMFT^CD57me zc%Eu%^47C}gRWUI98~b;ON(?sh+~0bHPAXo^KR+{_7}xMg_Bq}(N5b% z11633&v(Okh!>b}b`QVx(4^lnAV?VK)x!-7Lal{hOi|&Eb_|2<#st%XYebmf5r|G+ z#bUIxS<~4yRvNLoVv1%}oxW9z!=74fDClGXFCylQmCIiBqetAY&~j_h~(H)1LNkPQ+SAVL!h;+7Wk|ag^*az zv$Heq-&tp&FIbzTaA28csTYn$$v%Xs$I&~_;lD?~W7$NVAhz{Sj{{JjTJQ3HpW>N#|Ov1Jm>fM z@mpsU0XDFW&t!)xsG9&7BmsG}!mTYvTOSlgS5d$u!tR!6<`O@X6?J;%SDg1!s&pU3 z7oR;Nab~BZplQr_o?w6RGN6@5uM5VB8xNt!>*}C!bEau-OPu7e;(0VEu~@Bx#xBQ3 z>m_K4OQJ6rs(4qX8NZ_mV46eZMJ%%4+i9|P1%54x1(wX`&!0?*k*ld}NGRw5ebfB{ z>pj|MtK0kiZW)JGtjbwYBgmqH^WC&@&JR`=)*`iQRfByc)%dfsW~z>P$mKX_qfbVJ z)JdUhte^=47Xz6hFab>UqWr(UwlPub0a`7J0m25i4?$YoEOh`3>Ez_t8ighSOcafM zXJ^~8n`l#lE4Yey31yS!69z8Du7j`M4xUbvU@@pQ7=tau$EO9L`Tus7>&6P+(LeE zfyZtegSMvDG^&Y#XbAf8ex(TZa~=47gr0eKw?BWW*J*M80CU8GCW{ODsoK?o(nIzh z!tc%NH{Q?NWG-Xgx8vMbX`qZT++HYaFtb6KBIk)SNg*%UUS$?^qI@ExcXmE?Wy)AP zWN?w3Q8YBXa8Oq!f=0!kbwOh-8nm%s&qO`~0B5D4zDsM$R|J>xXI zC*JPfu4#MWMc0%{G*jFMQ&U(yo4*`Teujx)#b8=v^*{u}Hg&Q^HmGSSjH(WRrb)kk zxb)D1$wk5Xak=lpy@UIWcuyXQ5$0XkBdz${ilobMqgj(&=kb6K#GPyI5hWDst*jW9 z@u<7O6~u(GC)pHoP~|S`k=6G z(QOo_^Sl(#)s@x6%`OLlDF*`<&}z-< zHZix+odJMSHt|!d^D>8w`R2DSFnV=$-5K=7^VSC~$nF4|_rhnWubIVU#zuepp0ct} zS~LUD9Q7#BP95A7Rx9J;9Ta6MS)m)M%JS+5?m_NuveCGQJj)4S;%jT`-u2aWq7e*y zcUk2OR?O`95w5SST@!E#!pG|7=5M^95j38d2LgeEM!*sc522z7cS;H6_1%UJaahA< zry38oqXF&@q?Cwu6x-t=I>1Gxc_MXbe~(s7aVbo?SDTl*`S7Fg-A8xk24{WMXK+xU zjLy%`IssO{K6`+z}) zn>pGdUvN;N89IWKP~v9+{o6TL!bVLF)xw?E)oIQO0tTx9H!#-~7tAIDv+jx$u0XsJ z+y`!JYM2J?_(1~4&!4xsH4)7l&=zj|UX4<_0U>%@;B28mFqDD@AZVhUZP~3-}(aqq-`MnvM&7L4!#FeKNSOSKO&e48SGv&|XDnya(Y$hBd4r5O^3 zw+1>!5ZBRC9a=#HAVyqe-BM7+U=TPEF|S422{R1{o4?i3@7ZE-hz8Iesi{bMu%Rae z43k`Ow-uR=n-v-tthP&a9h9ZHDV#&jTU>l1{2F!kNF&^_X4q#wqv{UL{QzxYjUqgf zBqE?JkGqjAdR99t~gzceM)A3UyG-yAQ z_-*M z8No*yJA0e8e;t+#G@{YptOq~W5$be-)yAXgfG!_DdE#hp#Io#qSvSz?E4SB|!I<_k z0UyDlL=X@k{+gh^Pe4GiXJ5WBRWrVb6K@Ogt0KGB=Yd)X8XIEfm>8NyQFCMnt|9qh zY*;SVP9wB&Vyxx|W>$y$8A~Rq#?(}(Bd?7JelTgncW7IYr12KR!74Qfod{dDfXS zW7G%^$((nfPDPyv8HA0d`=J5c{Jb$y%lEUXnx9^`77lqpZZ6H5htW?@PZ_h6rJ*_z zJ%>5YdMGHK7l?&%EieGwPJDKmbAE86z?iZM8GmH>LdP0{R&bs1TwMi?vgtT)b(wBC z2%t}_1lC)Detr+1TNE1NXwlefnSi#l(iwu0iKH7F>t@}shujOF$2_4k$HNW}4{EOr zS8CC~)T!V{0I+yuT42=_-7VE;vQzdit{)bFjk~Qi7Xc!x z1R}n|S_v!^UW;}}dyel%-;EKMk)4CJ$=;)JmLc5bHXEd2B91P% z7j%JeXT3js{NSCR_1ExKPgLhVf6y-04d)ug2-@XdY0_yU5q5SOf37*h>>acR4I?;- zIW^(FDz|7Lko~=Vvu1c?>A#c~`mSMvhP*+D=9+SXg^{kb;X$aN?I|@htO4OBQ?Qf+ z`tI$!Qi)ee276zIpiy!{)_e5ZnE2_Z;m=@gS${siu1uMI$6B`;OHZGf2ce&)4+B?w zo4NwJA)X5Dia~gC2$$Tb8Qs zvY~{4=g*#dAGdoa$^1C^;XVK3xzSXTWSV(89Xs#9b;+)0r9i`wX~Dy}z`DG-$xq0j zpkP_ZBjRU;UHIMscfUosu)>xoVSu{y*Gr1ZEgG~WG(icOJmzH&Gt{#U!9ooP1P_Dz ze*E2-2L#_wdsBP?HxGvcA8^$fD|E*$OVC(1Zns9PV_LtxUyre6?%h~na#!Dmm0`f7 zuyDCneE%@yZdU%;`I#w%G^2d~{$1bCo|>BUHa6XB-@VUW;D!Zxl+p4xzv!A_hGVg= zSkXST#yusq2$YGc1Q6DW&mJ8e?cBqNE_tIG>m<|N317p!<SN zjk?7TxZnT$=Rc;gm09PY%rRUj(tyJZNaG=Jjj$iz+&-Y$_lkMxKANa`LT|)2P(<=* z&Is)x(2I1-vWKj7KjD%Fa>3E0a%^ne+@xvi>G%xRle0HD`M|PS5f<_wgx6LzW_%~_ zLCAzXJ;RX5Ftpv9n+CbHC>Es=;@R1$xiU^q|LtfC(PGIE%Ss|-Alh}OS@CXMBz2uZ z0yFYQ5tl0fVSxC&Dd@v3dG2BntoLm&P1P?dowTgI56+6|$830yMhP?#C?Z&E2pe2M zSeSz_IgLs$5=!@liQ}pogvNCSRmsoi9UNrKSku^R?w`-%+^P+b>T39R3?UN?R8(&| z*1gV6P_v-5XetKU3`B?@7D7jpM2m?sT|W@H;e<^804q!aKQ@dKqUH1IoS}kM!SzM6 zxXOZD6$oa81;4`xl)abKI@#FV@E%M&FlOCkum;bc|6yZ=skN!Ib*7FCN-4K}a|*!^ z$Eevqje3Y<+SXdhofTZsoG*k7uf@VG!je#J8|RaMd1y@h&!F?huy!G71x-az_KWY! z^*RkbV z8bTs~*_$%EHY5cM`_DiBaCGydqp+#SnCpJVVf`@wfOqV{J;IoWK3>MYTP>AfM!QaiZQ39eP~cHT5W%t z&YYDhJ`6$zpzz>U#EnGD8k8ce}Xz$!?;jQi?hl3x5I$8lnuS(qS;T`g6m<)uaBXgGnKc7V^-C_neI^=g zN;{V(q7^g+LzTn?oW~I`2M37+jNrQm{n!U`+MHmX|qylIpo}1nQ6^= zgG2^`=lJ#OK2U2Apd4XY^I)&2Cn>TLJ_^;6;RztFLZ7;BRy(i7Yk-wWrr_5;nU$Fae(;TtIOtfnnqJr z2jr%?EwsM~8LSwz>tTkq2@2*1SvI3naH#Q60KQ0pgF=N14`Fa|d6{Ssp%OWkT-=8D za*c7_1b~Ft-29x~TaxY?Ez+U2XbOO)deWX8>jgh@TspF9X4HD6+bnbn-#FxG&AbQU zMc|X{rY5^;@TYU{P3vz5V`BZc9OF9(Iy2m-0J7Y`C{Q-W`uxKCdi2#h{(hWm3`gBC z9=dbHcVPLZt+SGASuJ94L5JH8$(li!p)7DuoJqe&S~Jts>xfp+Sr{|I zebewvxj~_{a4d#}g?UTtLJ+7hF*;nZ7nJU+RxX^QX{#qYvokShl7-(ucFkSuk|}?< zIZ^(swM-Br)r;}Yc_nqCuC7qsck1A+tg(3$rfm@~)J3EFq_1;hB|)5nAlzeW|wQK29=+-%U@ zn!^_!iLA6up3{TFj%KLFesu&ck&_=MwH7k0E1EUes*x5jz}BVmJ5Y3>O~r9w=Csde zn4lFXj*WU=#H$9=M=9d(FnwHOvVw{d;_AINo6-&(`O;gsK5WQZ$x>HsR3;r5mRe za!{(6kDdfD7Tji}lh(EFgtdnO!F1S@A1S*1w(zZ$r_SB`Ey%{}#jJU)(*ETVE(;nI zh@yzKN-T(ThPxW;3X7)Q_rcK$nhY>kmeX?A+~I%$A!L&HauH0V^OF0I5P;z#WE7i0 zTekj3?!d_f!E5^kn|VD41r3cu{k7(99bIc(M;`mdfR9YeMPRpn`fO1B4VHC3VT0uW zy+a$;UHtoz_w%N_bys2z${Obq%PLJ7LW|yL1x-poglM{o#iVsjp}kp0SEn)2lZilh zxHEv&3RtY>L1$h}FDN#Iu)(!f_sNc2C&i5j&}h;_9e@C2J%_dUvTP^?T(vj-6d=wA zE-TGTD$}~DGnC-dRO_l0G%0|Fh7f?6BV_U%)D<4p>Ze41>8nkldCR|y7T5&sOV`ZU z=(=4mOc5rBo3fqsnjcV_9tSmF#3qwWfR%M43;n*vN&GP%tCrQry0CVf_ol5$D`*k` zApqf%b%a*dEu$9CH(X1G5XPMyFZ#?m?t23)ns z7XH8Q`p=fO1x%=bU?Ih7Q(EQxwfnuf=ar1^Dxz^-G>XcB{K_u^7^vAsFgr zlDNJkWSAq4$$Z{0Hv$ZTP6%~u*Mc>p5xNM&7R7oKYH9rBbWcXEiQ?+0?U?lMtLHYpPzfV(li zWhpQr^z5t|u^F_%+1XoG`A__f1I>M6dGW>1e8J4nEZIX1%?c<72o~r$?s6U%$HF{& ztud}MTyR)5oGX-qaa0Ughqd*!Xn$}2ib4uMKyjOyxo$M9_)U`$vd%mxW`aAbtU2j6 zh{=dzI{NtGW8WHxnon}Diqs)A5yZJT2%{Ws#yTQ2uqNjh=H0&B?DXRzo9En{D=)VN z@3Fb^?u#;pi;GL6i-Tn3FU&0@4wQqr*}3S!!v~#)u|)(9`$PdW3IUcf=VM&j1IhaV z4Z@FnK?*(M+GnklFpz#t(+~tr^ega&W~B^|GOmevySBO(ecy|_#SWr%!~xKo)@ZYh zObv^HwWSL{nRQ1+LDnGd83YUy<7x7Upx|C$O0rV&x?t-I!NaxCCUI#Xv^dz*G@zA_ z>X56q-OZaf+`a4-V$e-RTr;v}mI{I#!O?A9IUDQ)E>E%<*`K!8K&gQ%r3?FxPI;bQ ze1jS#_T6t^)ig}Jpni3X@{q&*^#G zj1&P-_Sj(r%5l;{2yOEA?IoLc!gH48cI}0je(xHy#dT!u_8r4%d+Qtohq`1#L#r#R zW{qkOVaWIIKXAh2%M}HT{Wx|OD;6GKeruksAdZ2x~@;4YRjKEr^yMqFQ@`))8UXRv{ zuxcaJYUaI*%hcsJw;4C?u)>hu*3>5pfuq?edS>?dQ*DKx*P@y0Gxp3>-M4R`cQ#l% zEE$9*XA0#3p@W4J$Iq^13RIXaX9filmlCepD|oOqt%p08Il1hbH^ZW30FURAdGtf+6u52!unRb~b6K^#lj0 zZS7EE0e;sQ1JC>N<;$pQ@R@ubt}?X2Q5IdI9B4!}46kZoCkvW2fXTL@vFaeuI1Toh zwcT^H_0hSC@}8A_`18-_(Tf)^YqKy10m_k@j;346s4M{f0xj{ZHneSV(O$>j)y`=Y z@y~uRk%H##=3Q%2_rfv3VSMVEH_ic2Cub3g+Qe+*V|Kq!3p1D^1cL^Xwra4lDCjIJ z`%ML?5M@#AP7^%)(FWC-83E2*U)Hw{a~`2BrJ)H-MQ%^lyzGt=S-?t$v9iD8o_?*i zO`ILh$+3Ipy+ncIEGvG7rav-&S(<2Nqmh0Eo9y?zIRT$&SSJ_DQIU1lk`DQeLxxWT9lZ8JBNm_GWpq-?={z}oWMFgUDFf3LVV6pfYD z6?boMTOXir8dzV}pL2+kp=q=Ho_)YVfO$8O5i2%~rvq!mR}i>=p6B7i2XWTf$4|8< zm{fs@iqXi|3j-fi$f$*kKa|X8FfbQBrx#Pl{LdR%3nB8iVT6pv5Th}1AYp_>T!g$2 z^Be|RM96Tj2u7Zp_2BTFp4QU=fPLVz5!Q7J8TOcUIzKXnB38M@U-^9!#gs%hZdGkFw?A7$64TO|{rz^X22T3r<39Kq=FiVXdW@-`8Ul)E>n4nO zt=5B|VfH*f2PMl&D;se&`KI}<>vosHEYL`a$6@ucR}|H<*8XT})*E51U@tTiCN@@s zVHLBg!5NX&PuvyZ=-;ndId`2WWIlEQk6UYdCm|fuFb$>8BtO^(&?}O)9#CMJZP8!ce+AO((C zB4;Z-)*6M7zuj~$=Ph@Z@IH)NtZnQq8o!}q(RhOoO$WSwea0ASUd%-x;kfOc1987U_-jNZ~c_leiG~0OM08RxiRuaI88;h!Eu- z^E5=uuygDA(?$1K516g8ilVFo! z-2K2ebzvs2UUjdFfT_dmy2T2}PScEuUJM!LgbNN~0`t^!00L{RIqIOH&Kl!84c zTu$6!#(ju_z_a7xD)RtyEi#2zH|XQ%<iJ*{QzX)KYR_#1siZH?>oed?xu+;_jrSGe0278YYo+x?De^*Xbo#00Z`50OLc z_>0p%W8TASB&cDmA3l7v!{ak+4iamHbx~LQ3+~&u1`#~dPHW6cc0k~BZ^W(G*FjT^ zm3V-n0Mk%Qoqhw*nsb8!5CqNm0EP5>MnASJ*Er`2zN8V&lals6mV!pXHTg-4Aa3Un zrzRtu(oD4oaADydfr!vrVc58siVStC0(>TJFxG=-fl=bVTU%RojPA5k5b8fC>tNF` z-Rlt6tVNbp#A|RDu)cM5jphD%9)yR$ebOB}&tlbZChuYk7j+PjL$KK*TwAMNq zEkC5(aG%6tzV3XzvbOTwBWT*z@nIOK`?ZoKpd|$ExCFd;iXzVfPr$-f47Taj{AFS+ z1r4Swj1V_Zk(6GvT^I;I4_nC%Gi!>LLgpEa7}C`>ovtOdMegLx2$= zM6xpDu+VTvaZnJXb%D>N!q~Smd8S_L9b0IFC0N>LIzg?j4}3?Mq`~?fWUM7S~WG!_4k-HfS&&O z=by0+`sZQn1P$7xGOc=95%QJt1w+7xK28LHHcx;aW~>AP&0z7tI#|;}7jGdbiDgFY zh58$RT&#*!H#XMYbuKzH-0OnX6(zASf#*?@L>9Pb4)>rPuw^N69Sxewa(nR^bIy|e z`|rPPou|l3bBrY|Xkf4!s5VXooJz6Ba^V@}B7FS#iSrKZb>=)ld%Q+dSD`bMJMAg! z!de2gm1#QQ8$1u?5B$t?YCMHccl^vbQ$hA5VXOoV2UuBRy#nqqC){hiXP8a_O-HwS z7{GyG%`Ps^tv8v>d9-p>wadMR<)=0uZ{BRxepvYI^s}H5b9nQV1T<>)+PBP%nK`C7 zb?x^Kz%WbR!|!2otOslH#4(X58IYEhH7170wRz2gTh}a_QhIukQ4wu{EZiG6u16P6 z0l@;aQOmcNotyKc?TxS8TqemJSOO$+uzx5wal^=k2vbnQ=-3CsnMybUwG+03a0`@&zG4@RL}r9mf;mN@O(z^}!*B zLGXKXA0-+q*>kVmp&Er?R>xW&Frsy5dBNyDc-?%>6*xN=-GBJNo=?B$S%@uZo{TSO zU=X+&Z&9P;o>6N8qf>^N3U$@ok?SFd_&uM`wUsTnofC)2YdzFPu+C5DH133kQg~sc zC@+Uc-Mf#gzUcY$=N21<-v0aVSL{N=>z+S*9=&?q?BRbpFKWF6J+cnIi&XvCni!i= z<6V?Vl!UU^x)MQyWVb$0PD`G;7SC9ynlZ77q~MYrl_%J4-lS#9rZHJGkV!}@OhZGI z5VWjQuMx{)$hZd3JU_XZ%&1~e+$a02G-JSb8ftQHFnR6`28K}Niy8&rn&BEMj7u|n zo>c|6X?jifq=LDkx${|SF9Va=b8aHC;PM8o-MD_kK4)B%n4GMUSgQ6%qoG6CS;9>7#}R;Ut@yIJ<4&uBns~AfX#AvB7IkOB z)Ug!gD<7rH2LZu-a46F5QCT5%xb&NQhL!vsPOWYkwS)f_fyDi9Xf=3FA^`{KC$dI_kEmPw>D1LtxuXh zdesO)SM&1=PRRVzbKg)HS!=&noeCt0CD6xkk}B-0M3pmrT3ts25#mCD35dBul&{~s z?zGCEBxo!Ex%}!7-EVf2CTzlpiRCmgJ1XwI_Q5O!GJ6mN)_(u=S34aE7M^2vZq~BB zREMLs~|9@$#z4?xN96Geakew&L73gd!R=w9+)w%vR>JJ zZ3NGj^-L1lc|b@U9v<{u58P!~F3@4Rl@IRl)ue8vt`X3rAn>rrD?C&!O;($o zxL#w<#Y}rAmO#@e5m|J|-hOk!hJ6TVK3XYg5HRt)_HzqhL5#0T&iif)G(H;bSt-kd z2wE5b)(~xIa8tC!#e?u5Q9%|A-+|m;Wmsf#l}T{PWhUXHS@rR2`P|D&{M=0g&z_~w zVCCUmz?w`4?AfE~>6t?AT-Lc;3O;K*P8}A;8LbpFn9Cw!Qbosc^l1^OFczUOY&Tdk z^wyoAp(4K5DC#Cc>7#Xj<|Jk^3VA8a2nPg8R)PeIk%sp5Lazi1ctD8Hv;(aZG!}76?k z?Em5{m@=~J)GdGmN`~5`wJZ-P2U*>;p{eDJ{%2)EQ*#dv56tDu*=S1-v`Iy41dZDJ z(|Uap+WK=~SDj`^t-$`lzRij$Lm;S4J{AlGgp)WTA)p*+T!Ihi7LRrv9tS?3>^0J$ zgP>_PS|e!i%RLNRdi4+0lKq=qZ(@n3|De)Stj!E)F z1YAWc1dXcI@yU*JY)17#xCp;m3!R-(EDqJkP;Y~Pk?SEKU>J~EOp0+DX?L0?Pgdbh zLwOpPZrJ0DRtOr1DXSg%xIk_E?>Zqv<`;8L%E|!a+E_8HA(>@OXfUdoGFWa1*Gm_7 zJNmJoK+(coj568lwVYkzwX3UZ(VI8T>MS0R zjm8TaESVzH`icMo3q#fXdY-SWz`Ug{-+vI@ycIL+K`kUSOav9{bQ>4NovES5 zaU--ysg~82eqMwM?iKbKS0A6v{RjL4dy9euqn&mxBi4(;exNo12Sk z6R>1hBN#txILM)9A9=q<+jawcg9Y~F$se{(p<6+`elTT%>Ke3mdU0yW9WdNc1j9K$ z>?!xAjL=^0PJgWW<>iIZ6?^R~kyt8v%?z(&MEB_r6R6OR#vw!NJb!$xAZVJ7MhhAU z7{)V*d5q?SW(?uN)Nzf8Ag%Svy}L%R2(hZ6X_Z(BXk%CgWyT%+9uo49`j49~zc$Sg)N3^?2{A>0wTPRj0N&DwC^d<_HbW4Bll_nl5Oz^X~7 z$<_&9Sy3AAz5Dl~Z@b@W$%FX@25ocRSTB@(*D- zOnC`K$L&C41r6F9y^M+;=ydTQX4f2u%9sc&7}5AGiX>F6rZvcD*)|tu9P4YIt{c3jTAHx?7{*`0L8M7Ai#%C zh~PfT>fgoRVGwA?FDsheE&(7dQ^EMM4x6&oa5KT+ur%5*@D1&=*ZdI#iJ;5C!fbWwyP`N#7mj`m6_++(Zl)qMMuDlV=*sl$ob}3;U}n87s&)yjSDCHZqGgcfm49mCS)5a zXs`^5^hILvZ63vp6C$sEuMlSAo;-dM9UdK;g;BOPz(G)fA=+EnaT29^LU?G42-3E- zXn_%9p};(ftPL7E!EZGB1!FMvKd%xKP*Ax4{rv+AbgRHrNdf9^Pz8vAKv#Leb?ya0 zIBKvS2pAMCgs+N^QMgzi6r`umo<`?q7fxs&#HrbC&7_@6sGz;8P6)ccdGm&Q&86{y zH*emKEEZJX)dD3G1q-Dt-tG7`){T}%){ckkZota+JTM5+dYvr)UpqW}5lpC%Z( zxdk9lm^_>F%4tw!!1#Z&;H_g6WUb*dE>i=rYUVo|=H6p$!S2pZXKD9#4!-5BcHGpZslzg{Z-B2CYoTc){og}y)_JkS^?z5^I7P(0|2U)bs zy*zUa{O#L!#)O*y)`*x5dyZnn{&DXN*pq-pIg?8$4QTx2$4PW_7z=g3bHum;0RpYF zHbwXpG>PH{e?SRQ;d#^1I6=cALacxP{ddpTAxP5GAt7SM6E7r%o@LId;BdoW6sX0D zDYN;dic2|+a*LS>QPk z25%~wSr0=Y7W6p@tY<+hif6c)YvsmuP9tMZ{D)Tky(tGwMFnji=%w5jq`}3<^3l7L^DV67}Q!;Aun`nYvrDo-evDn-MgU)+>WeUmLmpQ0_MFFP{b?CP=W%g2eb}-6JN2!Cr3{xYJO}Pcc^$-w?rBx z2H=8ga&N#!2^!377Ao&hsizkV>zVUk^I8PVUWqyNs>sGlV)KVhCFY)CS`ZY%i)$gY z&|ce`riq|@_~2pm{=@rHissVY2hikkE8wCW)tn$@P+1kaqq+CqJu0<$5J3YCEIV3A z7Zr0YDO$cjR(fkU93*Mh>`R$3$$)AAk72Lb^UXV(oel?lL30Qj6?|LfspCha)aUUb z^@G&N5$LLEF|S4m8V+H%+L`mDAQPbZ`6+ZYYT#xiIi{syhh-LuTk^R-|C zg=<1kO(cSTz#+j@f8+LHf4>^0#@@2m(8DNFIMw7+8t1KBSH~4YcUd)Cy?|xH+Tq?! z$1Gqd=?)4A7~WGvA%VVNPC?KN4o&z}gV(AQK@Wi=NBYHQ_ukS0c?V)yPsaP zMse}#+NzBR;hI>>O|otqBWN@PsapV862DBqRB&b3OmoLL30C(bU=;LJ3hbgIqA9e} z#Ap8HOSv5aMh#pv>_I<5Mpj+X7y<_b#>s&kg`i=-u1#Hwo@i9~ z+3qOzRW*0%q^G!0^=2nSk3!Y?`5QgEW6-XPFX-%l|NC=fJp%L47(t_=q`HEKX2Ce( zLNE|3dF5OgXc@h#v1euoT*p~d@fV|PV^<0rYrV&k+4tk-FNg872L0ByAjElZKf!9& zlDNevVA*jmUUq!mSuP6>zFH2!6qNml;K6Evmei7k>)?(>Gku$*Y4UosSJn!FqZr}G zkHu=h5ai>e(A0rno!sX-bC=W=0!GdLl41DC%Wf?Ladu|58w{GKCQwaFcXqy-vQZTA z)ZnSc@t#Jx&Nw10wA!+xUHA)ThbT$Nwx#bO<+Bc*-;|Qi(2NgWtE?!PY3#PR?!M1- z(oi)WFg9;V+5BNMIM6uL_d0G+rW9IUKQSwfl^!G`YLv4^(rzrJ>H^jP<^&B9wDlQ0 ze?wa(S%5eXGyy&BCjsBQwbRtswbeCe5j(}jF&p)>>jfKe>}Yz@XZQ32RpaxWTD-J^ zs~8-#8&BcAD8;(=Yli??wwD)is~LD7pXaA4^SxI#8P9-~1Rbn8I)MI~mhIOVL1T>~ z-2$r?M&_#!r~XA(Du2?OW-rit2(HCH=nAG8kVZP~HiQX(WUHB`8AYAz@cYZlpUL~K zbr_BUu1eYDekVzLv{Y|L%pdy91XG4xzZxM-ebmVk(rmFbcn*K}0~h-VGm%e6-H zk|~JcJbLuFlsGaOVoSG|I-fPDVBq~&G^!uO^@{S*#A4^h4n7NL7F2Mlw99Tl+viZ? ze#C{;??HlzqxIEYe8zpNSTw%ppU!&ObWCaIO4>b4N3{-BF?oa>0_EGcc%-&0oOt5r z4~q}Bt&?{XKrG{?Ti2{KaG3TZr(k?>`udj97s)Hg{IHo%1 zgfoK&p^2G?LC^#g0)pnqX2ksN(voJ65W?NGv_w5e9Q+=n) =*b&nGAWm3VitcQz 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 literal 0 HcmV?d00001 diff --git a/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json b/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json new file mode 100644 index 0000000000..9c698680c9 --- /dev/null +++ b/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "ChatBG.png", + "idiom" : "universal", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleStyle.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleStyle.swift index 3d918ee4b7..c4b49dd5ee 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleStyle.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleStyle.swift @@ -1,9 +1,8 @@ import SwiftUI -/// Resolved colors for a message-bubble surface. Incoming bubbles are a fixed light (white) -/// surface with dark content; outgoing are the accent surface with white content. Colors are -/// FIXED (non-adaptive) on purpose: watchOS runs this app in permanent dark mode, so `.primary` -/// would resolve to white and vanish on the white incoming surface. +/// Resolved colors for a message-bubble surface. Both directions use a dark fixed surface +/// with white content. Colors are FIXED (non-adaptive) on purpose: watchOS runs this app in +/// permanent dark mode. struct BubbleStyle: Equatable { let fill: Color let content: Color @@ -13,15 +12,15 @@ struct BubbleStyle: Equatable { let playIcon: Color static let incoming = BubbleStyle( - fill: .white, - content: .black, - secondary: Color.black.opacity(0.5), + fill: Color(red: 40 / 255, green: 40 / 255, blue: 40 / 255), + content: .white, + secondary: Color.white.opacity(0.7), replyBar: .accentColor, playFill: .accentColor, playIcon: .white ) static let outgoing = BubbleStyle( - fill: .accentColor, + fill: Color(red: 19 / 255, green: 44 / 255, blue: 73 / 255), content: .white, secondary: Color.white.opacity(0.7), replyBar: Color.white.opacity(0.7), diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/ComposeSheet.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/ComposeSheet.swift deleted file mode 100644 index 03fe0b6e5d..0000000000 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/ComposeSheet.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -struct ComposeSheet: View { - let initialText: String - let onSend: (String) async -> Bool - let onDismissWithDraft: (String) async -> Void - - @State private var text: String = "" - @State private var sending: Bool = false - @FocusState private var focused: Bool - @Environment(\.dismiss) private var dismiss - - var body: some View { - ScrollView { - VStack(spacing: 8) { - TextField("Reply…", text: $text, axis: .vertical) - .focused($focused) - .accessibilityIdentifier("composeField") - Button(action: send) { - if sending { - ProgressView() - } else { - Label("Send", systemImage: "paperplane.fill") - } - } - .buttonStyle(.borderedProminent) - .disabled(sending || text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - .accessibilityIdentifier("composeSend") - } - .padding(8) - } - .task { - text = initialText - focused = true - } - .onDisappear { - // If we're sending, the dismiss came from send() — skip the draft write. - // The send call has clearDraft: true server-side, so the draft is already cleared. - guard !sending else { return } - let snapshot = text - Task { await onDismissWithDraft(snapshot) } - } - } - - private func send() { - let snapshot = text.trimmingCharacters(in: .whitespacesAndNewlines) - guard !snapshot.isEmpty else { return } - sending = true - Task { - let success = await onSend(snapshot) - if success { - dismiss() - } else { - sending = false - } - } - } -} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift index cd691449f0..9f7e45b1a8 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift @@ -16,7 +16,6 @@ struct MessageListView: View { @State private var presentedVideo: VideoVisual? @State private var presentedVideoNote: VideoNoteVisual? @State private var presentedPoll: PollVoteTarget? - @State private var showCompose: Bool = false @State private var showAttachment: Bool = false @State private var stickerPickerStore: StickerPickerStore? // True when the user is parked within slop of the bottom edge. Updated only on @@ -79,18 +78,6 @@ struct MessageListView: View { ) } } - .sheet(isPresented: $showCompose) { - if let store { - ComposeSheet( - initialText: store.draftText, - onSend: { text in - await store.sendText(text) - return store.lastSendError == nil - }, - onDismissWithDraft: { text in await store.saveDraft(text) } - ) - } - } .sheet(isPresented: $showAttachment) { if let store { AttachmentSheet( @@ -243,7 +230,9 @@ struct MessageListView: View { if row.canSend { ReplyBar( onAttachTap: { showAttachment = true }, - onTextTap: { showCompose = true } + onSend: { snapshot in + Task { await store.sendText(snapshot) } + } ) .id("composeAnchor") .padding(.top, 8) @@ -261,6 +250,14 @@ struct MessageListView: View { } .ignoresSafeArea(edges: .bottom) .defaultScrollAnchor(.bottom) + .scrollContentBackground(.hidden) + .background { + Image("ChatBG") + .resizable() + .aspectRatio(contentMode: .fill) + .clipped() + .ignoresSafeArea() + } .environment(store) .task { // Default branch only appears once `loadState == .loaded` diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/ReplyBar.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/ReplyBar.swift index 8e19ccbb47..19e5db6b19 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/ReplyBar.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/ReplyBar.swift @@ -2,7 +2,7 @@ import SwiftUI struct ReplyBar: View { let onAttachTap: () -> Void - let onTextTap: () -> Void + let onSend: (String) -> Void // Circular "+" button and reply-field capsule share a single pill // diameter so they line up vertically and visually mirror the @@ -25,17 +25,31 @@ struct ReplyBar: View { .padding(.leading, 11) .accessibilityIdentifier("replyBarAttach") - Button(action: onTextTap) { + // TextFieldLink renders an arbitrary label and, when tapped, + // presents the watchOS system text input UI (QuickboardViewService). + // It returns the committed string in onSubmit. The label IS our + // glassy pill — fully styled, no double-chrome artifacts. + // + // Limitation: TextFieldLink does not accept an initial value, so + // the input UI opens blank every time. Existing server-side drafts + // are not visible or editable from the watch under this design — + // accepted regression vs. the prior ComposeSheet flow. + TextFieldLink(prompt: Text("Reply…")) { HStack(spacing: 0) { Text("Reply…") .font(.caption) .foregroundStyle(.secondary) + .lineLimit(1) Spacer(minLength: 0) } .padding(.horizontal, 12) .frame(height: Self.pillHeight) .contentShape(Rectangle()) .glassEffect() + } onSubmit: { newText in + let snapshot = newText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !snapshot.isEmpty else { return } + onSend(snapshot) } .buttonStyle(.plain) .layoutPriority(1) @@ -64,7 +78,7 @@ private struct ReplyBarGeometryPreviewHost: View { .padding(.top, 6) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - ReplyBar(onAttachTap: {}, onTextTap: {}) + ReplyBar(onAttachTap: {}, onSend: { _ in }) .padding(.horizontal, 4) .padding(.bottom, 19) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) diff --git a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj index ab950ef6f3..9b0fe1557e 100644 --- a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj +++ b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj @@ -75,7 +75,6 @@ A0899809BBC92D1563AC4160 /* ReplyBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7584BF25A84BAD24AB992A79 /* ReplyBar.swift */; }; A121DA7622C910B267D69B6E /* PhotoBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F7533C5A9B419B29727C86 /* PhotoBubbleView.swift */; }; A6179A26CB7FB298567682E6 /* DocumentVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2515F0EDA29D5270610A69 /* DocumentVisual.swift */; }; - A73233E40B531EA2D4A81B78 /* ComposeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */; }; A7967495A54936FE19218DC4 /* ServiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */; }; A8EF6F72C78B619893C05AFE /* ChatPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448938C1F69F0D9DE679238 /* ChatPreview.swift */; }; AA3F067A7BC4CDA579CFFC39 /* PollVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4D691106D7EA2FEE35F37F /* PollVisual.swift */; }; @@ -149,7 +148,6 @@ 4DB3C8238D41374CBAC66303 /* EmojiText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiText.swift; sourceTree = ""; }; 4E049ECC956A583A92C5C7CB /* SecretsValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsValidator.swift; sourceTree = ""; }; 53B9831274FA56126E3F5A42 /* LocationSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSendView.swift; sourceTree = ""; }; - 54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSheet.swift; sourceTree = ""; }; 596AF8BF88C38D2018397A33 /* LocationBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationBubbleView.swift; sourceTree = ""; }; 5D3A8FBA5EBDBFDE5AA2FFEC /* QrLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrLoginView.swift; sourceTree = ""; }; 5EA750417DF3AD62C7B788F6 /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = ""; }; @@ -340,7 +338,6 @@ 3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */, C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */, 182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */, - 54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */, E855E4C62401AD1CF41D1835 /* DaySeparatorView.swift */, BDBE08BBB88597B10CDF0142 /* DocumentBubbleView.swift */, FF2515F0EDA29D5270610A69 /* DocumentVisual.swift */, @@ -536,7 +533,6 @@ E48523AFA2E23409CC0C2C2C /* ChatRow.swift in Sources */, 7BF1C89F67290EBAF50900F8 /* ChatRowView.swift in Sources */, 04CEF3F9C267D8A7CEC5EC3C /* ClosedView.swift in Sources */, - A73233E40B531EA2D4A81B78 /* ComposeSheet.swift in Sources */, EF499261BE49A6B1C7B3B1F4 /* ContentView.swift in Sources */, FD5515DB22C571F4D3E5F5FA /* DaySeparatorView.swift in Sources */, F5596A4EF4D2055B10B4A183 /* DocumentBubbleView.swift in Sources */, From aa6314e5c4c46de8f04442f9e19ad4e9fc52b3a7 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 19:00:59 +0200 Subject: [PATCH 05/16] Fix layout --- .../Sources/InstantPageV2Layout.swift | 121 ++++++++---------- 1 file changed, 55 insertions(+), 66 deletions(-) diff --git a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift index dfb798709e..bf24dcede1 100644 --- a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift @@ -2014,21 +2014,17 @@ private func layoutList( ) -> [InstantPageV2LaidOutItem] { // Determine marker characteristics. var maxIndexWidth: CGFloat = 0.0 - var hasTaskMarkers = false + // hasNums: at least one ordered item carries an explicit `num` — in which case items + // without one fall back to a blank " " (preserves the source's numbering gaps) rather + // than auto-generated `(i + 1).`. Unordered lists never auto-generate numbers, so this + // flag is only meaningful when `ordered` is true. (`hasTaskMarkers` is no longer derived + // — the uniform 8pt gap below replaced the per-list `indexSpacing` ternary that consumed + // it; column right-alignment handles mixed bullet/checkbox lists without flagging.) var hasNums = false - if ordered { for item in listItems { - if item.checked != nil { - hasTaskMarkers = true - } else if let num = item.num, !num.isEmpty { + if item.checked == nil, let num = item.num, !num.isEmpty { hasNums = true - } - } - } else { - for item in listItems { - if item.checked != nil { - hasTaskMarkers = true break } } @@ -2046,12 +2042,18 @@ private func layoutList( stroke: context.theme.pageBackgroundColor, border: context.theme.controlColor ) + // Track maxIndexWidth for ALL marker kinds (ordered + unordered, all three shapes), not + // just ordered as V1/older V2 did. With every kind contributing to the marker column width + // we can right-align every marker to a single shared column edge — so in a mixed unordered + // list (bullets + checkboxes) both right-align flush to the same x, and the same uniform + // gap separates them from the text. The column width simply equals the widest marker; for + // a pure bullet list `maxIndexWidth == 6` and the bullet sits at `horizontalInset` (visually + // identical to the pre-change formula), and for a pure checkbox list `maxIndexWidth == 18` + // matches the previous left-aligned placement too. var markerInfos: [MarkerInfo] = [] for (i, item) in listItems.enumerated() { if let checked = item.checked { - if ordered { - maxIndexWidth = max(maxIndexWidth, checklistMarkerSize.width) - } + maxIndexWidth = max(maxIndexWidth, checklistMarkerSize.width) markerInfos.append(MarkerInfo(kind: .checklist(checked: checked, colors: checkboxColors), naturalWidth: checklistMarkerSize.width)) } else if ordered { let value: String @@ -2085,13 +2087,20 @@ private func layoutList( markerInfos.append(MarkerInfo(kind: .number(value), naturalWidth: w)) } else { // Bullet: 6×6 ellipse (matches V1 InstantPageShapeItem dimensions). + maxIndexWidth = max(maxIndexWidth, 6.0) markerInfos.append(MarkerInfo(kind: .bullet, naturalWidth: 6.0)) } } - // indexSpacing = gap between the right edge of markers and the text content. - // V1 values: ordered-task=16, ordered-num=12, unordered-task=24, unordered-bullet=20. - let indexSpacing: CGFloat = ordered ? (hasTaskMarkers ? 16.0 : 12.0) : (hasTaskMarkers ? 24.0 : 20.0) + // Uniform 8pt marker→text gap across all four cases (ordered/unordered × bullet/number/ + // checkbox). With markers right-aligned to a shared column of width `maxIndexWidth`, text + // starts at `horizontalInset + maxIndexWidth + indexSpacing` — so `indexSpacing` IS the + // gap, regardless of marker shape. V1 used 12/16/20/24 (a mix of marker-area-width and + // gap-after-marker, depending on alignment); the four gaps came out to 12/16/14/6 — far + // from uniform, and a 14pt bullet gap looked especially loose. 8pt is a standard iOS list + // gap; it tightens bullets (14→8), numbers (12→8) and ordered-checkbox (16→8), and only + // loosens unordered-checkbox very slightly (6→8) so all four kinds match. + let indexSpacing: CGFloat = 8.0 // Layout each item. var result: [InstantPageV2LaidOutItem] = [] @@ -2143,8 +2152,6 @@ private func layoutList( naturalWidth: markerInfo.naturalWidth, maxIndexWidth: maxIndexWidth, horizontalInset: horizontalInset, - indexSpacing: indexSpacing, - ordered: ordered, checklistMarkerSize: checklistMarkerSize, lineMidY: lineMidY, rtl: context.rtl, @@ -2207,22 +2214,22 @@ private func layoutList( previousBlock = subBlock } - // V1 alignment: number and bullet markers are positioned at originY (top of - // first sub-block); only checklist markers use firstBlockLineMidY for centering. - let markerLineMidY: CGFloat - switch markerInfo.kind { - case .checklist: - markerLineMidY = firstBlockLineMidY ?? originY - case .number, .bullet: - markerLineMidY = originY - } + // Mirror the .text case above (and what .checklist already does here): use the + // first text line's midY for centering. `originY` is the sub-block's TOP, NOT a + // line midpoint — `markerFrameFor` then subtracts `size.height / 2`, so feeding + // `originY` placed the marker straddling the sub-block boundary, ½·marker-height + // ABOVE the first text line. V1 hid the same arithmetic under a 6×12 shape with a + // 3pt internal offset (matching ½·fontLineHeight for 17pt paragraph text), which + // by coincidence equals `firstBlockLineMidY`. Using firstBlockLineMidY directly + // makes the alignment explicit, unifies the three marker kinds, and matches the + // .text case exactly. Fallback to `originY` when no text is in the first sub-block + // (image-first lists are rare); mirrors the existing .checklist fallback. + let markerLineMidY: CGFloat = firstBlockLineMidY ?? originY let markerFrame = markerFrameFor( kind: markerInfo.kind, naturalWidth: markerInfo.naturalWidth, maxIndexWidth: maxIndexWidth, horizontalInset: horizontalInset, - indexSpacing: indexSpacing, - ordered: ordered, checklistMarkerSize: checklistMarkerSize, lineMidY: markerLineMidY, rtl: context.rtl, @@ -2244,57 +2251,39 @@ private func layoutList( } /// Computes the frame for a list marker, handling RTL and all three marker kinds. +/// +/// All marker kinds are right-aligned within the shared `[horizontalInset, horizontalInset + +/// maxIndexWidth]` column (LTR) or left-aligned within the mirrored column on the right (RTL). +/// For a pure-kind list `maxIndexWidth == markerWidth`, so the marker lands at `horizontalInset` +/// exactly as before; for mixed unordered lists, bullets and checkboxes align flush at the +/// column's inner edge. Column right-alignment is the single rule across every marker shape +/// — no `ordered` / `indexSpacing` split — which is why those parameters dropped. private func markerFrameFor( kind: InstantPageV2ListMarkerKind, naturalWidth: CGFloat, maxIndexWidth: CGFloat, horizontalInset: CGFloat, - indexSpacing: CGFloat, - ordered: Bool, checklistMarkerSize: CGSize, lineMidY: CGFloat, rtl: Bool, boundingWidth: CGFloat ) -> CGRect { + let size: CGSize switch kind { case .bullet: - // Bullet: 6×6 ellipse at vertically centred position. - let size = CGSize(width: 6.0, height: 6.0) - if rtl { - let x = boundingWidth - horizontalInset - maxIndexWidth - indexSpacing + (maxIndexWidth - size.width) - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } else { - return CGRect(x: horizontalInset, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } + size = CGSize(width: 6.0, height: 6.0) case .number: - // Number: right-aligned within [horizontalInset, horizontalInset+maxIndexWidth]. - let size = CGSize(width: naturalWidth, height: 20.0) - if rtl { - let x = boundingWidth - horizontalInset - maxIndexWidth - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } else { - let x = horizontalInset + maxIndexWidth - naturalWidth - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } + size = CGSize(width: naturalWidth, height: 20.0) case .checklist: - let size = checklistMarkerSize - if ordered { - if rtl { - let x = boundingWidth - horizontalInset - maxIndexWidth - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } else { - let x = horizontalInset + maxIndexWidth - size.width - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } - } else { - if rtl { - let x = boundingWidth - horizontalInset - size.width - return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } else { - return CGRect(x: horizontalInset, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) - } - } + size = checklistMarkerSize } + let x: CGFloat + if rtl { + x = boundingWidth - horizontalInset - maxIndexWidth + } else { + x = horizontalInset + maxIndexWidth - size.width + } + return CGRect(x: x, y: floorToScreenPixels(lineMidY - size.height / 2.0), width: size.width, height: size.height) } // MARK: - Style helpers (ported from V1 InstantPageLayout.swift lines 32–88) From a3131ba4d215c768ce89f9789a1883a9bb533fc3 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 19:31:00 +0200 Subject: [PATCH 06/16] Update watch signing --- CLAUDE.md | 2 +- Telegram/BUILD | 8 ++++ Telegram/prebuilt_watchos.bzl | 29 ++++++++++++--- Telegram/prebuilt_watchos_patch.sh | 60 ++++++++++++++++++++---------- 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a73f8e41c7..16bd76e601 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ A standalone watchOS Telegram client (developed in the separate `~/build/tgwatch **`Telegram/WatchApp/` is a synced snapshot — do not hand-edit it.** The source of truth and dev tooling live in the `tgwatch` repo. To change the watch app, edit it there, then re-sync with `tgwatch/tools/export-sources.sh /abs/path/to/telegram-ios/Telegram/WatchApp` and commit the result. The committed `tgwatch.xcodeproj` is generated (kept via a `!tgwatch.xcodeproj` negation in `Telegram/WatchApp/.gitignore`, since the root `.gitignore` ignores `*.xcodeproj`); `.build`/`.swiftpm`/`xcuserdata` are excluded. -**How it's wired:** `//Telegram:TelegramWatchApp` (rule in `Telegram/prebuilt_watchos.bzl`) runs in **two actions**: `PrebuiltWatchosCompile` (`Telegram/prebuilt_watchos_compile.sh`) runs xcodebuild on the snapshot in a writable temp copy with PLACEHOLDER version/api values, emitting an unsigned `.app`; `PrebuiltWatchosPatchSign` (`Telegram/prebuilt_watchos_patch.sh`) then rewrites the four per-build Info.plist keys (`CFBundleShortVersionString`, `CFBundleVersion`, `TG_API_ID`, `TG_API_HASH`) and codesigns the `.app` + nested `TDLibFramework.framework` (identity + `ph.telegra.Telegraph.watchkitapp` profile from `--define`s). The result feeds the `Telegram` `ios_application`'s `watch_application` slot (gated by the `//Telegram:embedWatchApp` flag). **The compile action's only inputs are the snapshot (+ its worker)** — so changing the version, build number, api id/hash or signing identity re-runs only the cheap patch+sign action, not xcodebuild; xcodebuild re-runs only when the snapshot changes. This is correct because none of those four values reach the compiled binary: each lands only in the Info.plist (via `$(...)` substitution and a runtime `Bundle.main.object(forInfoDictionaryKey:)` lookup in `Secrets.swift`). +**How it's wired:** `//Telegram:TelegramWatchApp` (rule in `Telegram/prebuilt_watchos.bzl`) runs in **two actions**: `PrebuiltWatchosCompile` (`Telegram/prebuilt_watchos_compile.sh`) runs xcodebuild on the snapshot in a writable temp copy with PLACEHOLDER version/api values (the bundle ids are baked from the snapshot's pbxproj/Info.plist — `ph.telegra.Telegraph.watchkitapp` / `ph.telegra.Telegraph`), emitting an unsigned `.app`; `PrebuiltWatchosPatchSign` (`Telegram/prebuilt_watchos_patch.sh`) then rewrites **six** per-build Info.plist keys (`CFBundleShortVersionString`, `CFBundleVersion`, `TG_API_ID`, `TG_API_HASH`, `CFBundleIdentifier`, `WKCompanionAppBundleIdentifier`) and codesigns the `.app` + nested `TDLibFramework.framework` (identity + the watchkitapp profile from `--define`s). The result feeds the `Telegram` `ios_application`'s `watch_application` slot (gated by the `//Telegram:embedWatchApp` flag). The rule takes `bundle_id` (set to `"{telegram_bundle_id}.watchkitapp"` in `Telegram/BUILD`) and derives the host bundle id by stripping the `.watchkitapp` suffix; both are passed to the patch worker as args (not action inputs), so the patch action re-runs when the host bundle id changes but the (expensive) compile stays cached. **The compile action's only inputs are the snapshot (+ its worker)** — so changing the version, build number, api id/hash, host bundle id, or signing identity re-runs only the cheap patch+sign action, not xcodebuild; xcodebuild re-runs only when the snapshot changes. This is correct because none of those values reach the compiled binary: each lands only in the Info.plist (via `$(...)` substitution and a runtime `Bundle.main.object(forInfoDictionaryKey:)` lookup in `Secrets.swift`, except for the bundle-id keys which only Info.plist consumers read). **Non-obvious invariants** (also in the `.bzl` comments): `AppleBundleInfo`'s public init is banned — use the internal `new_applebundleinfo`; `watch_application` requires BOTH `AppleBundleInfo` (with a non-None `infoplist` File) AND `WatchosApplicationBundleInfo`; the embedded watch app's `CFBundleShortVersionString`/`CFBundleVersion` must exactly equal the host's (sourced from `versions.json['app']` + `--define=buildNumber`); the host does NOT re-sign the embedded watch app, so the worker must sign it; the watch bundle id `ph.telegra.Telegraph.watchkitapp` must track the host `telegram_bundle_id`. diff --git a/Telegram/BUILD b/Telegram/BUILD index 96d6b435e6..10de06ab84 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1743,6 +1743,14 @@ xcode_provisioning_profile( apple_prebuilt_watchos_application( name = "TelegramWatchApp", + # The watch app's bundle id must be `.watchkitapp` for the host + # ios_application's child-bundle-id prefix check; the rule derives the + # companion (host) bundle id from this by stripping `.watchkitapp` and the + # patch worker writes both CFBundleIdentifier and WKCompanionAppBundleIdentifier + # into the embedded watch app's Info.plist. + bundle_id = "{telegram_bundle_id}.watchkitapp".format( + telegram_bundle_id = telegram_bundle_id, + ), tags = ["manual"], ) diff --git a/Telegram/prebuilt_watchos.bzl b/Telegram/prebuilt_watchos.bzl index a72b955dd5..ae77e7ebef 100644 --- a/Telegram/prebuilt_watchos.bzl +++ b/Telegram/prebuilt_watchos.bzl @@ -6,13 +6,17 @@ it through the providers that `ios_application(watch_application = ...)` consume 1. PrebuiltWatchosCompile (prebuilt_watchos_compile.sh) — runs `xcodebuild` against the exported tgwatch source tree with PLACEHOLDER version/api values, emitting an unsigned .app archive. Depends only on the source snapshot. - 2. PrebuiltWatchosPatchSign (prebuilt_watchos_patch.sh) — rewrites the four per-build - Info.plist keys (version, build number, api id/hash) on the compiled app, then - optionally codesigns it. + 2. PrebuiltWatchosPatchSign (prebuilt_watchos_patch.sh) — rewrites the six per-build + Info.plist keys (version, build number, api id/hash, watch CFBundleIdentifier, + WKCompanionAppBundleIdentifier) on the compiled app, then optionally codesigns + it. The watch + companion bundle ids must track the host's `telegram_bundle_id` + (rules_apple validates both via the parent ios_application's + bundle_verification_targets), so they live in the patch action — not in the + xcodebuild snapshot — to keep the compile cached across host-bundle-id changes. Splitting them lets Bazel cache the (expensive, ~4-min) compile whenever only the -version/build number/api/identity change — those values never reach the compiled -binary, only the Info.plist. +version/build number/api/identity/bundle-id change — those values never reach the +compiled binary, only the Info.plist. The providers exposed: @@ -44,6 +48,19 @@ def _apple_prebuilt_watchos_application_impl(ctx): api_hash = ctx.var.get("watchApiHash", "placeholder") identity = ctx.var.get("watchSigningIdentity", "") + # The watch bundle id is `.watchkitapp`; strip the suffix to recover the + # host bundle id, which the patch worker writes to WKCompanionAppBundleIdentifier + # (and the watch's own CFBundleIdentifier needs to be the watch bundle id, not the + # hardcoded one baked in by xcodebuild from the snapshot's pbxproj). The host + # ios_application validates both: child CFBundleIdentifier must start with + # `.`, and child WKCompanionAppBundleIdentifier must equal the host's + # bundle id (see rules_apple's bundle_verification_targets in ios_rules.bzl). + watch_bundle_id = ctx.attr.bundle_id + _watchkitapp_suffix = ".watchkitapp" + if not watch_bundle_id.endswith(_watchkitapp_suffix): + fail("apple_prebuilt_watchos_application bundle_id must end with '.watchkitapp' (got %r)" % watch_bundle_id) + host_bundle_id = watch_bundle_id[:-len(_watchkitapp_suffix)] + # The provisioning profile is an external, machine-specific absolute path passed via # --define rather than a Bazel label, so the gitignored profile need not be exposed as # a target. The local action reads it directly. Empty => unsigned build; when set but @@ -104,6 +121,8 @@ def _apple_prebuilt_watchos_application_impl(ctx): infoplist.path, ctx.file.versions_json.path, build_number, + host_bundle_id, + watch_bundle_id, ], inputs = [ctx.file._patch_worker, compiled_archive, ctx.file.versions_json], outputs = [archive, infoplist], diff --git a/Telegram/prebuilt_watchos_patch.sh b/Telegram/prebuilt_watchos_patch.sh index 25a333768c..4c32f4982d 100755 --- a/Telegram/prebuilt_watchos_patch.sh +++ b/Telegram/prebuilt_watchos_patch.sh @@ -2,32 +2,49 @@ # Patch + sign worker for the apple_prebuilt_watchos_application Bazel rule (action 2 of 2). # # Takes the unsigned, placeholder-version watch .app archive produced by -# prebuilt_watchos_compile.sh, rewrites the four per-build Info.plist values -# (CFBundleShortVersionString, CFBundleVersion, TG_API_ID, TG_API_HASH) — none of -# which affect the compiled binary — then — if a provisioning profile is supplied — -# codesigns the app and its nested frameworks with the watchkitapp provisioning -# profile and a matching identity, and finally zips the .app into the rule's output -# archive. +# prebuilt_watchos_compile.sh, rewrites the six per-build Info.plist values +# (CFBundleShortVersionString, CFBundleVersion, TG_API_ID, TG_API_HASH, +# CFBundleIdentifier, WKCompanionAppBundleIdentifier) — none of which affect the +# compiled binary — then — if a provisioning profile is supplied — codesigns the +# app and its nested frameworks with the watchkitapp provisioning profile and a +# matching identity, and finally zips the .app into the rule's output archive. +# +# Bundle-id rewriting is needed because xcodebuild bakes the snapshot's pbxproj +# PRODUCT_BUNDLE_IDENTIFIER (ph.telegra.Telegraph.watchkitapp) into the compiled +# Info.plist, and WKCompanionAppBundleIdentifier is hardcoded in the snapshot's +# source Info.plist — but the host ios_application's bundle id varies by codesigning +# configuration (e.g. org.telegram.TelegramInternal for development) and rules_apple +# requires the child CFBundleIdentifier to start with the host bundle id and +# WKCompanionAppBundleIdentifier to equal it (bundle_verification_targets in +# ios_rules.bzl). Doing the rewrite here keeps the expensive xcodebuild action +# cached across host-bundle-id changes. # # Splitting this from the compile step lets Bazel cache the (expensive) xcodebuild -# whenever only the version/build number/api/identity change. +# whenever only the version/build number/api/identity/bundle-id change. # # The host ios_application embeds this archive under Watch/ and re-seals the host; # it does NOT re-sign the watch app, so the watch signing must happen here. # # Args: -# $1 input_zip Compiled (unsigned, placeholder-version) .app archive from action 1 -# $2 output_zip Path (declared by Bazel) to write the final .app archive to -# $3 api_id TG_API_ID Info.plist value -# $4 api_hash TG_API_HASH Info.plist value -# $5 identity Codesigning identity (SHA1 hash); empty => derived from $6's cert -# $6 profile Path to the watchkitapp .mobileprovision; empty => unsigned build -# $7 infoplist_out Path (declared by Bazel) to copy the patched Info.plist to -# $8 versions_json versions.json (key 'app' => CFBundleShortVersionString) -# $9 build_number CFBundleVersion +# $1 input_zip Compiled (unsigned, placeholder-version) .app archive from action 1 +# $2 output_zip Path (declared by Bazel) to write the final .app archive to +# $3 api_id TG_API_ID Info.plist value +# $4 api_hash TG_API_HASH Info.plist value +# $5 identity Codesigning identity (SHA1 hash); empty => derived from $6's cert +# $6 profile Path to the watchkitapp .mobileprovision; empty => unsigned build +# $7 infoplist_out Path (declared by Bazel) to copy the patched Info.plist to +# $8 versions_json versions.json (key 'app' => CFBundleShortVersionString) +# $9 build_number CFBundleVersion +# $10 host_bundle_id WKCompanionAppBundleIdentifier value (host app bundle id) +# $11 watch_bundle_id CFBundleIdentifier value (must be ".watchkitapp") set -euo pipefail -IN_ZIP="$1"; OUT_ZIP="$2"; API_ID="$3"; API_HASH="$4"; IDENTITY="${5:-}"; PROFILE="${6:-}"; INFOPLIST_OUT="${7:-}"; VERSIONS_JSON="${8:-}"; BUILD_NUMBER="${9:-1}" +IN_ZIP="$1"; OUT_ZIP="$2"; API_ID="$3"; API_HASH="$4"; IDENTITY="${5:-}"; PROFILE="${6:-}"; INFOPLIST_OUT="${7:-}"; VERSIONS_JSON="${8:-}"; BUILD_NUMBER="${9:-1}"; HOST_BUNDLE_ID="${10:-}"; WATCH_BUNDLE_ID="${11:-}" + +if [ -z "$HOST_BUNDLE_ID" ] || [ -z "$WATCH_BUNDLE_ID" ]; then + echo "error: host_bundle_id and watch_bundle_id must both be supplied (got host=$HOST_BUNDLE_ID watch=$WATCH_BUNDLE_ID)" >&2 + exit 1 +fi # Match the host app's version (rules_apple requires the embedded watch app's # CFBundleShortVersionString/CFBundleVersion to equal the parent's). @@ -46,15 +63,20 @@ if [ -z "$APP" ]; then exit 1 fi -# Overwrite the placeholder values baked in at compile time. All four keys already +# Overwrite the placeholder values baked in at compile time. All six keys already # exist in the compiled (binary-format) Info.plist, so PlistBuddy Set preserves their # (string) type — matching what $(...) substitution produced and what Secrets.swift -# expects from Bundle.main.object(forInfoDictionaryKey:). +# expects from Bundle.main.object(forInfoDictionaryKey:). The bundle-id keys track +# the host's `telegram_bundle_id` (see the file header for why); xcodebuild bakes +# `ph.telegra.Telegraph.watchkitapp` / `ph.telegra.Telegraph` here from the +# snapshot's pbxproj/Info.plist regardless of what host the rest of the build uses. PLIST="$APP/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $MARKETING_VERSION" "$PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST" /usr/libexec/PlistBuddy -c "Set :TG_API_ID $API_ID" "$PLIST" /usr/libexec/PlistBuddy -c "Set :TG_API_HASH $API_HASH" "$PLIST" +/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $WATCH_BUNDLE_ID" "$PLIST" +/usr/libexec/PlistBuddy -c "Set :WKCompanionAppBundleIdentifier $HOST_BUNDLE_ID" "$PLIST" # Expose the patched watch Info.plist (the host reads it to verify the companion # bundle-id linkage and the child version). Codesigning does not alter Info.plist From 55619c54f9444c93b645a0acde1ec7bae089d1c3 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 19:31:09 +0200 Subject: [PATCH 07/16] Fix crash --- .../TelegramStringFormatting/Sources/MessageContentKind.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index 046286b6c5..d93dfea46e 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -281,6 +281,9 @@ public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedStr } let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if range.upperBound >= updatedString.length { + continue + } let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) var updatedAttributes: [NSAttributedString.Key: Any] = currentDict From 3a62d3417f9d48cb468afd3fc5a23421119d0842 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 21:10:19 +0200 Subject: [PATCH 08/16] Update tgwatch --- .../tgwatch Watch App/Chats/AvatarView.swift | 13 ++-- .../Chats/ChatListView.swift | 61 ++++++++++--------- .../Messages/MessageListView.swift | 11 ++++ 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/Telegram/WatchApp/tgwatch Watch App/Chats/AvatarView.swift b/Telegram/WatchApp/tgwatch Watch App/Chats/AvatarView.swift index e8ed5a0b2f..02802ddc40 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Chats/AvatarView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Chats/AvatarView.swift @@ -1,17 +1,20 @@ import SwiftUI import UIKit -/// A 36pt circular chat avatar. Renders (priority): Saved-Messages bookmark glyph; +/// A circular chat avatar (default 36pt, customizable via `size`). Renders (priority): /// crisp downloaded photo; blurred minithumbnail placeholder; initials on a -/// per-chat color. Drives viewport-based download of the small photo via -/// `.onScrollVisibilityChange` (same as `PhotoBubbleView`) — only while the photo -/// exists and hasn't downloaded yet. +/// per-chat color. When embedded in a `ScrollView`, drives viewport-based download +/// of the small photo via `.onScrollVisibilityChange` (same as `PhotoBubbleView`) — +/// only while the photo exists and hasn't downloaded yet. Outside a scroll +/// container (e.g. as a `ToolbarItem`), the visibility callback fires once on +/// appear and once on disappear, so the download path is "request on chat open, +/// cancel on chat close." struct AvatarView: View { let avatar: AvatarVisual var onRequestDownload: (Int) -> Void = { _ in } var onCancelDownload: (Int) -> Void = { _ in } - private let size: CGFloat = 36 + var size: CGFloat = 36 var body: some View { content diff --git a/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift b/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift index 73f4a3c02a..f6dd7e2c78 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift @@ -13,39 +13,42 @@ struct ChatListView: View { @FocusState private var listFocused: Bool var body: some View { - ZStack(alignment: .top) { - NavigationStack { - ScrollViewReader { proxy in - VStack(spacing: 0) { - if case .failed(let message) = store.loadState(for: store.currentFolder) { - banner(text: message, kind: .retry) - } - if let err = client.lastError, dismissedLastError != err { - banner(text: err, kind: .dismiss) - } - content(proxy: proxy) + NavigationStack { + ScrollViewReader { proxy in + VStack(spacing: 0) { + if case .failed(let message) = store.loadState(for: store.currentFolder) { + banner(text: message, kind: .retry) } - .navigationDestination(for: ChatRow.self) { row in - MessageListView(row: row) + if let err = client.lastError, dismissedLastError != err { + banner(text: err, kind: .dismiss) } - .navigationTitle("") - .navigationBarTitleDisplayMode(.inline) - // Must live INSIDE the NavigationStack so the modifier's - // .navigationDestination(isPresented:) attaches to it. - .accountSwitcherSheet(presentation: .push, logoutAffordance: .allowed) + content(proxy: proxy) } + // Gradient is .overlay on the chat-list VStack — NOT a sibling + // of NavigationStack. Earlier this lived in an outer ZStack and + // rendered on top of every pushed destination (e.g. MessageListView's + // back chevron + title), since ZStack draws siblings front-to-back + // regardless of which destination is current. Scoping it to the + // root view's overlay restricts it to the chat list itself. + .overlay(alignment: .top) { + LinearGradient( + colors: [.black.opacity(0.8), .black.opacity(0)], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: 48) + .allowsHitTesting(false) + .ignoresSafeArea(edges: .top) + } + .navigationDestination(for: ChatRow.self) { row in + MessageListView(row: row) + } + .navigationTitle("") + .navigationBarTitleDisplayMode(.inline) + // Must live INSIDE the NavigationStack so the modifier's + // .navigationDestination(isPresented:) attaches to it. + .accountSwitcherSheet(presentation: .push, logoutAffordance: .allowed) } - // Gradient that fades chat content behind the system clock area, - // so rows scrolling up under the clock don't visually collide - // with the time readout. - LinearGradient( - colors: [.black.opacity(0.8), .black.opacity(0)], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: 48) - .allowsHitTesting(false) - .ignoresSafeArea(edges: .top) } .accessibilityIdentifier("chatListView") } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift index 9f7e45b1a8..d653925466 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift @@ -56,6 +56,17 @@ struct MessageListView: View { } .navigationTitle(row.title) .accessibilityIdentifier("messageListView") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + AvatarView( + avatar: row.avatar, + onRequestDownload: { fileId in store?.requestFileDownload(fileId: fileId) }, + onCancelDownload: { fileId in store?.cancelFileDownload(fileId: fileId) }, + size: 36 + ) + .glassEffect(in: Circle()) + } + } .sheet(item: $presentedPhoto) { photo in PhotoViewerView(photo: photo) } From 0be58ef45a1efa0625b001342fd251d9d8382df8 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 21:23:34 +0200 Subject: [PATCH 09/16] Enable watch app cache --- Telegram/prebuilt_watchos.bzl | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Telegram/prebuilt_watchos.bzl b/Telegram/prebuilt_watchos.bzl index ae77e7ebef..69f25f04f1 100644 --- a/Telegram/prebuilt_watchos.bzl +++ b/Telegram/prebuilt_watchos.bzl @@ -80,7 +80,29 @@ def _apple_prebuilt_watchos_application_impl(ctx): # separate output (resources.bzl bundle_verification crashes on a None infoplist). infoplist = ctx.actions.declare_file(ctx.label.name + "_Info.plist") - exec_requirements = { + # The compile action runs xcodebuild locally (needs the host's Xcode + SwiftPM + # network access), but its output — an unsigned, placeholder-version .app — is + # portable across machines that share the same Xcode SDK, so it IS shared via the + # remote cache: `no-remote-exec` (not `no-remote`) lets Bazel read/write the + # `--remote_cache` while still pinning execution local on cache miss. This is the + # big win when CI builders share a remote cache — a peer's xcodebuild result is + # reused instead of every fresh worker paying the ~4-min build. Caveat: if the + # build fleet runs different Xcode major versions, mismatched artifacts could be + # served (the action key does not include the Xcode version); align Xcode across + # builders, or downgrade to `no-remote-cache` to be safe. + compile_exec_requirements = { + "no-sandbox": "1", + "no-remote-exec": "1", + "local": "1", + "requires-network": "1", + } + + # The patch+sign action cannot share results across machines: its inputs include + # the absolute `--watchProvisioningProfile` path and the codesigning identity is + # resolved from the local keychain, both machine-specific. Remote-cache lookups + # would essentially never hit and uploads would just waste bandwidth, so keep the + # umbrella `no-remote` here. + patch_exec_requirements = { "no-sandbox": "1", "no-remote": "1", "local": "1", @@ -101,7 +123,7 @@ def _apple_prebuilt_watchos_application_impl(ctx): outputs = [compiled_archive], mnemonic = "PrebuiltWatchosCompile", progress_message = "Compiling watch app via xcodebuild", - execution_requirements = exec_requirements, + execution_requirements = compile_exec_requirements, use_default_shell_env = True, ) @@ -128,7 +150,7 @@ def _apple_prebuilt_watchos_application_impl(ctx): outputs = [archive, infoplist], mnemonic = "PrebuiltWatchosPatchSign", progress_message = "Patching%s watch app Info.plist" % (" + signing" if profile else ""), - execution_requirements = exec_requirements, + execution_requirements = patch_exec_requirements, use_default_shell_env = True, ) From a2548088f6cc24676c72946507d1c4f49bf58355 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 22:07:52 +0200 Subject: [PATCH 10/16] Update watch app --- .../Chats/ChatListView.swift | 46 +++++++------------ .../tgwatch Watch App/Chats/FolderPill.swift | 2 +- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift b/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift index f6dd7e2c78..5f41b221b7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift @@ -24,27 +24,20 @@ struct ChatListView: View { } content(proxy: proxy) } - // Gradient is .overlay on the chat-list VStack — NOT a sibling - // of NavigationStack. Earlier this lived in an outer ZStack and - // rendered on top of every pushed destination (e.g. MessageListView's - // back chevron + title), since ZStack draws siblings front-to-back - // regardless of which destination is current. Scoping it to the - // root view's overlay restricts it to the chat list itself. - .overlay(alignment: .top) { - LinearGradient( - colors: [.black.opacity(0.8), .black.opacity(0)], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: 48) - .allowsHitTesting(false) - .ignoresSafeArea(edges: .top) - } .navigationDestination(for: ChatRow.self) { row in MessageListView(row: row) } - .navigationTitle("") + .navigationTitle("Chats") .navigationBarTitleDisplayMode(.inline) + // `.toolbar(.visible)` forces the nav-bar container to + // materialize on the chat list. Without it, watchOS-26 skips + // chrome on a NavigationStack root view, leaving only the + // status row (clock top-right). The push to MessageListView + // then has to grow that minimal status row into a full glass + // nav bar with back chevron + title + trailing avatar, which + // glitches mid-reshape. With the bar present here, the push + // only has to morph content, not allocate a new glass surface. + .toolbar(.visible, for: .navigationBar) // Must live INSIDE the NavigationStack so the modifier's // .navigationDestination(isPresented:) attaches to it. .accountSwitcherSheet(presentation: .push, logoutAffordance: .allowed) @@ -99,18 +92,13 @@ struct ChatListView: View { .listStyle(.plain) .focused($listFocused) .onAppear { listFocused = true } - // Hide the empty title bar so the chat list starts directly under the - // system clock. Pill bar is the first List row, so it scrolls with content. - .toolbar(.hidden, for: .navigationBar) - // `.toolbar(.hidden)` hides the bar visuals but watchOS still reserves - // ~30pt of layout. With a pill bar present, pull up by 38 (slot + 8) - // so the pill row sits ~8pt under the clock — the gradient masks any - // overlap, and the negative bottom inset on the pill row tightens the - // gap to the first chat row to ~8pt. Without a pill bar the first row - // IS a chat row, so we stop at -22 (slot - 8) to leave ~8pt of clear - // space below the clock instead of shoving the chat row into the time - // readout. - .padding(.top, store.pills.count > 1 ? -38 : -22) + // Bar is intentionally visible (forced by `.toolbar(.visible, for: + // .navigationBar)` on the body chain). When folder pills are present, + // pull the pill row up by 24pt to tuck it under the bar's bottom edge + // — without that, the natural top-of-list inset leaves an awkward gap + // between the bar and the pill row. Plain chat rows (no pills) sit at + // the natural top. + .padding(.top, store.pills.count > 1 ? -20 : 0) } @ViewBuilder diff --git a/Telegram/WatchApp/tgwatch Watch App/Chats/FolderPill.swift b/Telegram/WatchApp/tgwatch Watch App/Chats/FolderPill.swift index 8272a87e41..6f25283a64 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Chats/FolderPill.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Chats/FolderPill.swift @@ -7,7 +7,7 @@ import TDLibKit /// is summed across all chats whose `positions` include `chatList`. struct FolderPill: Identifiable, Equatable, Hashable { static let allChatsId: Int = -1 - static let allChatsName = "All chats" + static let allChatsName = "All" let id: Int let chatList: ChatList From 4c254fe5b41ed1192f03f8621a2941e17d8a3322 Mon Sep 17 00:00:00 2001 From: isaac Date: Thu, 28 May 2026 22:19:28 +0200 Subject: [PATCH 11/16] Spec: InstantPage blockQuote nested-blocks payload Upgrades the InstantPageBlock.blockQuote case from a text-only payload to a nested-blocks payload, covering API parse, Postbox + FlatBuffers serialization, API encode, V1/V2 layout, markdown forward/reverse, entity-expressibility, and preview text. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...29-instantpage-blockquote-blocks-design.md | 514 ++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md diff --git a/docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md b/docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md new file mode 100644 index 0000000000..ad8c607583 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md @@ -0,0 +1,514 @@ +# InstantPage BlockQuote — nested blocks payload + +## Context + +Telegram's instant-view API has two parallel constructors for the block-quote +page block: + +- `pageBlockBlockquote(text: RichText, caption: RichText)` — legacy + text-only form. +- `pageBlockBlockquoteBlocks(blocks: [PageBlock], caption: RichText)` — new + form whose body is a sequence of nested page blocks (paragraphs, lists, + headings, code, even nested quotes). + +The Swift model currently represents only the legacy form +(`InstantPageBlock.blockQuote(text: RichText, caption: RichText)`) and the API +parser has a `//TODO` placeholder for the new constructor +(`submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift:251`). This spec +upgrades parsing, serialization (Postbox + FlatBuffers), encoding back to the +wire, and rendering (V1 + V2) to support nested blocks throughout. + +## Decisions + +- **Single enum case, blocks-only payload.** Replace + `(text: RichText, caption: RichText)` with + `(blocks: [InstantPageBlock], caption: RichText)`. The legacy text-only + inbound shape is lifted into the new shape at parse time by wrapping the + RichText in a synthetic `.paragraph(text)`. Downstream code branches on + zero shapes. +- **Full block-level layout in both renderers.** V2 + (`InstantPageV2Layout.swift`) and V1 (`InstantPageLayout.swift`) recurse + into the child blocks. No surface where nested-block quotes render + degraded. +- **Outbound: legacy when shape allows.** `apiInputBlock()` emits the legacy + `pageBlockBlockquote` constructor when blocks is `[.paragraph(text)]` (or + empty), and the new `pageBlockBlockquoteBlocks` constructor otherwise. + Keeps the common chat case on the wire constructor older client recipients + already understand. +- **`.pullQuote` is unchanged.** The TL API has no `pullQuoteBlocks` + constructor; the `.pullQuote(text: RichText, caption: RichText)` case keeps + its existing shape, parser, serializer, and renderer. + +## Architecture + +### Enum shape + +`SyncCore_InstantPage.swift:73`: + +```swift +case blockQuote(blocks: [InstantPageBlock], caption: RichText) +``` + +The enum is already declared `indirect`, so a `[InstantPageBlock]` payload +needs no further annotation. + +### API parsing (`InstantPage.swift:247–252`) + +```swift +case let .pageBlockBlockquote(data): + self = .blockQuote( + blocks: [.paragraph(RichText(apiText: data.text))], + caption: RichText(apiText: data.caption) + ) +case let .pageBlockBlockquoteBlocks(data): + self = .blockQuote( + blocks: data.blocks.map { InstantPageBlock(apiBlock: $0) }, + caption: RichText(apiText: data.caption) + ) +``` + +### API encoding (`InstantPage.swift:376`) + +```swift +case let .blockQuote(blocks, caption): + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + return .pageBlockBlockquote(.init( + text: text.apiRichText(), caption: caption.apiRichText() + )) + } + if blocks.isEmpty { + return .pageBlockBlockquote(.init( + text: RichText.empty.apiRichText(), + caption: caption.apiRichText() + )) + } + return .pageBlockBlockquoteBlocks(.init( + blocks: blocks.compactMap { $0.apiInputBlock() }, + caption: caption.apiRichText() + )) +``` + +### Postbox coding + +`SyncCore_InstantPage.swift:228` (encoder): write `"b"` (object array of +blocks) and `"c"` (caption). Stop writing `"t"`. + +`SyncCore_InstantPage.swift:123` (decoder): mirror the `decodeListItems` +pattern at line 39 — if the legacy `"t"` key is present, lift it into a +single-paragraph blocks array; otherwise decode the new `"b"` array. + +```swift +case InstantPageBlockType.blockQuote.rawValue: + let caption = decoder.decodeObjectForKey("c", decoder: { + RichText(decoder: $0) + }) as! RichText + if let legacyText = decoder.decodeObjectForKey("t", decoder: { + RichText(decoder: $0) + }) as? RichText { + self = .blockQuote(blocks: [.paragraph(legacyText)], caption: caption) + } else { + let blocks: [InstantPageBlock] = + decoder.decodeObjectArrayWithDecoderForKey("b") + self = .blockQuote(blocks: blocks, caption: caption) + } +``` + +Old stored cached pages (with `"t"` set) decode unchanged; new writes only use +`"b"`. + +### FlatBuffers + +`InstantPageBlock.fbs:93`: + +```fbs +table InstantPageBlock_BlockQuote { + text:RichText (id: 0); // (required) dropped — legacy only + caption:RichText (id: 1, required); + blocks:[InstantPageBlock] (id: 2); // new +} +``` + +Dropping `(required)` from an existing field and appending a new field at a +higher id are both schema-evolution-safe per FlatBuffers rules. Per the +`flatbuffers-codegen` memory the `.fbs` is the source of truth and Bazel +regenerates `*_generated.swift`; the checked-in copies in `Sources/` are +stale and should NOT be hand-edited. + +Codec decoder (`SyncCore_InstantPage.swift:620`): + +```swift +case .instantpageblockBlockquote: + guard let value = flatBuffersObject.value( + type: TelegramCore_InstantPageBlock_BlockQuote.self + ) else { + throw FlatBuffersError.missingRequiredField() + } + let caption = try RichText(flatBuffersObject: value.caption) + if value.blocksCount > 0 { + let blocks = try (0 ..< value.blocksCount).map { + try InstantPageBlock(flatBuffersObject: value.blocks(at: $0)!) + } + self = .blockQuote(blocks: blocks, caption: caption) + } else if let legacyText = value.text { + self = .blockQuote( + blocks: [.paragraph(try RichText(flatBuffersObject: legacyText))], + caption: caption + ) + } else { + self = .blockQuote(blocks: [], caption: caption) + } +``` + +Codec encoder (`SyncCore_InstantPage.swift:799`): write `blocks` + `caption`; +omit `text`. + +### Equality (`SyncCore_InstantPage.swift:448`) + +```swift +case let .blockQuote(lhsBlocks, lhsCaption): + if case let .blockQuote(rhsBlocks, rhsCaption) = rhs, + lhsBlocks == rhsBlocks, lhsCaption == rhsCaption { + return true + } else { + return false + } +``` + +Mirrors the `.collage`/`.slideshow` pattern (which already do +`[InstantPageBlock]` equality). + +### V2 renderer + +`InstantPageV2Layout.swift:597` keeps dispatching to `layoutBlockQuote` for +the blockquote arm. Split the existing function: + +- `layoutBlockQuote(blocks:caption:...)` — new, for `.blockQuote` only. +- `layoutPullQuote(text:caption:...)` — for `.pullQuote` only; same behavior + as today's `isPull = true` branch. + +`layoutBlockQuote` strategy: + +```swift +private func layoutBlockQuote( + blocks: [InstantPageBlock], + caption: RichText, + boundingWidth: CGFloat, + horizontalInset: CGFloat, + context: inout LayoutContext +) -> [InstantPageV2LaidOutItem] { + let verticalInset: CGFloat = 4.0 + let lineInset: CGFloat = 20.0 + let barWidth: CGFloat = 3.0 + + var result: [InstantPageV2LaidOutItem] = [] + var contentHeight: CGFloat = verticalInset + + let innerBoundingWidth = boundingWidth - horizontalInset * 2.0 - lineInset + let innerHorizontalInset = horizontalInset + lineInset + + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + // Fast path: preserve today's italicized body styling for the + // legacy single-paragraph shape (unchanged from current code). + } else { + var previousItems: [InstantPageV2LaidOutItem] = [] + for (i, child) in blocks.enumerated() { + var childItems = layoutBlock( + child, + boundingWidth: innerBoundingWidth, + horizontalInset: innerHorizontalInset, + isCover: false, + previousItems: previousItems, + isLast: i == blocks.count - 1, + context: &context + ) + // Stack vertically: offset Y by current contentHeight. + // X is already correct (children laid out at innerHorizontalInset). + for j in childItems.indices { + childItems[j] = childItems[j].translatedY(by: contentHeight) + } + let childMaxY = childItems.map { $0.frame.maxY }.max() ?? contentHeight + contentHeight = max(contentHeight, childMaxY) + previousItems.append(contentsOf: childItems) + result.append(contentsOf: childItems) + } + } + + // Caption (existing branch, unchanged). + if case .empty = caption { /* nothing */ } else { + contentHeight += 14.0 + // ...existing caption layout, using innerHorizontalInset... + } + contentHeight += verticalInset + + // Vertical bar on the leading edge (existing behavior). + let bar = InstantPageV2BarItem( + frame: CGRect(x: horizontalInset, y: 0.0, + width: barWidth, height: contentHeight), + color: context.theme.textCategories.paragraph.color, + cornerRadius: barWidth / 2.0 + ) + result.append(.blockQuoteBar(bar)) + + return result +} +``` + +Notes: + +- **Translation helper.** `InstantPageV2LaidOutItem` covers many variants + (text/shape/list-marker/bar/checklist/media/...). Add a small extension + method `translatedY(by:)` that returns a copy with the item's frame's + `origin.y` offset. Implemented as a switch over the case set, mirroring + any existing per-variant frame-edit helper in the file. +- **`previousItems` for spacing.** V2's per-block layout functions consume + `previousItems` to compute inter-block gaps. Passing a fresh array per + recursion gives correct in-quote spacing without affecting the outer + page's `previousItems`. +- **Styling of nested children.** Direct children render with their normal + category styling (heading stays a heading, list stays a list). Italics are + applied only by the single-paragraph fast path — consistent with the + visual fidelity goal for legacy quotes without complicating the recursive + case. + +### V1 renderer + +`InstantPageLayout.swift:504` is the `.blockQuote` arm of the giant top-level +`switch self` inside the `extension InstantPageBlock { func layout(...) -> InstantPageLayout }`. +Because the enum is already `indirect`, the arm can simply call +`child.layout(...)` recursively for each block in `blocks` — no signature +refactor needed. + +BlockQuote arm: + +```swift +case let .blockQuote(blocks, caption): + let lineInset: CGFloat = 20.0 + let verticalInset: CGFloat = 4.0 + var contentSize = CGSize(width: boundingWidth, height: verticalInset) + var items: [InstantPageItem] = [] + + let innerBoundingWidth = boundingWidth - horizontalInset * 2.0 - lineInset + let innerHorizontalInset = horizontalInset + lineInset + + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + // Fast path: existing italicized body layout (unchanged). + } else { + var previousChildItems: [InstantPageItem] = [] + for child in blocks { + let childLayout = child.layout( + boundingWidth: innerBoundingWidth, + horizontalInset: innerHorizontalInset, + safeInset: safeInset, + isCover: false, + previousItems: previousChildItems, + fillToSize: nil, + media: media, + mediaIndexCounter: &mediaIndexCounter, + embedIndexCounter: &embedIndexCounter, + detailsIndexCounter: &detailsIndexCounter, + theme: theme, + strings: strings, + /* ... whichever other params the current signature takes ... */ + fitToWidth: fitToWidth, + webpage: webpage + ) + for var item in childLayout.items { + item.frame = item.frame.offsetBy(dx: 0.0, dy: contentSize.height) + items.append(item) + previousChildItems.append(item) + } + contentSize.height += childLayout.contentSize.height + } + } + + // Caption (existing branch, using innerHorizontalInset). + if case .empty = caption { /* nothing */ } else { + contentSize.height += 14.0 + // ...existing caption layout, parameterized on innerHorizontalInset... + } + contentSize.height += verticalInset + + let shapeItem = InstantPageShapeItem( + frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), + size: CGSize(width: 3.0, height: contentSize.height)), + shapeFrame: CGRect(origin: .zero, + size: CGSize(width: 3.0, height: contentSize.height)), + shape: .roundLine, + color: theme.textCategories.paragraph.color + ) + items.append(shapeItem) + + return InstantPageLayout(origin: CGPoint(), + contentSize: contentSize, items: items) +``` + +The exact parameter list of the recursive `child.layout(...)` call mirrors +the current method's parameter list as it exists in the codebase; the planning +step will reconcile against the actual method signature (which may differ +from the snippet above in nominal arity). + +### Markdown forward parser + +`BrowserMarkdown.swift:1394`. Replace the current per-child-paragraph +fragmentation with a single quote that carries all child blocks: + +```swift +case .blockQuote: + var childBlocks: [InstantPageBlock] = [] + for child in node.children { + guard let parsed = markdownBlocks( + from: child, context: context, depth: depth + 1 + ) else { + return nil + } + childBlocks.append(contentsOf: parsed) + } + guard !childBlocks.isEmpty else { + return [] + } + return [.blockQuote(blocks: childBlocks, caption: .empty)] +``` + +**Behavior change worth noting.** Today a markdown +`> p1\n>\n> p2` produces TWO separate top-level quotes because the current +code emits one quote per child paragraph (a workaround for the text-only +model). Under the new model that becomes one quote with two paragraphs — +which is the correct semantics. Both forms continue to trigger the rich-send +gate (under the new entity-expressibility rule below, a multi-paragraph +quote is no longer entity-expressible — see "Risks" below). + +### Entity-expressibility (`BrowserMarkdown.swift:1119`) + +Telegram message-entity blockquotes are flat (single span of inline text). +The new gate: + +```swift +case .blockQuote(let blocks, let caption): + guard isEmptyRichText(caption) else { return false } + return blocks.allSatisfy { child in + if case let .paragraph(text) = child { + return richTextIsEntityExpressible(text) + } + return false + } +``` + +- A single `> quote` stays entity-expressible (`[.paragraph(t)]`, entity- + expressible text) → sends via the regular entity path. +- A nested-structure quote (`> # heading`, `> - list`) is not entity- + expressible → sends via the rich path. +- See "Risks" for the multi-paragraph case. + +### Markdown reverse converter + +`InstantPageToMarkdown.swift:42`. Recurse into each child block, dispatching +through the file's existing `markdownString(from:)` (which already knows +how to emit headings, lists, code, etc.), and prepend `> ` to every line: + +```swift +case let .blockQuote(blocks, _): + return markdownBlockQuote(blocks: blocks) + +private func markdownBlockQuote(blocks: [InstantPageBlock]) -> String { + var lines: [String] = [] + for block in blocks { + guard let body = markdownString(from: block, /* args */) else { + continue + } + for line in body.split(separator: "\n", + omittingEmptySubsequences: false) { + lines.append("> \(String(line))") + } + } + return lines.joined(separator: "\n") +} +``` + +`.pullQuote(text, _)` continues to call the existing +`markdownBlockQuote(_:RichText)`. + +### Preview text (`InstantPagePreviewText.swift:126`) + +```swift +case let .blockQuote(blocks, caption): + let body = blocks.map { $0.previewText() }.joined(separator: " ") + return body + caption.previewText() +``` + +Uses the existing per-block `previewText()` extension at the top of the +file so nested previews work transparently. + +### FAQ matcher (`CachedFaqInstantPage.swift:23`) + +The match is `case .blockQuote:` with no payload destructure — no change +needed. + +## Risks + +- **Behavior change for multi-paragraph quotes in chat send.** Under today's + text-only model, `> p1\n>\n> p2` fragments into two top-level + `.blockQuote(text: p_i, caption: .empty)` blocks, each of which is + individually entity-expressible — so the message sends via the regular + entity path (two consecutive blockquote entities). Under the new model + the markdown parser emits one `.blockQuote(blocks: [.paragraph(p1), + .paragraph(p2)], caption: .empty)`, which is no longer entity-expressible + under the proposed gate (multi-paragraph blockquote can't be a single + flat entity). So the same message starts going via the rich path. + + This is correct semantically — the structure IS preserved end to end — + but it changes the wire format for an existing user-visible flow. A + minor compromise is available: in `blockIsEntityExpressible`, treat a + multi-paragraph quote as entity-expressible by serializing each + paragraph through a separate entity at the message-build step; this is + more involved and out of scope for the first cut. The risk is small — + recipients of the rich message render it correctly; the only user- + visible difference is that the message lands as a rich block on + recipients who would otherwise have seen the consecutive-entities + flattening. + +- **Old recipients receiving `pageBlockBlockquoteBlocks` over the wire.** + Older clients that haven't been updated to parse the new constructor + will route it to `.unsupported` and skip it. The outbound choice + ("legacy when shape allows") keeps the common single-paragraph case + on the legacy constructor, minimizing this risk to actual nested-block + quotes where there's no legacy equivalent anyway. + +- **FlatBuffers schema evolution.** Dropping `(required)` and appending a + new field at a higher id are documented as safe under FBS rules. The + same iOS app and a Telegram-Mac peer share the schema definition (per + the project's TelegramCore conventions), so both ends must move together + or accept that one side will see `text` populated while the other writes + only `blocks` — which the decoder handles correctly (legacy path). + +- **V1 recursive layout call signature drift.** The exact parameter list of + `child.layout(...)` is reconciled in the implementation plan, not in the + spec — the V1 layout method's signature is long and any plumbing detail + is best captured at edit time rather than guessed here. + +## Out of scope + +- `.pullQuote` enum shape, parsing, encoding, and renderer. +- Streaming/reveal animation in `ChatMessageRichDataBubbleContentNode`. + Nested-block quotes emit ordinary `InstantPageV2LaidOutItem`s consumed by + the existing width-based reveal cost map; no special handling. +- Inline animated emoji owned by `InstantPageV2View`. Quotes carrying + paragraphs with custom emoji "just work" because each child paragraph's + text items route through `updateInlineEmoji` normally. + +## Files affected + +| File | Change | +|---|---| +| `submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift` | Enum case shape; Postbox encoder/decoder; equality; FlatBuffers encoder/decoder. | +| `submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs` | Drop `(required)` from `text`; add `blocks:[InstantPageBlock] (id: 2)`. | +| `submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift` | API parse for both inbound constructors; API encode with legacy-when-possible. | +| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` | Split `layoutBlockQuote` into block- and pull- variants; recurse into child blocks. Add `translatedY(by:)` helper on `InstantPageV2LaidOutItem`. | +| `submodules/InstantPageUI/Sources/InstantPageLayout.swift` | Recurse into child blocks in the `.blockQuote` arm. | +| `submodules/BrowserUI/Sources/BrowserMarkdown.swift` | Single quote with all child blocks (forward); entity-expressibility gate. | +| `submodules/BrowserUI/Sources/InstantPageToMarkdown.swift` | Recursive `markdownBlockQuote(blocks:)`. | +| `submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift` | Concatenate child previews. | + +`submodules/SettingsUI/Sources/CachedFaqInstantPage.swift` (line 23) is a +payload-less match and needs no edit, but should be re-verified during the +implementation build (full build is the completeness gate per the +project's "no per-module build" rule). From 54958c5b19f169f605c5a22f0fb6deffd3771a19 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Thu, 28 May 2026 23:38:11 +0200 Subject: [PATCH 12/16] Fix parsing --- .../Sources/ApiUtils/InstantPage.swift | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 211951e578..5fbc475515 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -89,30 +89,25 @@ extension InstantPageListItem { } } - func apiInputPageOrderedListItem() -> Api.InputPageListOrderedItem { + func apiInputPageOrderedListItem() -> Api.PageListOrderedItem { switch self { case let .text(value, num, checked): var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked) - var inputNum: Int32? - if let num, let numValue = Int32(num) { - inputNum = numValue + if num != nil { flags |= (1 << 2) } - - return .inputPageListOrderedItemText(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemText(flags: flags, text: value.apiRichText(), value: inputNum, type: nil)) + return .pageListOrderedItemText(Api.PageListOrderedItem.Cons_pageListOrderedItemText(flags: flags, num: num, text: value.apiRichText(), value: nil, type: nil)) case let .blocks(blocks, num, checked): var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked) - var inputNum: Int32? - if let num, let numValue = Int32(num) { - inputNum = numValue + if num != nil { flags |= (1 << 2) } - return .inputPageListOrderedItemBlocks(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemBlocks(flags: flags, blocks: blocks.compactMap { $0.apiInputBlock() }, value: inputNum, type: nil)) + return .pageListOrderedItemBlocks(Api.PageListOrderedItem.Cons_pageListOrderedItemBlocks(flags: flags, num: num, blocks: blocks.compactMap { $0.apiInputBlock() }, value: nil, type: nil)) case .unknown: - return .inputPageListOrderedItemText(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemText(flags: 0, text: .textPlain(Api.RichText.Cons_textPlain(text: "")), value: nil, type: nil)) + return .pageListOrderedItemText(Api.PageListOrderedItem.Cons_pageListOrderedItemText(flags: 0, num: nil, text: .textPlain(Api.RichText.Cons_textPlain(text: "")), value: nil, type: nil)) } } } @@ -331,7 +326,7 @@ extension InstantPageBlock { self = .heading(text: RichText(apiText: pageBlockHeading6.text), level: 6) case let .pageBlockMath(pageBlockMath): self = .formula(latex: pageBlockMath.source) - case .inputPageBlockMap, .inputPageBlockOrderedList, .pageBlockThinking: + case .inputPageBlockMap, .pageBlockThinking: self = .unsupported } } From ef7d5828492b2b19d7f10f8269d506a1b1f32fec Mon Sep 17 00:00:00 2001 From: isaac <> Date: Fri, 29 May 2026 00:55:12 +0200 Subject: [PATCH 13/16] Update consumers for regenerated TL API Regenerated TelegramApi sources and updated the affected consumers: - InstantPage ordered-list encode: inputPageBlockOrderedList -> pageBlockOrderedList - ChatContextResult: handle new constructor (exhaustive switch) - JoinChannel/JoinLink: chatInviteJoinResultWebView now (botId, webview, users) - WebUI chat-context-result preview consumers --- submodules/TelegramApi/Sources/Api0.swift | 18 +- submodules/TelegramApi/Sources/Api11.swift | 134 ++---- submodules/TelegramApi/Sources/Api12.swift | 330 +++++++++++---- submodules/TelegramApi/Sources/Api13.swift | 400 +++++++----------- submodules/TelegramApi/Sources/Api14.swift | 234 ++++------ submodules/TelegramApi/Sources/Api15.swift | 77 ---- submodules/TelegramApi/Sources/Api19.swift | 146 ++++--- submodules/TelegramApi/Sources/Api2.swift | 49 +++ submodules/TelegramApi/Sources/Api20.swift | 88 +++- submodules/TelegramApi/Sources/Api36.swift | 34 +- submodules/TelegramApi/Sources/Api42.swift | 10 +- submodules/TelegramApi/Sources/Api6.swift | 10 +- submodules/TelegramApi/Sources/Api8.swift | 49 +++ .../Sources/ApiUtils/ChatContextResult.swift | 26 +- .../Sources/ApiUtils/InstantPage.swift | 2 +- ...OutgoingMessageWithChatContextResult.swift | 10 +- .../TelegramEngine/Peers/JoinChannel.swift | 5 +- .../TelegramEngine/Peers/JoinLink.swift | 5 +- .../WebAppMessageChatPreviewItem.swift | 10 + .../Sources/WebAppMessagePreviewScreen.swift | 8 +- 20 files changed, 849 insertions(+), 796 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ae1b5a7e68..ec029e5835 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -107,6 +107,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) } dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) } dict[-2137335386] = { return Api.BotInlineMessage.parse_botInlineMessageMediaWebPage($0) } + dict[174161531] = { return Api.BotInlineMessage.parse_botInlineMessageRichMessage($0) } dict[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) } dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) } dict[295067450] = { return Api.BotInlineResult.parse_botInlineResult($0) } @@ -259,7 +260,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) } dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) } dict[1137015880] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[-1743452271] = { return Api.DraftMessage.parse_draftMessage($0) } + dict[1627271828] = { return Api.DraftMessage.parse_draftMessage($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) } dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($0) } @@ -339,6 +340,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-672693723] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaInvoice($0) } dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) } dict[-1109605104] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaWebPage($0) } + dict[-1271007892] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageRichMessage($0) } dict[1036876423] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageText($0) } dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) } dict[-1227287081] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID64($0) } @@ -443,8 +445,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) } dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) } dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) } - dict[2105227266] = { return Api.InputPageListOrderedItem.parse_inputPageListOrderedItemBlocks($0) } - dict[-1682665696] = { return Api.InputPageListOrderedItem.parse_inputPageListOrderedItemText($0) } dict[1528613672] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialFirebasePNV($0) } dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) } dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($0) } @@ -756,11 +756,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1001931436] = { return Api.OutboxReadDate.parse_outboxReadDate($0) } dict[-1738178803] = { return Api.Page.parse_page($0) } dict[1464557951] = { return Api.PageBlock.parse_inputPageBlockMap($0) } - dict[-1186155733] = { return Api.PageBlock.parse_inputPageBlockOrderedList($0) } dict[-837994576] = { return Api.PageBlock.parse_pageBlockAnchor($0) } dict[-2143067670] = { return Api.PageBlock.parse_pageBlockAudio($0) } dict[-1162877472] = { return Api.PageBlock.parse_pageBlockAuthorDate($0) } dict[641563686] = { return Api.PageBlock.parse_pageBlockBlockquote($0) } + dict[242108356] = { return Api.PageBlock.parse_pageBlockBlockquoteBlocks($0) } dict[-283684427] = { return Api.PageBlock.parse_pageBlockChannel($0) } dict[1705048653] = { return Api.PageBlock.parse_pageBlockCollage($0) } dict[972174080] = { return Api.PageBlock.parse_pageBlockCover($0) } @@ -780,7 +780,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-454524911] = { return Api.PageBlock.parse_pageBlockList($0) } dict[-1538310410] = { return Api.PageBlock.parse_pageBlockMap($0) } dict[1493699616] = { return Api.PageBlock.parse_pageBlockMath($0) } - dict[-1702174239] = { return Api.PageBlock.parse_pageBlockOrderedList($0) } + dict[534181569] = { return Api.PageBlock.parse_pageBlockOrderedList($0) } dict[1182402406] = { return Api.PageBlock.parse_pageBlockParagraph($0) } dict[391759200] = { return Api.PageBlock.parse_pageBlockPhoto($0) } dict[-1066346178] = { return Api.PageBlock.parse_pageBlockPreformatted($0) } @@ -797,8 +797,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1869903447] = { return Api.PageCaption.parse_pageCaption($0) } dict[1674209194] = { return Api.PageListItem.parse_pageListItemBlocks($0) } dict[794323004] = { return Api.PageListItem.parse_pageListItemText($0) } - dict[1109995988] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) } - dict[-851533770] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) } + dict[-1879910928] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) } + dict[352522633] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) } dict[-1282352120] = { return Api.PageRelatedArticle.parse_pageRelatedArticle($0) } dict[878078826] = { return Api.PageTableCell.parse_pageTableCell($0) } dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) } @@ -1495,7 +1495,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) } dict[-2118733814] = { return Api.messages.ChatInviteImporters.parse_chatInviteImporters($0) } dict[1146512295] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultOk($0) } - dict[2001452532] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultWebView($0) } + dict[793887543] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultWebView($0) } dict[1694474197] = { return Api.messages.Chats.parse_chats($0) } dict[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($0) } dict[-1571952873] = { return Api.messages.CheckedHistoryImportPeer.parse_checkedHistoryImportPeer($0) } @@ -2042,8 +2042,6 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputNotifyPeer: _1.serialize(buffer, boxed) - case let _1 as Api.InputPageListOrderedItem: - _1.serialize(buffer, boxed) case let _1 as Api.InputPasskeyCredential: _1.serialize(buffer, boxed) case let _1 as Api.InputPasskeyResponse: diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 2d6b640d41..dc425a13f2 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1451,131 +1451,85 @@ public extension Api { } } public extension Api { - indirect enum InputPageListOrderedItem: TypeConstructorDescription { - public class Cons_inputPageListOrderedItemBlocks: TypeConstructorDescription { - public var flags: Int32 - public var blocks: [Api.PageBlock] - public var value: Int32? - public var type: String? - public init(flags: Int32, blocks: [Api.PageBlock], value: Int32?, type: String?) { - self.flags = flags - self.blocks = blocks - self.value = value - self.type = type + enum InputPasskeyCredential: TypeConstructorDescription { + public class Cons_inputPasskeyCredentialFirebasePNV: TypeConstructorDescription { + public var pnvToken: String + public init(pnvToken: String) { + self.pnvToken = pnvToken } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(self.flags)), ("blocks", ConstructorParameterDescription(self.blocks)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))]) + return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(self.pnvToken))]) } } - public class Cons_inputPageListOrderedItemText: TypeConstructorDescription { - public var flags: Int32 - public var text: Api.RichText - public var value: Int32? - public var type: String? - public init(flags: Int32, text: Api.RichText, value: Int32?, type: String?) { - self.flags = flags - self.text = text - self.value = value - self.type = type + public class Cons_inputPasskeyCredentialPublicKey: TypeConstructorDescription { + public var id: String + public var rawId: String + public var response: Api.InputPasskeyResponse + public init(id: String, rawId: String, response: Api.InputPasskeyResponse) { + self.id = id + self.rawId = rawId + self.response = response } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPageListOrderedItemText", [("flags", ConstructorParameterDescription(self.flags)), ("text", ConstructorParameterDescription(self.text)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))]) + return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(self.id)), ("rawId", ConstructorParameterDescription(self.rawId)), ("response", ConstructorParameterDescription(self.response))]) } } - case inputPageListOrderedItemBlocks(Cons_inputPageListOrderedItemBlocks) - case inputPageListOrderedItemText(Cons_inputPageListOrderedItemText) + case inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV) + case inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .inputPageListOrderedItemBlocks(let _data): + case .inputPasskeyCredentialFirebasePNV(let _data): if boxed { - buffer.appendInt32(2105227266) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.blocks.count)) - for item in _data.blocks { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt32(_data.value!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeString(_data.type!, buffer: buffer, boxed: false) + buffer.appendInt32(1528613672) } + serializeString(_data.pnvToken, buffer: buffer, boxed: false) break - case .inputPageListOrderedItemText(let _data): + case .inputPasskeyCredentialPublicKey(let _data): if boxed { - buffer.appendInt32(-1682665696) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - _data.text.serialize(buffer, true) - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt32(_data.value!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeString(_data.type!, buffer: buffer, boxed: false) + buffer.appendInt32(1009235855) } + serializeString(_data.id, buffer: buffer, boxed: false) + serializeString(_data.rawId, buffer: buffer, boxed: false) + _data.response.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { - case .inputPageListOrderedItemBlocks(let _data): - return ("inputPageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(_data.flags)), ("blocks", ConstructorParameterDescription(_data.blocks)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))]) - case .inputPageListOrderedItemText(let _data): - return ("inputPageListOrderedItemText", [("flags", ConstructorParameterDescription(_data.flags)), ("text", ConstructorParameterDescription(_data.text)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))]) + case .inputPasskeyCredentialFirebasePNV(let _data): + return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(_data.pnvToken))]) + case .inputPasskeyCredentialPublicKey(let _data): + return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(_data.id)), ("rawId", ConstructorParameterDescription(_data.rawId)), ("response", ConstructorParameterDescription(_data.response))]) } } - public static func parse_inputPageListOrderedItemBlocks(_ reader: BufferReader) -> InputPageListOrderedItem? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.PageBlock]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageBlock.self) - } - var _3: Int32? - if Int(_1 ?? 0) & Int(1 << 2) != 0 { - _3 = reader.readInt32() - } - var _4: String? - if Int(_1 ?? 0) & Int(1 << 3) != 0 { - _4 = parseString(reader) - } + public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? { + var _1: String? + _1 = parseString(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputPageListOrderedItem.inputPageListOrderedItemBlocks(Cons_inputPageListOrderedItemBlocks(flags: _1!, blocks: _2!, value: _3, type: _4)) + if _c1 { + return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV(pnvToken: _1!)) } else { return nil } } - public static func parse_inputPageListOrderedItemText(_ reader: BufferReader) -> InputPageListOrderedItem? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.RichText? + public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: Api.InputPasskeyResponse? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.RichText - } - var _3: Int32? - if Int(_1 ?? 0) & Int(1 << 2) != 0 { - _3 = reader.readInt32() - } - var _4: String? - if Int(_1 ?? 0) & Int(1 << 3) != 0 { - _4 = parseString(reader) + _3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputPageListOrderedItem.inputPageListOrderedItemText(Cons_inputPageListOrderedItemText(flags: _1!, text: _2!, value: _3, type: _4)) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!)) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 952b136e13..89a9c99847 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,90 +1,3 @@ -public extension Api { - enum InputPasskeyCredential: TypeConstructorDescription { - public class Cons_inputPasskeyCredentialFirebasePNV: TypeConstructorDescription { - public var pnvToken: String - public init(pnvToken: String) { - self.pnvToken = pnvToken - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(self.pnvToken))]) - } - } - public class Cons_inputPasskeyCredentialPublicKey: TypeConstructorDescription { - public var id: String - public var rawId: String - public var response: Api.InputPasskeyResponse - public init(id: String, rawId: String, response: Api.InputPasskeyResponse) { - self.id = id - self.rawId = rawId - self.response = response - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(self.id)), ("rawId", ConstructorParameterDescription(self.rawId)), ("response", ConstructorParameterDescription(self.response))]) - } - } - case inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV) - case inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPasskeyCredentialFirebasePNV(let _data): - if boxed { - buffer.appendInt32(1528613672) - } - serializeString(_data.pnvToken, buffer: buffer, boxed: false) - break - case .inputPasskeyCredentialPublicKey(let _data): - if boxed { - buffer.appendInt32(1009235855) - } - serializeString(_data.id, buffer: buffer, boxed: false) - serializeString(_data.rawId, buffer: buffer, boxed: false) - _data.response.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .inputPasskeyCredentialFirebasePNV(let _data): - return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(_data.pnvToken))]) - case .inputPasskeyCredentialPublicKey(let _data): - return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(_data.id)), ("rawId", ConstructorParameterDescription(_data.rawId)), ("response", ConstructorParameterDescription(_data.response))]) - } - } - - public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV(pnvToken: _1!)) - } - else { - return nil - } - } - public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: Api.InputPasskeyResponse? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!)) - } - else { - return nil - } - } - } -} public extension Api { enum InputPasskeyResponse: TypeConstructorDescription { public class Cons_inputPasskeyResponseLogin: TypeConstructorDescription { @@ -969,3 +882,246 @@ public extension Api { } } } +public extension Api { + enum InputPrivacyRule: TypeConstructorDescription { + public class Cons_inputPrivacyValueAllowChatParticipants: TypeConstructorDescription { + public var chats: [Int64] + public init(chats: [Int64]) { + self.chats = chats + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))]) + } + } + public class Cons_inputPrivacyValueAllowUsers: TypeConstructorDescription { + public var users: [Api.InputUser] + public init(users: [Api.InputUser]) { + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(self.users))]) + } + } + public class Cons_inputPrivacyValueDisallowChatParticipants: TypeConstructorDescription { + public var chats: [Int64] + public init(chats: [Int64]) { + self.chats = chats + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))]) + } + } + public class Cons_inputPrivacyValueDisallowUsers: TypeConstructorDescription { + public var users: [Api.InputUser] + public init(users: [Api.InputUser]) { + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(self.users))]) + } + } + case inputPrivacyValueAllowAll + case inputPrivacyValueAllowBots + case inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants) + case inputPrivacyValueAllowCloseFriends + case inputPrivacyValueAllowContacts + case inputPrivacyValueAllowPremium + case inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers) + case inputPrivacyValueDisallowAll + case inputPrivacyValueDisallowBots + case inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants) + case inputPrivacyValueDisallowContacts + case inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPrivacyValueAllowAll: + if boxed { + buffer.appendInt32(407582158) + } + break + case .inputPrivacyValueAllowBots: + if boxed { + buffer.appendInt32(1515179237) + } + break + case .inputPrivacyValueAllowChatParticipants(let _data): + if boxed { + buffer.appendInt32(-2079962673) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .inputPrivacyValueAllowCloseFriends: + if boxed { + buffer.appendInt32(793067081) + } + break + case .inputPrivacyValueAllowContacts: + if boxed { + buffer.appendInt32(218751099) + } + break + case .inputPrivacyValueAllowPremium: + if boxed { + buffer.appendInt32(2009975281) + } + break + case .inputPrivacyValueAllowUsers(let _data): + if boxed { + buffer.appendInt32(320652927) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .inputPrivacyValueDisallowAll: + if boxed { + buffer.appendInt32(-697604407) + } + break + case .inputPrivacyValueDisallowBots: + if boxed { + buffer.appendInt32(-991594219) + } + break + case .inputPrivacyValueDisallowChatParticipants(let _data): + if boxed { + buffer.appendInt32(-380694650) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .inputPrivacyValueDisallowContacts: + if boxed { + buffer.appendInt32(195371015) + } + break + case .inputPrivacyValueDisallowUsers(let _data): + if boxed { + buffer.appendInt32(-1877932953) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .inputPrivacyValueAllowAll: + return ("inputPrivacyValueAllowAll", []) + case .inputPrivacyValueAllowBots: + return ("inputPrivacyValueAllowBots", []) + case .inputPrivacyValueAllowChatParticipants(let _data): + return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))]) + case .inputPrivacyValueAllowCloseFriends: + return ("inputPrivacyValueAllowCloseFriends", []) + case .inputPrivacyValueAllowContacts: + return ("inputPrivacyValueAllowContacts", []) + case .inputPrivacyValueAllowPremium: + return ("inputPrivacyValueAllowPremium", []) + case .inputPrivacyValueAllowUsers(let _data): + return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(_data.users))]) + case .inputPrivacyValueDisallowAll: + return ("inputPrivacyValueDisallowAll", []) + case .inputPrivacyValueDisallowBots: + return ("inputPrivacyValueDisallowBots", []) + case .inputPrivacyValueDisallowChatParticipants(let _data): + return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))]) + case .inputPrivacyValueDisallowContacts: + return ("inputPrivacyValueDisallowContacts", []) + case .inputPrivacyValueDisallowUsers(let _data): + return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_inputPrivacyValueAllowAll(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowAll + } + public static func parse_inputPrivacyValueAllowBots(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowBots + } + public static func parse_inputPrivacyValueAllowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants(chats: _1!)) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends + } + public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowContacts + } + public static func parse_inputPrivacyValueAllowPremium(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowPremium + } + public static func parse_inputPrivacyValueAllowUsers(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Api.InputUser]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers(users: _1!)) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueDisallowAll(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueDisallowAll + } + public static func parse_inputPrivacyValueDisallowBots(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueDisallowBots + } + public static func parse_inputPrivacyValueDisallowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants(chats: _1!)) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueDisallowContacts(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueDisallowContacts + } + public static func parse_inputPrivacyValueDisallowUsers(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Api.InputUser]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers(users: _1!)) + } + else { + return nil + } + } + } +} diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index d4296b5adb..2373270d99 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -1,246 +1,3 @@ -public extension Api { - enum InputPrivacyRule: TypeConstructorDescription { - public class Cons_inputPrivacyValueAllowChatParticipants: TypeConstructorDescription { - public var chats: [Int64] - public init(chats: [Int64]) { - self.chats = chats - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))]) - } - } - public class Cons_inputPrivacyValueAllowUsers: TypeConstructorDescription { - public var users: [Api.InputUser] - public init(users: [Api.InputUser]) { - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(self.users))]) - } - } - public class Cons_inputPrivacyValueDisallowChatParticipants: TypeConstructorDescription { - public var chats: [Int64] - public init(chats: [Int64]) { - self.chats = chats - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))]) - } - } - public class Cons_inputPrivacyValueDisallowUsers: TypeConstructorDescription { - public var users: [Api.InputUser] - public init(users: [Api.InputUser]) { - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(self.users))]) - } - } - case inputPrivacyValueAllowAll - case inputPrivacyValueAllowBots - case inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants) - case inputPrivacyValueAllowCloseFriends - case inputPrivacyValueAllowContacts - case inputPrivacyValueAllowPremium - case inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers) - case inputPrivacyValueDisallowAll - case inputPrivacyValueDisallowBots - case inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants) - case inputPrivacyValueDisallowContacts - case inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPrivacyValueAllowAll: - if boxed { - buffer.appendInt32(407582158) - } - break - case .inputPrivacyValueAllowBots: - if boxed { - buffer.appendInt32(1515179237) - } - break - case .inputPrivacyValueAllowChatParticipants(let _data): - if boxed { - buffer.appendInt32(-2079962673) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .inputPrivacyValueAllowCloseFriends: - if boxed { - buffer.appendInt32(793067081) - } - break - case .inputPrivacyValueAllowContacts: - if boxed { - buffer.appendInt32(218751099) - } - break - case .inputPrivacyValueAllowPremium: - if boxed { - buffer.appendInt32(2009975281) - } - break - case .inputPrivacyValueAllowUsers(let _data): - if boxed { - buffer.appendInt32(320652927) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .inputPrivacyValueDisallowAll: - if boxed { - buffer.appendInt32(-697604407) - } - break - case .inputPrivacyValueDisallowBots: - if boxed { - buffer.appendInt32(-991594219) - } - break - case .inputPrivacyValueDisallowChatParticipants(let _data): - if boxed { - buffer.appendInt32(-380694650) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .inputPrivacyValueDisallowContacts: - if boxed { - buffer.appendInt32(195371015) - } - break - case .inputPrivacyValueDisallowUsers(let _data): - if boxed { - buffer.appendInt32(-1877932953) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .inputPrivacyValueAllowAll: - return ("inputPrivacyValueAllowAll", []) - case .inputPrivacyValueAllowBots: - return ("inputPrivacyValueAllowBots", []) - case .inputPrivacyValueAllowChatParticipants(let _data): - return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))]) - case .inputPrivacyValueAllowCloseFriends: - return ("inputPrivacyValueAllowCloseFriends", []) - case .inputPrivacyValueAllowContacts: - return ("inputPrivacyValueAllowContacts", []) - case .inputPrivacyValueAllowPremium: - return ("inputPrivacyValueAllowPremium", []) - case .inputPrivacyValueAllowUsers(let _data): - return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(_data.users))]) - case .inputPrivacyValueDisallowAll: - return ("inputPrivacyValueDisallowAll", []) - case .inputPrivacyValueDisallowBots: - return ("inputPrivacyValueDisallowBots", []) - case .inputPrivacyValueDisallowChatParticipants(let _data): - return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))]) - case .inputPrivacyValueDisallowContacts: - return ("inputPrivacyValueDisallowContacts", []) - case .inputPrivacyValueDisallowUsers(let _data): - return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_inputPrivacyValueAllowAll(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowAll - } - public static func parse_inputPrivacyValueAllowBots(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowBots - } - public static func parse_inputPrivacyValueAllowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants(chats: _1!)) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends - } - public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowContacts - } - public static func parse_inputPrivacyValueAllowPremium(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowPremium - } - public static func parse_inputPrivacyValueAllowUsers(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Api.InputUser]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers(users: _1!)) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueDisallowAll(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueDisallowAll - } - public static func parse_inputPrivacyValueDisallowBots(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueDisallowBots - } - public static func parse_inputPrivacyValueDisallowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants(chats: _1!)) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueDisallowContacts(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueDisallowContacts - } - public static func parse_inputPrivacyValueDisallowUsers(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Api.InputUser]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers(users: _1!)) - } - else { - return nil - } - } - } -} public extension Api { enum InputQuickReplyShortcut: TypeConstructorDescription { public class Cons_inputQuickReplyShortcut: TypeConstructorDescription { @@ -1559,3 +1316,160 @@ public extension Api { } } } +public extension Api { + enum InputStickerSetItem: TypeConstructorDescription { + public class Cons_inputStickerSetItem: TypeConstructorDescription { + public var flags: Int32 + public var document: Api.InputDocument + public var emoji: String + public var maskCoords: Api.MaskCoords? + public var keywords: String? + public init(flags: Int32, document: Api.InputDocument, emoji: String, maskCoords: Api.MaskCoords?, keywords: String?) { + self.flags = flags + self.document = document + self.emoji = emoji + self.maskCoords = maskCoords + self.keywords = keywords + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputStickerSetItem", [("flags", ConstructorParameterDescription(self.flags)), ("document", ConstructorParameterDescription(self.document)), ("emoji", ConstructorParameterDescription(self.emoji)), ("maskCoords", ConstructorParameterDescription(self.maskCoords)), ("keywords", ConstructorParameterDescription(self.keywords))]) + } + } + case inputStickerSetItem(Cons_inputStickerSetItem) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetItem(let _data): + if boxed { + buffer.appendInt32(853188252) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + _data.document.serialize(buffer, true) + serializeString(_data.emoji, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + _data.maskCoords!.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeString(_data.keywords!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .inputStickerSetItem(let _data): + return ("inputStickerSetItem", [("flags", ConstructorParameterDescription(_data.flags)), ("document", ConstructorParameterDescription(_data.document)), ("emoji", ConstructorParameterDescription(_data.emoji)), ("maskCoords", ConstructorParameterDescription(_data.maskCoords)), ("keywords", ConstructorParameterDescription(_data.keywords))]) + } + } + + public static func parse_inputStickerSetItem(_ reader: BufferReader) -> InputStickerSetItem? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputDocument? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputDocument + } + var _3: String? + _3 = parseString(reader) + var _4: Api.MaskCoords? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.MaskCoords + } + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _5 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputStickerSetItem.inputStickerSetItem(Cons_inputStickerSetItem(flags: _1!, document: _2!, emoji: _3!, maskCoords: _4, keywords: _5)) + } + else { + return nil + } + } + } +} +public extension Api { + enum InputStickeredMedia: TypeConstructorDescription { + public class Cons_inputStickeredMediaDocument: TypeConstructorDescription { + public var id: Api.InputDocument + public init(id: Api.InputDocument) { + self.id = id + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputStickeredMediaDocument", [("id", ConstructorParameterDescription(self.id))]) + } + } + public class Cons_inputStickeredMediaPhoto: TypeConstructorDescription { + public var id: Api.InputPhoto + public init(id: Api.InputPhoto) { + self.id = id + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputStickeredMediaPhoto", [("id", ConstructorParameterDescription(self.id))]) + } + } + case inputStickeredMediaDocument(Cons_inputStickeredMediaDocument) + case inputStickeredMediaPhoto(Cons_inputStickeredMediaPhoto) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickeredMediaDocument(let _data): + if boxed { + buffer.appendInt32(70813275) + } + _data.id.serialize(buffer, true) + break + case .inputStickeredMediaPhoto(let _data): + if boxed { + buffer.appendInt32(1251549527) + } + _data.id.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .inputStickeredMediaDocument(let _data): + return ("inputStickeredMediaDocument", [("id", ConstructorParameterDescription(_data.id))]) + case .inputStickeredMediaPhoto(let _data): + return ("inputStickeredMediaPhoto", [("id", ConstructorParameterDescription(_data.id))]) + } + } + + public static func parse_inputStickeredMediaDocument(_ reader: BufferReader) -> InputStickeredMedia? { + var _1: Api.InputDocument? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputDocument + } + let _c1 = _1 != nil + if _c1 { + return Api.InputStickeredMedia.inputStickeredMediaDocument(Cons_inputStickeredMediaDocument(id: _1!)) + } + else { + return nil + } + } + public static func parse_inputStickeredMediaPhoto(_ reader: BufferReader) -> InputStickeredMedia? { + var _1: Api.InputPhoto? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPhoto + } + let _c1 = _1 != nil + if _c1 { + return Api.InputStickeredMedia.inputStickeredMediaPhoto(Cons_inputStickeredMediaPhoto(id: _1!)) + } + else { + return nil + } + } + } +} diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 547478f81f..21323b5388 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -1,160 +1,3 @@ -public extension Api { - enum InputStickerSetItem: TypeConstructorDescription { - public class Cons_inputStickerSetItem: TypeConstructorDescription { - public var flags: Int32 - public var document: Api.InputDocument - public var emoji: String - public var maskCoords: Api.MaskCoords? - public var keywords: String? - public init(flags: Int32, document: Api.InputDocument, emoji: String, maskCoords: Api.MaskCoords?, keywords: String?) { - self.flags = flags - self.document = document - self.emoji = emoji - self.maskCoords = maskCoords - self.keywords = keywords - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputStickerSetItem", [("flags", ConstructorParameterDescription(self.flags)), ("document", ConstructorParameterDescription(self.document)), ("emoji", ConstructorParameterDescription(self.emoji)), ("maskCoords", ConstructorParameterDescription(self.maskCoords)), ("keywords", ConstructorParameterDescription(self.keywords))]) - } - } - case inputStickerSetItem(Cons_inputStickerSetItem) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickerSetItem(let _data): - if boxed { - buffer.appendInt32(853188252) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - _data.document.serialize(buffer, true) - serializeString(_data.emoji, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - _data.maskCoords!.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeString(_data.keywords!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .inputStickerSetItem(let _data): - return ("inputStickerSetItem", [("flags", ConstructorParameterDescription(_data.flags)), ("document", ConstructorParameterDescription(_data.document)), ("emoji", ConstructorParameterDescription(_data.emoji)), ("maskCoords", ConstructorParameterDescription(_data.maskCoords)), ("keywords", ConstructorParameterDescription(_data.keywords))]) - } - } - - public static func parse_inputStickerSetItem(_ reader: BufferReader) -> InputStickerSetItem? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputDocument? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputDocument - } - var _3: String? - _3 = parseString(reader) - var _4: Api.MaskCoords? - if Int(_1 ?? 0) & Int(1 << 0) != 0 { - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.MaskCoords - } - } - var _5: String? - if Int(_1 ?? 0) & Int(1 << 1) != 0 { - _5 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputStickerSetItem.inputStickerSetItem(Cons_inputStickerSetItem(flags: _1!, document: _2!, emoji: _3!, maskCoords: _4, keywords: _5)) - } - else { - return nil - } - } - } -} -public extension Api { - enum InputStickeredMedia: TypeConstructorDescription { - public class Cons_inputStickeredMediaDocument: TypeConstructorDescription { - public var id: Api.InputDocument - public init(id: Api.InputDocument) { - self.id = id - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputStickeredMediaDocument", [("id", ConstructorParameterDescription(self.id))]) - } - } - public class Cons_inputStickeredMediaPhoto: TypeConstructorDescription { - public var id: Api.InputPhoto - public init(id: Api.InputPhoto) { - self.id = id - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputStickeredMediaPhoto", [("id", ConstructorParameterDescription(self.id))]) - } - } - case inputStickeredMediaDocument(Cons_inputStickeredMediaDocument) - case inputStickeredMediaPhoto(Cons_inputStickeredMediaPhoto) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickeredMediaDocument(let _data): - if boxed { - buffer.appendInt32(70813275) - } - _data.id.serialize(buffer, true) - break - case .inputStickeredMediaPhoto(let _data): - if boxed { - buffer.appendInt32(1251549527) - } - _data.id.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .inputStickeredMediaDocument(let _data): - return ("inputStickeredMediaDocument", [("id", ConstructorParameterDescription(_data.id))]) - case .inputStickeredMediaPhoto(let _data): - return ("inputStickeredMediaPhoto", [("id", ConstructorParameterDescription(_data.id))]) - } - } - - public static func parse_inputStickeredMediaDocument(_ reader: BufferReader) -> InputStickeredMedia? { - var _1: Api.InputDocument? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputDocument - } - let _c1 = _1 != nil - if _c1 { - return Api.InputStickeredMedia.inputStickeredMediaDocument(Cons_inputStickeredMediaDocument(id: _1!)) - } - else { - return nil - } - } - public static func parse_inputStickeredMediaPhoto(_ reader: BufferReader) -> InputStickeredMedia? { - var _1: Api.InputPhoto? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPhoto - } - let _c1 = _1 != nil - if _c1 { - return Api.InputStickeredMedia.inputStickeredMediaPhoto(Cons_inputStickeredMediaPhoto(id: _1!)) - } - else { - return nil - } - } - } -} public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { public class Cons_inputStorePaymentAuthCode: TypeConstructorDescription { @@ -1699,3 +1542,80 @@ public extension Api { } } } +public extension Api { + enum JoinChatBotResult: TypeConstructorDescription { + public class Cons_joinChatBotResultWebView: TypeConstructorDescription { + public var url: String + public init(url: String) { + self.url = url + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("joinChatBotResultWebView", [("url", ConstructorParameterDescription(self.url))]) + } + } + case joinChatBotResultApproved + case joinChatBotResultDeclined + case joinChatBotResultQueued + case joinChatBotResultWebView(Cons_joinChatBotResultWebView) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .joinChatBotResultApproved: + if boxed { + buffer.appendInt32(-1374344599) + } + break + case .joinChatBotResultDeclined: + if boxed { + buffer.appendInt32(251265428) + } + break + case .joinChatBotResultQueued: + if boxed { + buffer.appendInt32(-1734105024) + } + break + case .joinChatBotResultWebView(let _data): + if boxed { + buffer.appendInt32(-689719277) + } + serializeString(_data.url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .joinChatBotResultApproved: + return ("joinChatBotResultApproved", []) + case .joinChatBotResultDeclined: + return ("joinChatBotResultDeclined", []) + case .joinChatBotResultQueued: + return ("joinChatBotResultQueued", []) + case .joinChatBotResultWebView(let _data): + return ("joinChatBotResultWebView", [("url", ConstructorParameterDescription(_data.url))]) + } + } + + public static func parse_joinChatBotResultApproved(_ reader: BufferReader) -> JoinChatBotResult? { + return Api.JoinChatBotResult.joinChatBotResultApproved + } + public static func parse_joinChatBotResultDeclined(_ reader: BufferReader) -> JoinChatBotResult? { + return Api.JoinChatBotResult.joinChatBotResultDeclined + } + public static func parse_joinChatBotResultQueued(_ reader: BufferReader) -> JoinChatBotResult? { + return Api.JoinChatBotResult.joinChatBotResultQueued + } + public static func parse_joinChatBotResultWebView(_ reader: BufferReader) -> JoinChatBotResult? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.JoinChatBotResult.joinChatBotResultWebView(Cons_joinChatBotResultWebView(url: _1!)) + } + else { + return nil + } + } + } +} diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index a7fab8381c..7df4305b1c 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -1,80 +1,3 @@ -public extension Api { - enum JoinChatBotResult: TypeConstructorDescription { - public class Cons_joinChatBotResultWebView: TypeConstructorDescription { - public var url: String - public init(url: String) { - self.url = url - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("joinChatBotResultWebView", [("url", ConstructorParameterDescription(self.url))]) - } - } - case joinChatBotResultApproved - case joinChatBotResultDeclined - case joinChatBotResultQueued - case joinChatBotResultWebView(Cons_joinChatBotResultWebView) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .joinChatBotResultApproved: - if boxed { - buffer.appendInt32(-1374344599) - } - break - case .joinChatBotResultDeclined: - if boxed { - buffer.appendInt32(251265428) - } - break - case .joinChatBotResultQueued: - if boxed { - buffer.appendInt32(-1734105024) - } - break - case .joinChatBotResultWebView(let _data): - if boxed { - buffer.appendInt32(-689719277) - } - serializeString(_data.url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .joinChatBotResultApproved: - return ("joinChatBotResultApproved", []) - case .joinChatBotResultDeclined: - return ("joinChatBotResultDeclined", []) - case .joinChatBotResultQueued: - return ("joinChatBotResultQueued", []) - case .joinChatBotResultWebView(let _data): - return ("joinChatBotResultWebView", [("url", ConstructorParameterDescription(_data.url))]) - } - } - - public static func parse_joinChatBotResultApproved(_ reader: BufferReader) -> JoinChatBotResult? { - return Api.JoinChatBotResult.joinChatBotResultApproved - } - public static func parse_joinChatBotResultDeclined(_ reader: BufferReader) -> JoinChatBotResult? { - return Api.JoinChatBotResult.joinChatBotResultDeclined - } - public static func parse_joinChatBotResultQueued(_ reader: BufferReader) -> JoinChatBotResult? { - return Api.JoinChatBotResult.joinChatBotResultQueued - } - public static func parse_joinChatBotResultWebView(_ reader: BufferReader) -> JoinChatBotResult? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.JoinChatBotResult.joinChatBotResultWebView(Cons_joinChatBotResultWebView(url: _1!)) - } - else { - return nil - } - } - } -} public extension Api { indirect enum KeyboardButton: TypeConstructorDescription { public class Cons_inputKeyboardButtonRequestPeer: TypeConstructorDescription { diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift index 542180c858..9134cda518 100644 --- a/submodules/TelegramApi/Sources/Api19.swift +++ b/submodules/TelegramApi/Sources/Api19.swift @@ -430,21 +430,6 @@ public extension Api { return ("inputPageBlockMap", [("geo", ConstructorParameterDescription(self.geo)), ("zoom", ConstructorParameterDescription(self.zoom)), ("w", ConstructorParameterDescription(self.w)), ("h", ConstructorParameterDescription(self.h)), ("caption", ConstructorParameterDescription(self.caption))]) } } - public class Cons_inputPageBlockOrderedList: TypeConstructorDescription { - public var flags: Int32 - public var items: [Api.InputPageListOrderedItem] - public var start: Int32? - public var type: String? - public init(flags: Int32, items: [Api.InputPageListOrderedItem], start: Int32?, type: String?) { - self.flags = flags - self.items = items - self.start = start - self.type = type - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputPageBlockOrderedList", [("flags", ConstructorParameterDescription(self.flags)), ("items", ConstructorParameterDescription(self.items)), ("start", ConstructorParameterDescription(self.start)), ("type", ConstructorParameterDescription(self.type))]) - } - } public class Cons_pageBlockAnchor: TypeConstructorDescription { public var name: String public init(name: String) { @@ -487,6 +472,17 @@ public extension Api { return ("pageBlockBlockquote", [("text", ConstructorParameterDescription(self.text)), ("caption", ConstructorParameterDescription(self.caption))]) } } + public class Cons_pageBlockBlockquoteBlocks: TypeConstructorDescription { + public var blocks: [Api.PageBlock] + public var caption: Api.RichText + public init(blocks: [Api.PageBlock], caption: Api.RichText) { + self.blocks = blocks + self.caption = caption + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("pageBlockBlockquoteBlocks", [("blocks", ConstructorParameterDescription(self.blocks)), ("caption", ConstructorParameterDescription(self.caption))]) + } + } public class Cons_pageBlockChannel: TypeConstructorDescription { public var channel: Api.Chat public init(channel: Api.Chat) { @@ -688,12 +684,18 @@ public extension Api { } } public class Cons_pageBlockOrderedList: TypeConstructorDescription { + public var flags: Int32 public var items: [Api.PageListOrderedItem] - public init(items: [Api.PageListOrderedItem]) { + public var start: Int32? + public var type: String? + public init(flags: Int32, items: [Api.PageListOrderedItem], start: Int32?, type: String?) { + self.flags = flags self.items = items + self.start = start + self.type = type } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("pageBlockOrderedList", [("items", ConstructorParameterDescription(self.items))]) + return ("pageBlockOrderedList", [("flags", ConstructorParameterDescription(self.flags)), ("items", ConstructorParameterDescription(self.items)), ("start", ConstructorParameterDescription(self.start)), ("type", ConstructorParameterDescription(self.type))]) } } public class Cons_pageBlockParagraph: TypeConstructorDescription { @@ -829,11 +831,11 @@ public extension Api { } } case inputPageBlockMap(Cons_inputPageBlockMap) - case inputPageBlockOrderedList(Cons_inputPageBlockOrderedList) case pageBlockAnchor(Cons_pageBlockAnchor) case pageBlockAudio(Cons_pageBlockAudio) case pageBlockAuthorDate(Cons_pageBlockAuthorDate) case pageBlockBlockquote(Cons_pageBlockBlockquote) + case pageBlockBlockquoteBlocks(Cons_pageBlockBlockquoteBlocks) case pageBlockChannel(Cons_pageBlockChannel) case pageBlockCollage(Cons_pageBlockCollage) case pageBlockCover(Cons_pageBlockCover) @@ -880,23 +882,6 @@ public extension Api { serializeInt32(_data.h, buffer: buffer, boxed: false) _data.caption.serialize(buffer, true) break - case .inputPageBlockOrderedList(let _data): - if boxed { - buffer.appendInt32(-1186155733) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.items.count)) - for item in _data.items { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt32(_data.start!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeString(_data.type!, buffer: buffer, boxed: false) - } - break case .pageBlockAnchor(let _data): if boxed { buffer.appendInt32(-837994576) @@ -924,6 +909,17 @@ public extension Api { _data.text.serialize(buffer, true) _data.caption.serialize(buffer, true) break + case .pageBlockBlockquoteBlocks(let _data): + if boxed { + buffer.appendInt32(242108356) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.blocks.count)) + for item in _data.blocks { + item.serialize(buffer, true) + } + _data.caption.serialize(buffer, true) + break case .pageBlockChannel(let _data): if boxed { buffer.appendInt32(-283684427) @@ -1084,13 +1080,20 @@ public extension Api { break case .pageBlockOrderedList(let _data): if boxed { - buffer.appendInt32(-1702174239) + buffer.appendInt32(534181569) } + serializeInt32(_data.flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(_data.items.count)) for item in _data.items { item.serialize(buffer, true) } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt32(_data.start!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeString(_data.type!, buffer: buffer, boxed: false) + } break case .pageBlockParagraph(let _data): if boxed { @@ -1204,8 +1207,6 @@ public extension Api { switch self { case .inputPageBlockMap(let _data): return ("inputPageBlockMap", [("geo", ConstructorParameterDescription(_data.geo)), ("zoom", ConstructorParameterDescription(_data.zoom)), ("w", ConstructorParameterDescription(_data.w)), ("h", ConstructorParameterDescription(_data.h)), ("caption", ConstructorParameterDescription(_data.caption))]) - case .inputPageBlockOrderedList(let _data): - return ("inputPageBlockOrderedList", [("flags", ConstructorParameterDescription(_data.flags)), ("items", ConstructorParameterDescription(_data.items)), ("start", ConstructorParameterDescription(_data.start)), ("type", ConstructorParameterDescription(_data.type))]) case .pageBlockAnchor(let _data): return ("pageBlockAnchor", [("name", ConstructorParameterDescription(_data.name))]) case .pageBlockAudio(let _data): @@ -1214,6 +1215,8 @@ public extension Api { return ("pageBlockAuthorDate", [("author", ConstructorParameterDescription(_data.author)), ("publishedDate", ConstructorParameterDescription(_data.publishedDate))]) case .pageBlockBlockquote(let _data): return ("pageBlockBlockquote", [("text", ConstructorParameterDescription(_data.text)), ("caption", ConstructorParameterDescription(_data.caption))]) + case .pageBlockBlockquoteBlocks(let _data): + return ("pageBlockBlockquoteBlocks", [("blocks", ConstructorParameterDescription(_data.blocks)), ("caption", ConstructorParameterDescription(_data.caption))]) case .pageBlockChannel(let _data): return ("pageBlockChannel", [("channel", ConstructorParameterDescription(_data.channel))]) case .pageBlockCollage(let _data): @@ -1253,7 +1256,7 @@ public extension Api { case .pageBlockMath(let _data): return ("pageBlockMath", [("source", ConstructorParameterDescription(_data.source))]) case .pageBlockOrderedList(let _data): - return ("pageBlockOrderedList", [("items", ConstructorParameterDescription(_data.items))]) + return ("pageBlockOrderedList", [("flags", ConstructorParameterDescription(_data.flags)), ("items", ConstructorParameterDescription(_data.items)), ("start", ConstructorParameterDescription(_data.start)), ("type", ConstructorParameterDescription(_data.type))]) case .pageBlockParagraph(let _data): return ("pageBlockParagraph", [("text", ConstructorParameterDescription(_data.text))]) case .pageBlockPhoto(let _data): @@ -1310,32 +1313,6 @@ public extension Api { return nil } } - public static func parse_inputPageBlockOrderedList(_ reader: BufferReader) -> PageBlock? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.InputPageListOrderedItem]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPageListOrderedItem.self) - } - var _3: Int32? - if Int(_1 ?? 0) & Int(1 << 0) != 0 { - _3 = reader.readInt32() - } - var _4: String? - if Int(_1 ?? 0) & Int(1 << 1) != 0 { - _4 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.PageBlock.inputPageBlockOrderedList(Cons_inputPageBlockOrderedList(flags: _1!, items: _2!, start: _3, type: _4)) - } - else { - return nil - } - } public static func parse_pageBlockAnchor(_ reader: BufferReader) -> PageBlock? { var _1: String? _1 = parseString(reader) @@ -1397,6 +1374,24 @@ public extension Api { return nil } } + public static func parse_pageBlockBlockquoteBlocks(_ reader: BufferReader) -> PageBlock? { + var _1: [Api.PageBlock]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageBlock.self) + } + var _2: Api.RichText? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.RichText + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.PageBlock.pageBlockBlockquoteBlocks(Cons_pageBlockBlockquoteBlocks(blocks: _1!, caption: _2!)) + } + else { + return nil + } + } public static func parse_pageBlockChannel(_ reader: BufferReader) -> PageBlock? { var _1: Api.Chat? if let signature = reader.readInt32() { @@ -1708,13 +1703,26 @@ public extension Api { } } public static func parse_pageBlockOrderedList(_ reader: BufferReader) -> PageBlock? { - var _1: [Api.PageListOrderedItem]? + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.PageListOrderedItem]? if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageListOrderedItem.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageListOrderedItem.self) + } + var _3: Int32? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = reader.readInt32() + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) } let _c1 = _1 != nil - if _c1 { - return Api.PageBlock.pageBlockOrderedList(Cons_pageBlockOrderedList(items: _1!)) + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.PageBlock.pageBlockOrderedList(Cons_pageBlockOrderedList(flags: _1!, items: _2!, start: _3, type: _4)) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index d6f92cf706..2bec29fc7d 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -759,6 +759,19 @@ public extension Api { return ("botInlineMessageMediaWebPage", [("flags", ConstructorParameterDescription(self.flags)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("url", ConstructorParameterDescription(self.url)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup))]) } } + public class Cons_botInlineMessageRichMessage: TypeConstructorDescription { + public var flags: Int32 + public var replyMarkup: Api.ReplyMarkup? + public var richMessage: Api.RichMessage + public init(flags: Int32, replyMarkup: Api.ReplyMarkup?, richMessage: Api.RichMessage) { + self.flags = flags + self.replyMarkup = replyMarkup + self.richMessage = richMessage + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("botInlineMessageRichMessage", [("flags", ConstructorParameterDescription(self.flags)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup)), ("richMessage", ConstructorParameterDescription(self.richMessage))]) + } + } public class Cons_botInlineMessageText: TypeConstructorDescription { public var flags: Int32 public var message: String @@ -780,6 +793,7 @@ public extension Api { case botInlineMessageMediaInvoice(Cons_botInlineMessageMediaInvoice) case botInlineMessageMediaVenue(Cons_botInlineMessageMediaVenue) case botInlineMessageMediaWebPage(Cons_botInlineMessageMediaWebPage) + case botInlineMessageRichMessage(Cons_botInlineMessageRichMessage) case botInlineMessageText(Cons_botInlineMessageText) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -882,6 +896,16 @@ public extension Api { _data.replyMarkup!.serialize(buffer, true) } break + case .botInlineMessageRichMessage(let _data): + if boxed { + buffer.appendInt32(174161531) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 2) != 0 { + _data.replyMarkup!.serialize(buffer, true) + } + _data.richMessage.serialize(buffer, true) + break case .botInlineMessageText(let _data): if boxed { buffer.appendInt32(-1937807902) @@ -916,6 +940,8 @@ public extension Api { return ("botInlineMessageMediaVenue", [("flags", ConstructorParameterDescription(_data.flags)), ("geo", ConstructorParameterDescription(_data.geo)), ("title", ConstructorParameterDescription(_data.title)), ("address", ConstructorParameterDescription(_data.address)), ("provider", ConstructorParameterDescription(_data.provider)), ("venueId", ConstructorParameterDescription(_data.venueId)), ("venueType", ConstructorParameterDescription(_data.venueType)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) case .botInlineMessageMediaWebPage(let _data): return ("botInlineMessageMediaWebPage", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("url", ConstructorParameterDescription(_data.url)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) + case .botInlineMessageRichMessage(let _data): + return ("botInlineMessageRichMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup)), ("richMessage", ConstructorParameterDescription(_data.richMessage))]) case .botInlineMessageText(let _data): return ("botInlineMessageText", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) } @@ -1123,6 +1149,29 @@ public extension Api { return nil } } + public static func parse_botInlineMessageRichMessage(_ reader: BufferReader) -> BotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.ReplyMarkup? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } + } + var _3: Api.RichMessage? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.RichMessage + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BotInlineMessage.botInlineMessageRichMessage(Cons_botInlineMessageRichMessage(flags: _1!, replyMarkup: _2, richMessage: _3!)) + } + else { + return nil + } + } public static func parse_botInlineMessageText(_ reader: BufferReader) -> BotInlineMessage? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index d47874f004..15df72e4d0 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -149,28 +149,36 @@ public extension Api { indirect enum PageListOrderedItem: TypeConstructorDescription { public class Cons_pageListOrderedItemBlocks: TypeConstructorDescription { public var flags: Int32 - public var num: String + public var num: String? public var blocks: [Api.PageBlock] - public init(flags: Int32, num: String, blocks: [Api.PageBlock]) { + public var value: Int32? + public var type: String? + public init(flags: Int32, num: String?, blocks: [Api.PageBlock], value: Int32?, type: String?) { self.flags = flags self.num = num self.blocks = blocks + self.value = value + self.type = type } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("pageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(self.flags)), ("num", ConstructorParameterDescription(self.num)), ("blocks", ConstructorParameterDescription(self.blocks))]) + return ("pageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(self.flags)), ("num", ConstructorParameterDescription(self.num)), ("blocks", ConstructorParameterDescription(self.blocks)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))]) } } public class Cons_pageListOrderedItemText: TypeConstructorDescription { public var flags: Int32 - public var num: String + public var num: String? public var text: Api.RichText - public init(flags: Int32, num: String, text: Api.RichText) { + public var value: Int32? + public var type: String? + public init(flags: Int32, num: String?, text: Api.RichText, value: Int32?, type: String?) { self.flags = flags self.num = num self.text = text + self.value = value + self.type = type } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("pageListOrderedItemText", [("flags", ConstructorParameterDescription(self.flags)), ("num", ConstructorParameterDescription(self.num)), ("text", ConstructorParameterDescription(self.text))]) + return ("pageListOrderedItemText", [("flags", ConstructorParameterDescription(self.flags)), ("num", ConstructorParameterDescription(self.num)), ("text", ConstructorParameterDescription(self.text)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))]) } } case pageListOrderedItemBlocks(Cons_pageListOrderedItemBlocks) @@ -180,23 +188,39 @@ public extension Api { switch self { case .pageListOrderedItemBlocks(let _data): if boxed { - buffer.appendInt32(1109995988) + buffer.appendInt32(-1879910928) } serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.num, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeString(_data.num!, buffer: buffer, boxed: false) + } buffer.appendInt32(481674261) buffer.appendInt32(Int32(_data.blocks.count)) for item in _data.blocks { item.serialize(buffer, true) } + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt32(_data.value!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeString(_data.type!, buffer: buffer, boxed: false) + } break case .pageListOrderedItemText(let _data): if boxed { - buffer.appendInt32(-851533770) + buffer.appendInt32(352522633) } serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.num, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeString(_data.num!, buffer: buffer, boxed: false) + } _data.text.serialize(buffer, true) + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt32(_data.value!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeString(_data.type!, buffer: buffer, boxed: false) + } break } } @@ -204,9 +228,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { case .pageListOrderedItemBlocks(let _data): - return ("pageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(_data.flags)), ("num", ConstructorParameterDescription(_data.num)), ("blocks", ConstructorParameterDescription(_data.blocks))]) + return ("pageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(_data.flags)), ("num", ConstructorParameterDescription(_data.num)), ("blocks", ConstructorParameterDescription(_data.blocks)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))]) case .pageListOrderedItemText(let _data): - return ("pageListOrderedItemText", [("flags", ConstructorParameterDescription(_data.flags)), ("num", ConstructorParameterDescription(_data.num)), ("text", ConstructorParameterDescription(_data.text))]) + return ("pageListOrderedItemText", [("flags", ConstructorParameterDescription(_data.flags)), ("num", ConstructorParameterDescription(_data.num)), ("text", ConstructorParameterDescription(_data.text)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))]) } } @@ -214,16 +238,28 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - _2 = parseString(reader) + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _2 = parseString(reader) + } var _3: [Api.PageBlock]? if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageBlock.self) } + var _4: Int32? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _4 = reader.readInt32() + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _5 = parseString(reader) + } let _c1 = _1 != nil - let _c2 = _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.PageListOrderedItem.pageListOrderedItemBlocks(Cons_pageListOrderedItemBlocks(flags: _1!, num: _2!, blocks: _3!)) + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.PageListOrderedItem.pageListOrderedItemBlocks(Cons_pageListOrderedItemBlocks(flags: _1!, num: _2, blocks: _3!, value: _4, type: _5)) } else { return nil @@ -233,16 +269,28 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - _2 = parseString(reader) + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _2 = parseString(reader) + } var _3: Api.RichText? if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.RichText } + var _4: Int32? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _4 = reader.readInt32() + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _5 = parseString(reader) + } let _c1 = _1 != nil - let _c2 = _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.PageListOrderedItem.pageListOrderedItemText(Cons_pageListOrderedItemText(flags: _1!, num: _2!, text: _3!)) + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.PageListOrderedItem.pageListOrderedItemText(Cons_pageListOrderedItemText(flags: _1!, num: _2, text: _3!, value: _4, type: _5)) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index e16cb3b45e..ff6fd215ca 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -211,17 +211,15 @@ public extension Api.messages { } public class Cons_chatInviteJoinResultWebView: TypeConstructorDescription { public var botId: Int64 - public var url: String - public var queryId: Int64 + public var webview: Api.WebViewResult public var users: [Api.User] - public init(botId: Int64, url: String, queryId: Int64, users: [Api.User]) { + public init(botId: Int64, webview: Api.WebViewResult, users: [Api.User]) { self.botId = botId - self.url = url - self.queryId = queryId + self.webview = webview self.users = users } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("chatInviteJoinResultWebView", [("botId", ConstructorParameterDescription(self.botId)), ("url", ConstructorParameterDescription(self.url)), ("queryId", ConstructorParameterDescription(self.queryId)), ("users", ConstructorParameterDescription(self.users))]) + return ("chatInviteJoinResultWebView", [("botId", ConstructorParameterDescription(self.botId)), ("webview", ConstructorParameterDescription(self.webview)), ("users", ConstructorParameterDescription(self.users))]) } } case chatInviteJoinResultOk(Cons_chatInviteJoinResultOk) @@ -237,11 +235,10 @@ public extension Api.messages { break case .chatInviteJoinResultWebView(let _data): if boxed { - buffer.appendInt32(2001452532) + buffer.appendInt32(793887543) } serializeInt64(_data.botId, buffer: buffer, boxed: false) - serializeString(_data.url, buffer: buffer, boxed: false) - serializeInt64(_data.queryId, buffer: buffer, boxed: false) + _data.webview.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(_data.users.count)) for item in _data.users { @@ -256,7 +253,7 @@ public extension Api.messages { case .chatInviteJoinResultOk(let _data): return ("chatInviteJoinResultOk", [("updates", ConstructorParameterDescription(_data.updates))]) case .chatInviteJoinResultWebView(let _data): - return ("chatInviteJoinResultWebView", [("botId", ConstructorParameterDescription(_data.botId)), ("url", ConstructorParameterDescription(_data.url)), ("queryId", ConstructorParameterDescription(_data.queryId)), ("users", ConstructorParameterDescription(_data.users))]) + return ("chatInviteJoinResultWebView", [("botId", ConstructorParameterDescription(_data.botId)), ("webview", ConstructorParameterDescription(_data.webview)), ("users", ConstructorParameterDescription(_data.users))]) } } @@ -276,20 +273,19 @@ public extension Api.messages { public static func parse_chatInviteJoinResultWebView(_ reader: BufferReader) -> ChatInviteJoinResult? { var _1: Int64? _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: [Api.User]? + var _2: Api.WebViewResult? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.WebViewResult + } + var _3: [Api.User]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.ChatInviteJoinResult.chatInviteJoinResultWebView(Cons_chatInviteJoinResultWebView(botId: _1!, url: _2!, queryId: _3!, users: _4!)) + if _c1 && _c2 && _c3 { + return Api.messages.ChatInviteJoinResult.chatInviteJoinResultWebView(Cons_chatInviteJoinResultWebView(botId: _1!, webview: _2!, users: _3!)) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api42.swift b/submodules/TelegramApi/Sources/Api42.swift index 3879eaff5a..2bea97a246 100644 --- a/submodules/TelegramApi/Sources/Api42.swift +++ b/submodules/TelegramApi/Sources/Api42.swift @@ -1644,19 +1644,19 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func toggleWebBrowserSettingsException(flags: Int32, openExternalBrowser: Api.Bool?, url: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func toggleWebBrowserSettingsException(flags: Int32, openExternalBrowser: Api.Bool?, url: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(755631473) + buffer.appendInt32(1626161705) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 { openExternalBrowser!.serialize(buffer, true) } serializeString(url, buffer: buffer, boxed: false) - return (FunctionDescription(name: "account.toggleWebBrowserSettingsException", parameters: [("flags", ConstructorParameterDescription(flags)), ("openExternalBrowser", ConstructorParameterDescription(openExternalBrowser)), ("url", ConstructorParameterDescription(url))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "account.toggleWebBrowserSettingsException", parameters: [("flags", ConstructorParameterDescription(flags)), ("openExternalBrowser", ConstructorParameterDescription(openExternalBrowser)), ("url", ConstructorParameterDescription(url))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.Updates? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.Updates } return result }) diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index e4cfa5833d..0192271ffb 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -553,8 +553,8 @@ public extension Api { public var date: Int32 public var effect: Int64? public var suggestedPost: Api.SuggestedPost? - public var richMessage: Api.InputRichMessage? - public init(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32, effect: Int64?, suggestedPost: Api.SuggestedPost?, richMessage: Api.InputRichMessage?) { + public var richMessage: Api.RichMessage? + public init(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32, effect: Int64?, suggestedPost: Api.SuggestedPost?, richMessage: Api.RichMessage?) { self.flags = flags self.replyTo = replyTo self.message = message @@ -587,7 +587,7 @@ public extension Api { switch self { case .draftMessage(let _data): if boxed { - buffer.appendInt32(-1743452271) + buffer.appendInt32(1627271828) } serializeInt32(_data.flags, buffer: buffer, boxed: false) if Int(_data.flags) & Int(1 << 4) != 0 { @@ -671,10 +671,10 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.SuggestedPost } } - var _9: Api.InputRichMessage? + var _9: Api.RichMessage? if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.InputRichMessage + _9 = Api.parse(reader, signature: signature) as? Api.RichMessage } } let _c1 = _1 != nil diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 6d78d0b9ed..bf3ed2bfc0 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -997,6 +997,19 @@ public extension Api { return ("inputBotInlineMessageMediaWebPage", [("flags", ConstructorParameterDescription(self.flags)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("url", ConstructorParameterDescription(self.url)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup))]) } } + public class Cons_inputBotInlineMessageRichMessage: TypeConstructorDescription { + public var flags: Int32 + public var replyMarkup: Api.ReplyMarkup? + public var richMessage: Api.InputRichMessage + public init(flags: Int32, replyMarkup: Api.ReplyMarkup?, richMessage: Api.InputRichMessage) { + self.flags = flags + self.replyMarkup = replyMarkup + self.richMessage = richMessage + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputBotInlineMessageRichMessage", [("flags", ConstructorParameterDescription(self.flags)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup)), ("richMessage", ConstructorParameterDescription(self.richMessage))]) + } + } public class Cons_inputBotInlineMessageText: TypeConstructorDescription { public var flags: Int32 public var message: String @@ -1019,6 +1032,7 @@ public extension Api { case inputBotInlineMessageMediaInvoice(Cons_inputBotInlineMessageMediaInvoice) case inputBotInlineMessageMediaVenue(Cons_inputBotInlineMessageMediaVenue) case inputBotInlineMessageMediaWebPage(Cons_inputBotInlineMessageMediaWebPage) + case inputBotInlineMessageRichMessage(Cons_inputBotInlineMessageRichMessage) case inputBotInlineMessageText(Cons_inputBotInlineMessageText) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -1132,6 +1146,16 @@ public extension Api { _data.replyMarkup!.serialize(buffer, true) } break + case .inputBotInlineMessageRichMessage(let _data): + if boxed { + buffer.appendInt32(-1271007892) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 2) != 0 { + _data.replyMarkup!.serialize(buffer, true) + } + _data.richMessage.serialize(buffer, true) + break case .inputBotInlineMessageText(let _data): if boxed { buffer.appendInt32(1036876423) @@ -1168,6 +1192,8 @@ public extension Api { return ("inputBotInlineMessageMediaVenue", [("flags", ConstructorParameterDescription(_data.flags)), ("geoPoint", ConstructorParameterDescription(_data.geoPoint)), ("title", ConstructorParameterDescription(_data.title)), ("address", ConstructorParameterDescription(_data.address)), ("provider", ConstructorParameterDescription(_data.provider)), ("venueId", ConstructorParameterDescription(_data.venueId)), ("venueType", ConstructorParameterDescription(_data.venueType)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) case .inputBotInlineMessageMediaWebPage(let _data): return ("inputBotInlineMessageMediaWebPage", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("url", ConstructorParameterDescription(_data.url)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) + case .inputBotInlineMessageRichMessage(let _data): + return ("inputBotInlineMessageRichMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup)), ("richMessage", ConstructorParameterDescription(_data.richMessage))]) case .inputBotInlineMessageText(let _data): return ("inputBotInlineMessageText", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))]) } @@ -1403,6 +1429,29 @@ public extension Api { return nil } } + public static func parse_inputBotInlineMessageRichMessage(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.ReplyMarkup? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } + } + var _3: Api.InputRichMessage? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.InputRichMessage + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputBotInlineMessage.inputBotInlineMessageRichMessage(Cons_inputBotInlineMessageRichMessage(flags: _1!, replyMarkup: _2, richMessage: _3!)) + } + else { + return nil + } + } public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 81b3454c13..8fc467b2be 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -11,7 +11,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } case auto(caption: String, entities: TextEntitiesMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) - case text(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) + case text(text: String, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, disableUrlPreview: Bool, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?) case mapLocation(media: TelegramMediaMap, replyMarkup: ReplyMarkupMessageAttribute?) case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?) case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?) @@ -22,7 +22,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { case 0: self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) case 1: - self = .text(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, disableUrlPreview: decoder.decodeInt32ForKey("du", orElse: 0) != 0, previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + self = .text(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, richText: decoder.decodeObjectForKey("rt", decoder: { RichTextMessageAttribute(decoder: $0) }) as? RichTextMessageAttribute, disableUrlPreview: decoder.decodeInt32ForKey("du", orElse: 0) != 0, previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) case 2: self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) case 3: @@ -51,7 +51,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } else { encoder.encodeNil(forKey: "m") } - case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup): + case let .text(text, entities, richText, disableUrlPreview, previewParameters, replyMarkup): encoder.encodeInt32(1, forKey: "_v") encoder.encodeString(text, forKey: "t") if let entities = entities { @@ -59,6 +59,11 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } else { encoder.encodeNil(forKey: "e") } + if let richText { + encoder.encodeObject(richText, forKey: "rt") + } else { + encoder.encodeNil(forKey: "rt") + } encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du") if let previewParameters = previewParameters { encoder.encodeObject(previewParameters, forKey: "prp") @@ -148,14 +153,17 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } else { return false } - case let .text(lhsText, lhsEntities, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup): - if case let .text(rhsText, rhsEntities, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs { + case let .text(lhsText, lhsEntities, lhsRichText, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup): + if case let .text(rhsText, rhsEntities, rhsRichText, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs { if lhsText != rhsText { return false } if lhsEntities != rhsEntities { return false } + if lhsRichText != rhsRichText { + return false + } if lhsDisableUrlPreview != rhsDisableUrlPreview { return false } @@ -508,12 +516,18 @@ extension ChatContextResultMessage { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } let leadingPreview = (flags & (1 << 3)) != 0 - self = .text(text: message, entities: parsedEntities, disableUrlPreview: (flags & (1 << 0)) != 0, previewParameters: WebpagePreviewMessageAttribute( + self = .text(text: message, entities: parsedEntities, richText: nil, disableUrlPreview: (flags & (1 << 0)) != 0, previewParameters: WebpagePreviewMessageAttribute( leadingPreview: leadingPreview, forceLargeMedia: nil, isManuallyAdded: false, isSafe: false ), replyMarkup: parsedReplyMarkup) + case let .botInlineMessageRichMessage(botInlineMessageRichMessage): + var parsedReplyMarkup: ReplyMarkupMessageAttribute? + if let replyMarkup = botInlineMessageRichMessage.replyMarkup { + parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) + } + self = .text(text: "", entities: nil, richText: RichTextMessageAttribute(apiRichMessage: botInlineMessageRichMessage.richMessage), disableUrlPreview: false, previewParameters: nil, replyMarkup: parsedReplyMarkup) case let .botInlineMessageMediaGeo(botInlineMessageMediaGeoData): let (_, geo, heading, period, proximityNotificationRadius, replyMarkup) = (botInlineMessageMediaGeoData.flags, botInlineMessageMediaGeoData.geo, botInlineMessageMediaGeoData.heading, botInlineMessageMediaGeoData.period, botInlineMessageMediaGeoData.proximityNotificationRadius, botInlineMessageMediaGeoData.replyMarkup) let media = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 5fbc475515..cf3ef21812 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -366,7 +366,7 @@ extension InstantPageBlock { return .pageBlockAnchor(Api.PageBlock.Cons_pageBlockAnchor(name: value)) case let .list(items, ordered): if ordered { - return .inputPageBlockOrderedList(Api.PageBlock.Cons_inputPageBlockOrderedList(flags: 0, items: items.map { $0.apiInputPageOrderedListItem() }, start: nil, type: nil)) + return .pageBlockOrderedList(Api.PageBlock.Cons_pageBlockOrderedList(flags: 0, items: items.map { $0.apiInputPageOrderedListItem() }, start: nil, type: nil)) } else { return .pageBlockList(Api.PageBlock.Cons_pageBlockList(items: items.map { $0.apiInputPageListItem() })) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index e7d30b530f..0809161243 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -145,9 +145,13 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } } - case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup): - if let entities = entities { - attributes.append(entities) + case let .text(text, entities, richText, disableUrlPreview, previewParameters, replyMarkup): + if let richText { + attributes.append(richText) + } else { + if let entities = entities { + attributes.append(entities) + } } if let replyMarkup = replyMarkup { attributes.append(replyMarkup) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index 0587fafd12..01d8f00167 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -150,7 +150,10 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S } |> castError(JoinChannelError.self) |> map { _ -> JoinChannelOutcome in - return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: data.url, queryId: data.queryId) + switch data.webview { + case let .webViewResultUrl(webViewResultUrl): + return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: webViewResultUrl.url, queryId: webViewResultUrl.queryId ?? 0) + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index af6b5b3e8a..3c4f10db2c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -110,7 +110,10 @@ func _internal_joinChatInteractively(with hash: String, account: Account) -> Sig } |> castError(JoinLinkError.self) |> map { _ -> JoinChatInteractivelyOutcome in - return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: data.url, queryId: data.queryId) + switch data.webview { + case let .webViewResultUrl(webViewResultUrl): + return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: webViewResultUrl.url, queryId: webViewResultUrl.queryId ?? 0) + } } } } diff --git a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift index af6446c1ef..c7dac9573c 100644 --- a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift +++ b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift @@ -19,6 +19,12 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemCo if lhs.text != rhs.text { return false } + if lhs.entities != rhs.entities { + return false + } + if lhs.richText != rhs.richText { + return false + } if lhs.media.count != rhs.media.count { return false } @@ -35,6 +41,7 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemCo let text: String let entities: TextEntitiesMessageAttribute? + let richText: RichTextMessageAttribute? let media: [EngineRawMedia] let replyMarkup: ReplyMarkupMessageAttribute? let botAddress: String @@ -207,6 +214,9 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { if let entities = messageItem.entities { attributes.append(entities) } + if let richText = messageItem.richText { + attributes.append(richText) + } if let replyMarkup = messageItem.replyMarkup { attributes.append(replyMarkup) } diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 49da936b4e..9362bf658f 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -137,6 +137,7 @@ private final class SheetContent: CombinedComponent { var text: String = "" var entities: TextEntitiesMessageAttribute? + var richText: RichTextMessageAttribute? var media: [EngineRawMedia] = [] var replyMarkup: ReplyMarkupMessageAttribute? @@ -152,8 +153,9 @@ private final class SheetContent: CombinedComponent { media = [image] } replyMarkup = replyMarkupValue - case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue): + case let .text(textValue, entitiesValue, richTextValue, disableUrlPreview, previewParameters, replyMarkupValue): text = textValue + richText = richTextValue entities = entitiesValue let _ = disableUrlPreview let _ = previewParameters @@ -181,9 +183,10 @@ private final class SheetContent: CombinedComponent { media = [content] } replyMarkup = replyMarkupValue - case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue): + case let .text(textValue, entitiesValue, richTextValue, disableUrlPreview, previewParameters, replyMarkupValue): text = textValue entities = entitiesValue + richText = richTextValue let _ = disableUrlPreview let _ = previewParameters replyMarkup = replyMarkupValue @@ -206,6 +209,7 @@ private final class SheetContent: CombinedComponent { let messageItem = PeerNameColorChatPreviewItem.MessageItem( text: text, entities: entities, + richText: richText, media: media, replyMarkup: replyMarkup, botAddress: component.botAddress From 28763bb92d7e7f52440368ec21806e241a85c155 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Fri, 29 May 2026 00:57:22 +0200 Subject: [PATCH 14/16] InstantPage blockQuote: nested-blocks payload Upgrade InstantPageBlock.blockQuote from (text, caption) to (blocks: [InstantPageBlock], caption). Legacy inbound shapes (API pageBlockBlockquote, Postbox "t", FlatBuffers text) lift into [.paragraph(text)]; outbound stays on the legacy wire constructor for empty/single-paragraph quotes and uses pageBlockBlockquoteBlocks otherwise. V1/V2 renderers recurse into child blocks with a fixed 10pt inter-child gap; markdown forward/reverse, entity-expressibility, and preview text updated. pullQuote is unchanged. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 28 ++++++ .../BrowserUI/Sources/BrowserMarkdown.swift | 36 ++++---- .../Sources/BrowserReadability.swift | 2 +- .../Sources/InstantPageToMarkdown.swift | 17 +++- .../Sources/InstantPageLayout.swift | 63 +++++++++---- .../Sources/InstantPageV2Layout.swift | 92 +++++++++++++++++-- .../Models/InstantPageBlock.fbs | 3 +- .../Sources/ApiUtils/InstantPage.swift | 14 ++- .../SyncCore/SyncCore_InstantPage.swift | 35 +++++-- .../Sources/InstantPagePreviewText.swift | 5 +- 10 files changed, 233 insertions(+), 62 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 16bd76e601..c0a8abfcd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -228,6 +228,34 @@ Spec: [`docs/superpowers/specs/2026-05-27-instantpage-list-checkbox-design.md`]( - **Forward parser keeps `[ ]` detection but routes to `checked`.** `markdownApplyTaskListMarker`/`markdownStrippingTaskListMarker`/`markdownTaskListMarker` still strip the marker from the item text; the state flows into `checked` while ordered items keep their real `"\(ordinal)"` number. The reverse converter emits lowercase `[x]` / `[ ]`, which the forward `hasPrefix` guards re-parse — that is the round-trip contract. - **The enum-arity change is compile-enforced.** Adding the third associated value broke every `.text`/`.blocks` construction/destructure; the full build is the completeness gate. Read-only consumers outside the core set exist (`BrowserInstantPageContent.swift`, `CachedFaqInstantPage.swift`) — grep `\.(text|blocks)\(` repo-wide when touching the enum again. +## InstantPageBlock.blockQuote nested blocks + +`InstantPageBlock.blockQuote` carries `(blocks: [InstantPageBlock], caption: RichText)` — a sequence of nested page blocks (paragraphs, headings, lists, code, even nested quotes), not the legacy text-only payload. `.pullQuote` is unchanged (still `(text: RichText, caption: RichText)`; the TL API has no `pullQuoteBlocks` constructor). + +Spec: [`docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md`](docs/superpowers/specs/2026-05-29-instantpage-blockquote-blocks-design.md). Plan: [`docs/superpowers/plans/2026-05-29-instantpage-blockquote-blocks.md`](docs/superpowers/plans/2026-05-29-instantpage-blockquote-blocks.md). + +### Where things live + +| File | Responsibility | +|---|---| +| `submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift` | Enum case shape; Postbox coding (legacy `"t"` lift → new `"b"` object array); equality (array-aware, mirrors `.collage`); FlatBuffers codec. | +| `submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs` | `InstantPageBlock_BlockQuote`: `text` (now optional, legacy fallback) + `caption (required)` + new `blocks:[InstantPageBlock] (id: 2)`. **Source of truth**; Bazel regenerates the `*_generated.swift`. | +| `submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift` | Parse both `pageBlockBlockquote` (lift text→`[.paragraph]`) and `pageBlockBlockquoteBlocks`; encode legacy-when-possible. | +| `submodules/InstantPageUI/Sources/InstantPageV2Layout.swift` | `layoutBlockQuote(blocks:…)` recurses into children; legacy single-paragraph fast path delegates to `layoutQuoteText` (the renamed shared text core, also used by `.pullQuote`). | +| `submodules/InstantPageUI/Sources/InstantPageLayout.swift` | V1 `.blockQuote` arm recurses via `layoutInstantPageBlock(...)`; same single-paragraph fast path. | +| `submodules/BrowserUI/Sources/BrowserMarkdown.swift` | Forward: one quote carrying all child blocks. Entity-expressibility gate (below). | +| `submodules/BrowserUI/Sources/InstantPageToMarkdown.swift` | Reverse: `markdownBlockQuoteBlocks(_:)` recurses per child and prefixes `> ` per line. | +| `submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift` | Concatenates child `previewText()`s + caption. | + +### Non-obvious invariants + +- **Legacy shapes lift to `[.paragraph(text)]` at every decode boundary.** API `pageBlockBlockquote`, the Postbox `"t"` key (old cached pages), and the FlatBuffers `text` field (now optional) each lift into a single-paragraph blocks array. New writes emit only `blocks` (`"b"` / the FB vector). So pre-existing stored pages and older senders decode unchanged. +- **Outbound stays on the legacy wire constructor when the shape allows.** `apiInputBlock()` emits `pageBlockBlockquote` for empty or single-`.paragraph` quotes (so older recipients understand the common chat case) and `pageBlockBlockquoteBlocks` only for genuinely nested quotes. +- **Both renderers share one text core for the single-paragraph fast path.** `layoutQuoteText` (V2; the function formerly named `layoutBlockQuote`, `isPull:` distinguishes pull vs block) and the V1 fast-path branch keep the legacy italicized-body styling; nested children render with their own normal category styling. +- **Nested children use a FIXED 10pt inter-child gap, not `spacingBetweenBlocks`.** The full page-flow spacing (~27pt around quotes) is too airy when nested, and 0 is too tight. `childSpacing = 10.0` lives in both layout files; the first child hugs the container's `verticalInset` (no leading gap). Combined with a nested quote's own 4pt top inset this gives ~14pt effective separation. +- **Entity-expressibility:** a quote is entity-expressible (→ regular message path) only if its caption is empty AND every child is an entity-expressible `.paragraph`. A nested-structure or multi-paragraph quote is not, so it sends via the rich path. **Behavior change:** markdown `> p1\n>\n> p2` is now ONE quote with two paragraphs (rich) rather than two consecutive entity quotes — correct semantics. +- **The enum-arity change is compile-enforced** across all modules; the full Bazel build is the completeness gate (no per-module build). `CachedFaqInstantPage.swift` matches `case .blockQuote:` payload-less and needs no edit. `BrowserReadability.swift` constructs `.blockQuote(blocks: [.paragraph(.italic(...))], …)` and is easy to miss in the spec's file list — grep `\.blockQuote(` repo-wide when touching the case again. + ## Postbox → TelegramEngine refactor (in progress) A gradual migration is underway to eliminate direct `import Postbox` from consumer submodules in favor of `TelegramEngine`. diff --git a/submodules/BrowserUI/Sources/BrowserMarkdown.swift b/submodules/BrowserUI/Sources/BrowserMarkdown.swift index 5a1481b9bc..2196c0c5ee 100644 --- a/submodules/BrowserUI/Sources/BrowserMarkdown.swift +++ b/submodules/BrowserUI/Sources/BrowserMarkdown.swift @@ -1116,8 +1116,14 @@ private func blockIsEntityExpressible(_ block: InstantPageBlock) -> Bool { return richTextIsEntityExpressible(text) case .preformatted(let text, _): return richTextIsEntityExpressible(text) - case .blockQuote(let text, let caption): - return isEmptyRichText(caption) && richTextIsEntityExpressible(text) + case .blockQuote(let blocks, let caption): + guard isEmptyRichText(caption) else { return false } + return blocks.allSatisfy { child in + if case let .paragraph(text) = child { + return richTextIsEntityExpressible(text) + } + return false + } case .anchor, .unsupported: return true case .divider: @@ -1392,24 +1398,17 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv case .thematicBreak: return [.divider] case .blockQuote: - var result: [InstantPageBlock] = [] + var childBlocks: [InstantPageBlock] = [] for child in node.children { - guard let childBlocks = markdownBlocks(from: child, context: context, depth: depth + 1) else { + guard let parsed = markdownBlocks(from: child, context: context, depth: depth + 1) else { return nil } - for childBlock in childBlocks { - switch childBlock { - case let .paragraph(text): - result.append(.blockQuote(text: text, caption: .empty)) - default: - let plainText = markdownPlainText(from: childBlock, depth: depth + 1) - if !plainText.isEmpty { - result.append(.blockQuote(text: .plain(plainText), caption: .empty)) - } - } - } + childBlocks.append(contentsOf: parsed) } - return result + guard !childBlocks.isEmpty else { + return [] + } + return [.blockQuote(blocks: childBlocks, caption: .empty)] case .orderedList: guard let items = markdownListItems(from: node.children, ordered: true, context: context, depth: depth + 1) else { return nil @@ -2213,8 +2212,9 @@ private func markdownPlainText(from block: InstantPageBlock, depth: Int = 0) -> return text.plainText case let .footer(text): return text.plainText - case let .blockQuote(text, caption): - return text.plainText.isEmpty ? caption.plainText : text.plainText + case let .blockQuote(blocks, caption): + let blocksText = blocks.map { markdownPlainText(from: $0, depth: depth + 1) }.joined(separator: "\n") + return blocksText.isEmpty ? caption.plainText : blocksText case let .pullQuote(text, caption): return text.plainText.isEmpty ? caption.plainText : text.plainText case let .kicker(text): diff --git a/submodules/BrowserUI/Sources/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift index 0d56552f2d..5a388762d6 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -781,7 +781,7 @@ private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [Engi case "pre": result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil)) case "blockquote": - result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty)) + result.append(.blockQuote(blocks: [.paragraph(.italic(trim(parseRichText(item, &media))))], caption: .empty)) case "img": if let image = parseImage(item, &media) { result.append(image) diff --git a/submodules/BrowserUI/Sources/InstantPageToMarkdown.swift b/submodules/BrowserUI/Sources/InstantPageToMarkdown.swift index 5db9133393..e08a2edc94 100644 --- a/submodules/BrowserUI/Sources/InstantPageToMarkdown.swift +++ b/submodules/BrowserUI/Sources/InstantPageToMarkdown.swift @@ -39,8 +39,8 @@ private func markdownString(from block: InstantPageBlock) -> String? { case let .preformatted(text, language): let language = language ?? "" return "```\(language)\n\(markdownInline(from: text))\n```" - case let .blockQuote(text, _): - return markdownBlockQuote(text) + case let .blockQuote(blocks, _): + return markdownBlockQuoteBlocks(blocks) case let .pullQuote(text, _): return markdownBlockQuote(text) case let .list(items, ordered): @@ -69,6 +69,19 @@ private func markdownBlockQuote(_ text: RichText) -> String { return lines.map { "> \($0)" }.joined(separator: "\n") } +private func markdownBlockQuoteBlocks(_ blocks: [InstantPageBlock]) -> String { + var lines: [String] = [] + for block in blocks { + guard let body = markdownString(from: block) else { + continue + } + for line in body.split(separator: "\n", omittingEmptySubsequences: false) { + lines.append("> \(String(line))") + } + } + return lines.joined(separator: "\n") +} + private func markdownInline(from richText: RichText) -> String { switch richText { case .empty: diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index 1e4ac8765b..0f227366c7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -501,40 +501,65 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems) - case let .blockQuote(text, caption): + case let .blockQuote(blocks, caption): let lineInset: CGFloat = 20.0 let verticalInset: CGFloat = 4.0 var contentSize = CGSize(width: boundingWidth, height: verticalInset) - + var items: [InstantPageItem] = [] - - let styleStack = InstantPageTextStyleStack() - setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) - styleStack.push(.italic) - - let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage, fitToWidth: fitToWidth) - - contentSize.height += textContentSize.height - items.append(contentsOf: textItems) - + + let innerBoundingWidth = boundingWidth - horizontalInset * 2.0 - lineInset + let innerHorizontalInset = horizontalInset + lineInset + + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + // Legacy single-paragraph fast path: italicized body (unchanged from prior behavior). + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.italic) + + let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: innerBoundingWidth, offset: CGPoint(x: innerHorizontalInset, y: contentSize.height), media: media, webpage: webpage, fitToWidth: fitToWidth) + + contentSize.height += textContentSize.height + items.append(contentsOf: textItems) + } else { + var previousChildItems: [InstantPageItem] = [] + // Fixed, compact gap between a quote's child blocks (the full page-flow + // spacing around quotes is too airy when nested); the first child hugs + // the top (only verticalInset above it). + let childSpacing: CGFloat = 10.0 + for (i, child) in blocks.enumerated() { + let childLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: child, boundingWidth: innerBoundingWidth, horizontalInset: innerHorizontalInset, safeInset: safeInset, isCover: false, previousItems: previousChildItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: excludeCaptions, isLast: i == blocks.count - 1 && isLast, fitToWidth: fitToWidth) + let spacing: CGFloat = i == 0 ? 0.0 : childSpacing + contentSize.height += spacing + var childItems: [InstantPageItem] = [] + for var item in childLayout.items { + item.frame = item.frame.offsetBy(dx: 0.0, dy: contentSize.height) + childItems.append(item) + } + items.append(contentsOf: childItems) + previousChildItems = childItems + contentSize.height += childLayout.contentSize.height + } + } + if case .empty = caption { } else { contentSize.height += 14.0 - + let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .caption, link: false) - - let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage) - + + let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: innerBoundingWidth, offset: CGPoint(x: innerHorizontalInset, y: contentSize.height), media: media, webpage: webpage) + contentSize.height += captionContentSize.height items.append(contentsOf: captionItems) } contentSize.height += verticalInset - + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: theme.textCategories.paragraph.color) - + items.append(shapeItem) - + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .pullQuote(text, caption): let verticalInset: CGFloat = 4.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift index bf24dcede1..2b55936e95 100644 --- a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift @@ -594,14 +594,14 @@ private func layoutBlock( return layoutCodeBlock(text, language: language, boundingWidth: boundingWidth, horizontalInset: horizontalInset, context: &context) - case let .blockQuote(text, caption): - return layoutBlockQuote(text: text, caption: caption, isPull: false, + case let .blockQuote(blocks, caption): + return layoutBlockQuote(blocks: blocks, caption: caption, boundingWidth: boundingWidth, horizontalInset: horizontalInset, - context: &context) + isLast: isLast, context: &context) case let .pullQuote(text, caption): - return layoutBlockQuote(text: text, caption: caption, isPull: true, - boundingWidth: boundingWidth, horizontalInset: horizontalInset, - context: &context) + return layoutQuoteText(text: text, caption: caption, isPull: true, + boundingWidth: boundingWidth, horizontalInset: horizontalInset, + context: &context) case let .image(id, caption, url, webpageId): if case let .image(image) = context.media[id], let largest = largestImageRepresentation(image.representations) { @@ -1901,6 +1901,86 @@ private func layoutCodeBlock( // MARK: - Block quote / pull quote layout (ported from V1 InstantPageLayout.swift lines 517–586) private func layoutBlockQuote( + blocks: [InstantPageBlock], + caption: RichText, + boundingWidth: CGFloat, + horizontalInset: CGFloat, + isLast: Bool, + context: inout LayoutContext +) -> [InstantPageV2LaidOutItem] { + // Legacy single-paragraph fast path: preserve today's italicized body styling. + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + return layoutQuoteText(text: text, caption: caption, isPull: false, + boundingWidth: boundingWidth, horizontalInset: horizontalInset, + context: &context) + } + + let verticalInset: CGFloat = 4.0 + let lineInset: CGFloat = 20.0 + let barWidth: CGFloat = 3.0 + + let innerBoundingWidth = boundingWidth - horizontalInset * 2.0 - lineInset + let innerHorizontalInset = horizontalInset + lineInset + + var result: [InstantPageV2LaidOutItem] = [] + var contentHeight: CGFloat = verticalInset + + // Fixed, compact gap between a quote's child blocks. The full page-flow spacing + // (spacingBetweenBlocks ~27pt around quotes) is too airy when nested; the first + // child hugs the top (only verticalInset above it). + let childSpacing: CGFloat = 10.0 + for (i, child) in blocks.enumerated() { + let spacing: CGFloat = i == 0 ? 0.0 : childSpacing + let childItems = layoutBlock( + child, + boundingWidth: innerBoundingWidth, + horizontalInset: innerHorizontalInset, + isCover: false, + previousItems: result, + isLast: i == blocks.count - 1 && isLast, + context: &context + ) + let dy = contentHeight + spacing + let offsetItems = childItems.map { $0.offsetBy(CGPoint(x: 0.0, y: dy)) } + let childMaxY = offsetItems.map { $0.frame.maxY }.max() ?? dy + contentHeight = max(contentHeight, childMaxY) + result.append(contentsOf: offsetItems) + } + + // Optional caption (mirrors layoutQuoteText's caption branch). + if case .empty = caption { + // no caption + } else { + contentHeight += 14.0 + let captionStyleStack = InstantPageTextStyleStack() + setupStyleStack(captionStyleStack, theme: context.theme, category: .caption, link: false) + let attributedCaption = attributedStringForRichText(caption, styleStack: captionStyleStack) + let (_, captionItems, captionSize) = layoutTextItem( + attributedCaption, + boundingWidth: innerBoundingWidth, + alignment: .natural, + offset: CGPoint(x: innerHorizontalInset, y: contentHeight), + fitToWidth: context.fitToWidth, + computeRevealCharacterRects: context.computeRevealCharacterRects + ) + result.append(contentsOf: captionItems) + contentHeight += captionSize.height + } + + contentHeight += verticalInset + + // Vertical bar on the leading edge (matches the blockQuote branch of layoutQuoteText). + let bar = InstantPageV2BarItem( + frame: CGRect(x: horizontalInset, y: 0.0, width: barWidth, height: contentHeight), + color: context.theme.textCategories.paragraph.color, + cornerRadius: barWidth / 2.0 + ) + result.append(.blockQuoteBar(bar)) + + return result +} + +private func layoutQuoteText( text: RichText, caption: RichText, isPull: Bool, diff --git a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs index fc5e67b3ce..eb51bad065 100644 --- a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs +++ b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs @@ -91,8 +91,9 @@ table InstantPageBlock_List { } table InstantPageBlock_BlockQuote { - text:RichText (id: 0, required); + text:RichText (id: 0); caption:RichText (id: 1, required); + blocks:[InstantPageBlock] (id: 2); } table InstantPageBlock_PullQuote { diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index cf3ef21812..4241837632 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -246,7 +246,9 @@ extension InstantPageBlock { self = .anchor(name) case let .pageBlockBlockquote(pageBlockBlockquoteData): let (text, caption) = (pageBlockBlockquoteData.text, pageBlockBlockquoteData.caption) - self = .blockQuote(text: RichText(apiText: text), caption: RichText(apiText: caption)) + self = .blockQuote(blocks: [.paragraph(RichText(apiText: text))], caption: RichText(apiText: caption)) + case let .pageBlockBlockquoteBlocks(pageBlockBlockquoteBlocksData): + self = .blockQuote(blocks: pageBlockBlockquoteBlocksData.blocks.map { InstantPageBlock(apiBlock: $0) }, caption: RichText(apiText: pageBlockBlockquoteBlocksData.caption)) case let .pageBlockPullquote(pageBlockPullquoteData): let (text, caption) = (pageBlockPullquoteData.text, pageBlockPullquoteData.caption) self = .pullQuote(text: RichText(apiText: text), caption: RichText(apiText: caption)) @@ -370,8 +372,14 @@ extension InstantPageBlock { } else { return .pageBlockList(Api.PageBlock.Cons_pageBlockList(items: items.map { $0.apiInputPageListItem() })) } - case let .blockQuote(text, caption): - return .pageBlockBlockquote(Api.PageBlock.Cons_pageBlockBlockquote(text: text.apiRichText(), caption: caption.apiRichText())) + case let .blockQuote(blocks, caption): + if blocks.isEmpty { + return .pageBlockBlockquote(Api.PageBlock.Cons_pageBlockBlockquote(text: RichText.empty.apiRichText(), caption: caption.apiRichText())) + } + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + return .pageBlockBlockquote(Api.PageBlock.Cons_pageBlockBlockquote(text: text.apiRichText(), caption: caption.apiRichText())) + } + return .pageBlockBlockquoteBlocks(Api.PageBlock.Cons_pageBlockBlockquoteBlocks(blocks: blocks.compactMap { $0.apiInputBlock() }, caption: caption.apiRichText())) case let .pullQuote(text, caption): return .pageBlockPullquote(Api.PageBlock.Cons_pageBlockPullquote(text: text.apiRichText(), caption: caption.apiRichText())) case let .image(id, caption, url, webpageId): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift index d1694a05f6..87e276953f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift @@ -70,7 +70,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case divider case anchor(String) case list(items: [InstantPageListItem], ordered: Bool) - case blockQuote(text: RichText, caption: RichText) + case blockQuote(blocks: [InstantPageBlock], caption: RichText) case pullQuote(text: RichText, caption: RichText) case image(id: MediaId, caption: InstantPageCaption, url: String?, webpageId: MediaId?) case video(id: MediaId, caption: InstantPageCaption, autoplay: Bool, loop: Bool) @@ -121,7 +121,13 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case InstantPageBlockType.list.rawValue: self = .list(items: decodeListItems(decoder), ordered: decoder.decodeOptionalInt32ForKey("o") != 0) case InstantPageBlockType.blockQuote.rawValue: - self = .blockQuote(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, caption: decoder.decodeObjectForKey("c", decoder: { RichText(decoder: $0) }) as! RichText) + let caption = decoder.decodeObjectForKey("c", decoder: { RichText(decoder: $0) }) as! RichText + if let legacyText = decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as? RichText { + self = .blockQuote(blocks: [.paragraph(legacyText)], caption: caption) + } else { + let blocks: [InstantPageBlock] = decoder.decodeObjectArrayWithDecoderForKey("b") + self = .blockQuote(blocks: blocks, caption: caption) + } case InstantPageBlockType.pullQuote.rawValue: self = .pullQuote(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, caption: decoder.decodeObjectForKey("c", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.image.rawValue: @@ -225,9 +231,9 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { encoder.encodeInt32(InstantPageBlockType.list.rawValue, forKey: "r") encoder.encodeObjectArray(items, forKey: "ml") encoder.encodeInt32(ordered ? 1 : 0, forKey: "o") - case let .blockQuote(text, caption): + case let .blockQuote(blocks, caption): encoder.encodeInt32(InstantPageBlockType.blockQuote.rawValue, forKey: "r") - encoder.encodeObject(text, forKey: "t") + encoder.encodeObjectArray(blocks, forKey: "b") encoder.encodeObject(caption, forKey: "c") case let .pullQuote(text, caption): encoder.encodeInt32(InstantPageBlockType.pullQuote.rawValue, forKey: "r") @@ -445,8 +451,8 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { } else { return false } - case let .blockQuote(text, caption): - if case .blockQuote(text, caption) = rhs { + case let .blockQuote(lhsBlocks, lhsCaption): + if case let .blockQuote(rhsBlocks, rhsCaption) = rhs, lhsBlocks == rhsBlocks, lhsCaption == rhsCaption { return true } else { return false @@ -621,7 +627,15 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_BlockQuote.self) else { throw FlatBuffersError.missingRequiredField() } - self = .blockQuote(text: try RichText(flatBuffersObject: value.text), caption: try RichText(flatBuffersObject: value.caption)) + let caption = try RichText(flatBuffersObject: value.caption) + if value.blocksCount > 0 { + let blocks = try (0 ..< value.blocksCount).map { try InstantPageBlock(flatBuffersObject: value.blocks(at: $0)!) } + self = .blockQuote(blocks: blocks, caption: caption) + } else if let legacyText = value.text { + self = .blockQuote(blocks: [.paragraph(try RichText(flatBuffersObject: legacyText))], caption: caption) + } else { + self = .blockQuote(blocks: [], caption: caption) + } case .instantpageblockPullquote: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_PullQuote.self) else { throw FlatBuffersError.missingRequiredField() @@ -796,12 +810,13 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { TelegramCore_InstantPageBlock_List.addVectorOf(items: itemsOffset, &builder) TelegramCore_InstantPageBlock_List.add(ordered: ordered, &builder) offset = TelegramCore_InstantPageBlock_List.endInstantPageBlock_List(&builder, start: start) - case let .blockQuote(text, caption): + case let .blockQuote(blocks, caption): valueType = .instantpageblockBlockquote - let textOffset = text.encodeToFlatBuffers(builder: &builder) + let blocksOffsets = blocks.map { $0.encodeToFlatBuffers(builder: &builder) } + let blocksOffset = builder.createVector(ofOffsets: blocksOffsets, len: blocksOffsets.count) let captionOffset = caption.encodeToFlatBuffers(builder: &builder) let start = TelegramCore_InstantPageBlock_BlockQuote.startInstantPageBlock_BlockQuote(&builder) - TelegramCore_InstantPageBlock_BlockQuote.add(text: textOffset, &builder) + TelegramCore_InstantPageBlock_BlockQuote.addVectorOf(blocks: blocksOffset, &builder) TelegramCore_InstantPageBlock_BlockQuote.add(caption: captionOffset, &builder) offset = TelegramCore_InstantPageBlock_BlockQuote.endInstantPageBlock_BlockQuote(&builder, start: start) case let .pullQuote(text, caption): diff --git a/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift b/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift index 04e5a2960a..ed6e7fb3d1 100644 --- a/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift +++ b/submodules/TelegramStringFormatting/Sources/InstantPagePreviewText.swift @@ -123,8 +123,9 @@ extension InstantPageBlock { result.append(item.previewText()) } return result - case let .blockQuote(text, caption): - return text.previewText() + caption.previewText() + case let .blockQuote(blocks, caption): + let body = blocks.map { $0.previewText() }.joined(separator: " ") + return body + caption.previewText() case let .pullQuote(text, caption): return text.previewText() + caption.previewText() case .image(_, _, _, _): From befacaa5fe027b6658351d9864c826f97364b31f Mon Sep 17 00:00:00 2001 From: isaac <> Date: Fri, 29 May 2026 01:21:36 +0200 Subject: [PATCH 15/16] InstantPage: distinct per-level heading sizes --- .../Sources/InstantPageTheme.swift | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 72a6008861..5d820ae9a6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -163,16 +163,28 @@ public final class InstantPageTheme { } func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes { - let clampedLevel = max(Int32(3), min(level, Int32(6))) - let subheaderAttributes = self.textCategories.subheader - guard clampedLevel > 3 else { - return subheaderAttributes.withUnderline(link) + let clampedLevel = max(Int32(1), min(level, Int32(6))) + + // H1/H2 reuse the theme's existing big-text categories verbatim, so they + // pick up the theme color, line-spacing, and any dynamic-type scaling. + switch clampedLevel { + case 1: + return self.textCategories.header.withUnderline(link) + case 2: + return self.textCategories.subheader.withUnderline(link) + default: + break } + // H3–H6: serif at a per-level base size, scaled by the same dynamic-type + // multiplier the subheader category uses (subheader.size / 19.0). + let subheaderAttributes = self.textCategories.subheader let baseSize: CGFloat switch clampedLevel { - case 4: + case 3: baseSize = 17.0 + case 4: + baseSize = 16.0 case 5: baseSize = 15.0 default: From 65eac2fa941ef61e72bc4fe357315a6bcd86c0f6 Mon Sep 17 00:00:00 2001 From: isaac <> Date: Fri, 29 May 2026 02:03:31 +0200 Subject: [PATCH 16/16] Update tgwatch --- .../Messages/AudioBubbleView.swift | 54 ++++----------- .../Messages/BubbleShape.swift | 13 ++++ .../Messages/DocumentBubbleView.swift | 19 ++---- .../Messages/LocationBubbleView.swift | 28 +++----- .../Messages/MessageBubbleView.swift | 68 +++++++------------ .../Messages/MessageRow.swift | 30 +++++--- .../Messages/PhotoBubbleView.swift | 28 ++------ .../Messages/PollBubbleView.swift | 27 +++----- .../Messages/ReviewVoiceBubble.swift | 2 +- .../Messages/VideoBubbleView.swift | 35 ++-------- .../Messages/VideoNoteBubbleView.swift | 11 +-- .../Messages/VoiceNoteBubbleView.swift | 51 ++++---------- .../Stickers/StickerBubbleView.swift | 36 +++++----- .../tgwatch.xcodeproj/project.pbxproj | 4 ++ 14 files changed, 142 insertions(+), 264 deletions(-) create mode 100644 Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift index 8ff6757aca..c4bafa94c6 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift @@ -13,10 +13,8 @@ import SwiftUI struct AudioBubbleView: View { let audio: AudioVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? - let sendingState: SendingState @Environment(ChatHistoryStore.self) private var store @Environment(\.bubbleMetrics) private var metrics @@ -73,13 +71,12 @@ struct AudioBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - timeRow } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -118,28 +115,6 @@ struct AudioBubbleView: View { .shadow(radius: audio.albumArt != nil ? 1 : 0) } - // Time + send-state inline at the bottom-right inside the chrome, matching the - // text-bubble convention (and VoiceNoteBubbleView). - private var timeRow: some View { - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if isOutgoing { sendStateGlyph } - } - } - - @ViewBuilder - private var sendStateGlyph: some View { - switch sendingState { - case .sent: EmptyView() - case .pending: Image(systemName: "clock").font(.system(size: 8)) - .foregroundStyle(Color.white.opacity(0.7)) - case .failed: Image(systemName: "exclamationmark.circle.fill").font(.system(size: 8)) - .foregroundStyle(.red) - } - } } #if DEBUG @@ -186,8 +161,8 @@ private func previewAudio( #Preview("Audio — incoming, idle") { AudioBubbleView( audio: previewAudio(), caption: "", - time: "12:34", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -196,8 +171,8 @@ private func previewAudio( #Preview("Audio — outgoing, idle") { AudioBubbleView( audio: previewAudio(id: 2), caption: "", - time: "12:35", isOutgoing: true, - replyHeader: nil, sendingState: .sent + isOutgoing: true, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -207,8 +182,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 3, title: "A Very Long Track Title That Will Truncate", performer: "Some Long Performer Name"), caption: "", - time: "12:36", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -218,8 +193,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 4, title: "Untitled", performer: ""), caption: "", - time: "12:37", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -229,8 +204,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 5), caption: "this song goes hard", - time: "12:38", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -239,13 +214,12 @@ private func previewAudio( #Preview("Audio — incoming with reply") { AudioBubbleView( audio: previewAudio(id: 6), caption: "", - time: "12:39", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "anchor message earlier in the chat", minithumbnail: nil, isOutgoing: false - ), - sendingState: .sent + ) ) .bubblePreview() .environment(previewStore()) diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift new file mode 100644 index 0000000000..70030dd712 --- /dev/null +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift @@ -0,0 +1,13 @@ +import CoreGraphics + +/// Single source of truth for message-bubble geometry. Every bubble surface +/// (text, voice, audio, document, poll, photo, video, map) clips/fills to this +/// radius; fill-chrome bubbles also enforce the minimum so a one-word message +/// renders as a 28×28 pill (radius == half-size → circular ends). +enum BubbleShape { + static let cornerRadius: CGFloat = 14 + /// Minimum bubble edge (min-width and min-height). Equals `2 × cornerRadius` + /// so a one-word fill-chrome bubble renders as a circle/pill rather than a + /// rounded-rect with concave-looking corners. + static let minSize: CGFloat = 28 +} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift index b5e9fb1b6c..b056d22dd6 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift @@ -1,11 +1,10 @@ import SwiftUI /// Renders one generic-document bubble: BubbleStyle chrome with a rounded-square doc icon, -/// filename, size, optional caption, and a time footer. Display-only (no download/open on +/// filename, size, optional caption. Display-only (no download/open on /// watch in milestone #4). struct DocumentBubbleView: View { let document: DocumentVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -35,15 +34,11 @@ struct DocumentBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time).font(.system(size: 8)).foregroundStyle(style.secondary) - } } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) - .background(RoundedRectangle(cornerRadius: 10).fill(style.fill)) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) + .background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(style.fill)) .foregroundStyle(style.content) } @@ -61,7 +56,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 1, fileName: "report.pdf", sizeBytes: 2_400_000, localPath: nil, caption: ""), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ).bubblePreview() } @@ -69,7 +64,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 2, fileName: "quarterly-financial-summary-2026.xlsx", sizeBytes: 52_428_800, localPath: nil, caption: ""), - time: "12:35", isOutgoing: true, replyHeader: nil + isOutgoing: true, replyHeader: nil ).bubblePreview() } @@ -77,7 +72,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 3, fileName: "archive.zip", sizeBytes: 52_428_800, localPath: nil, caption: "here are the files"), - time: "12:36", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ).bubblePreview() } @@ -85,7 +80,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 4, fileName: "notes.txt", sizeBytes: 1024, localPath: nil, caption: ""), - time: "12:37", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader(senderName: "Bob", snippet: "the doc", minithumbnail: nil, isOutgoing: false) ).bubblePreview() } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift index 81ead594d2..faba8db478 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift @@ -5,7 +5,7 @@ import UIKit /// Renders one location / venue bubble: a rounded static map image /// (`MKMapSnapshotter`) with a centered pin, a "LIVE" badge for live -/// locations, and an inside time footer. The snapshot renders in `.task(id:)`, +/// locations. The snapshot renders in `.task(id:)`, /// keyed by the cache key, so live-location coordinate changes re-render /// automatically. /// @@ -17,7 +17,6 @@ import UIKit /// Tap builds an `MKMapItem` and hands the coordinate to the system Maps app. struct LocationBubbleView: View { let location: LocationVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -91,7 +90,7 @@ struct LocationBubbleView: View { .frame(width: mapWidth, alignment: .leading) .background(style.fill) .foregroundStyle(style.content) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } @ViewBuilder @@ -107,9 +106,8 @@ struct LocationBubbleView: View { private func mapView(roundedCorners: Bool) -> some View { mapImage .frame(width: mapWidth, height: mapHeight) - .clipShape(RoundedRectangle(cornerRadius: roundedCorners ? 12 : 0)) + .clipShape(RoundedRectangle(cornerRadius: roundedCorners ? BubbleShape.cornerRadius : 0)) .overlay { pin } - .overlay(alignment: .bottomTrailing) { timeFooter } .contentShape(Rectangle()) .onTapGesture { openInMaps() } } @@ -150,16 +148,6 @@ struct LocationBubbleView: View { } } - private var timeFooter: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) - } - private func openInMaps() { let mapItem = MKMapItem( location: CLLocation(latitude: location.latitude, longitude: location.longitude), @@ -178,7 +166,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -190,7 +178,7 @@ struct LocationBubbleView: View { title: "Eiffel Tower", address: "Champ de Mars, 75007 Paris", isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:35", isOutgoing: true, replyHeader: nil + isOutgoing: true, replyHeader: nil ) .bubblePreview() } @@ -202,7 +190,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: true, heading: 90, isExpired: false, liveUpdatedAt: Date().addingTimeInterval(-90) ), - time: "12:36", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -214,7 +202,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: true, heading: 0, isExpired: true, liveUpdatedAt: Date().addingTimeInterval(-7200) ), - time: "12:30", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -226,7 +214,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:34", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "where are you?", diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift index ea0c79e6b1..aa1322c4e7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift @@ -24,7 +24,6 @@ struct MessageBubbleView: View { sticker: sticker, senderName: bubble.senderName, senderColorIndex: bubble.senderColorIndex, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) @@ -32,7 +31,6 @@ struct MessageBubbleView: View { PhotoBubbleView( photo: photo, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: onPhotoTap @@ -41,7 +39,6 @@ struct MessageBubbleView: View { VideoBubbleView( video: video, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: { onVideoTap(video) } @@ -49,7 +46,6 @@ struct MessageBubbleView: View { } else if let note = bubble.videoNote { VideoNoteBubbleView( note: note, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: { onVideoNoteTap(note) } @@ -58,38 +54,31 @@ struct MessageBubbleView: View { VoiceNoteBubbleView( note: voice, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, - replyHeader: bubble.replyHeader, - sendingState: bubble.sendingState + replyHeader: bubble.replyHeader ) } else if let audio = bubble.audio { AudioBubbleView( audio: audio, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, - replyHeader: bubble.replyHeader, - sendingState: bubble.sendingState + replyHeader: bubble.replyHeader ) } else if let document = bubble.document { DocumentBubbleView( document: document, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) } else if let location = bubble.location { LocationBubbleView( location: location, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) } else if let poll = bubble.poll { PollBubbleView( poll: poll, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onVote: { onPollTap(bubble.messageId, poll) } @@ -101,14 +90,7 @@ struct MessageBubbleView: View { textBubbleContent } } - .overlay(alignment: .bottomLeading) { - if bubble.isUnreadOutgoing { - Circle() - .fill(Color.accentColor) - .frame(width: 7, height: 7) - .offset(x: -11) - } - } + .overlay(alignment: .bottomLeading) { statusIndicator } if !bubble.isOutgoing { Spacer(minLength: 16) } } .accessibilityIdentifier("bubble.\(bubble.messageId)") @@ -127,42 +109,42 @@ struct MessageBubbleView: View { if let header = bubble.replyHeader { ReplyHeaderView(header: header, style: BubbleStyle.resolve(isOutgoing: bubble.isOutgoing)) } - HStack(alignment: .lastTextBaseline, spacing: 4) { - Text(bubble.body) - .font(.caption) - .fixedSize(horizontal: false, vertical: true) - HStack(spacing: 2) { - Text(bubble.time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if bubble.isOutgoing { - sendStateGlyph - } - } - } + Text(bubble.body) + .font(.caption) + .fixedSize(horizontal: false, vertical: true) } - .padding(.horizontal, 8) + .padding(.horizontal, 9) .padding(.vertical, 4) + .frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) } + /// The single outgoing delivery indicator, in the bubble's leading gutter. + /// Glyphs sit slightly further left than the dot so they clear the bubble edge. @ViewBuilder - private var sendStateGlyph: some View { - switch bubble.sendingState { - case .sent: + private var statusIndicator: some View { + switch bubble.outgoingStatus { + case .none: EmptyView() + case .unread: + Circle() + .fill(Color.accentColor) + .frame(width: 7, height: 7) + .offset(x: -11) case .pending: Image(systemName: "clock") .font(.system(size: 8)) .foregroundStyle(Color.white.opacity(0.7)) + .offset(x: -13) case .failed: Image(systemName: "exclamationmark.circle.fill") .font(.system(size: 8)) .foregroundStyle(.red) + .offset(x: -13) } } } @@ -171,7 +153,6 @@ struct MessageBubbleView: View { private let previewSampleBubble = MessageBubble( messageId: 1, isOutgoing: false, senderName: "Alice", body: "this is a reply to text", - time: "12:34", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: ReplyHeader( @@ -197,7 +178,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 2, isOutgoing: true, senderName: nil, body: "ok", - time: "12:35", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: ReplyHeader( @@ -219,7 +199,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 3, isOutgoing: false, senderName: "Alice", body: "colored name above me", - time: "12:36", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil, @@ -236,7 +215,7 @@ private let previewSampleBubble = MessageBubble( #Preview("Emoji only — incoming 1") { MessageBubbleView( bubble: MessageBubble( - messageId: 10, isOutgoing: false, senderName: nil, body: "🥰", time: "12:40", + messageId: 10, isOutgoing: false, senderName: nil, body: "🥰", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil ), @@ -250,7 +229,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 12, isOutgoing: true, senderName: nil, body: "delivered but not yet read", - time: "12:42", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil, isUnreadOutgoing: true @@ -263,7 +241,7 @@ private let previewSampleBubble = MessageBubble( #Preview("Emoji only — outgoing 3") { MessageBubbleView( bubble: MessageBubble( - messageId: 11, isOutgoing: true, senderName: nil, body: "😀🎉🥰", time: "12:41", + messageId: 11, isOutgoing: true, senderName: nil, body: "😀🎉🥰", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil ), diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift index 57e6b44660..1a206b5ea7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift @@ -24,8 +24,6 @@ struct MessageBubble: Equatable, Hashable { let senderName: String? /// Body text — for text bubbles, the message body; for photo / video bubbles, the caption (may be ""). let body: String - /// `HH:mm` of the message date in the user's time zone. - let time: String /// Photo metadata for `messagePhoto` content; nil for non-photo bubbles. let photo: PhotoVisual? /// Video metadata for `messageVideo` content; nil for non-video bubbles. @@ -57,6 +55,27 @@ struct MessageBubble: Equatable, Hashable { var senderColorIndex: Int? = nil /// True for a sent outgoing message the recipient hasn't read yet (`id > lastReadOutboxMessageId`). var isUnreadOutgoing: Bool = false + + /// The delivery-status indicator to render for this bubble. Outgoing only; + /// `isUnreadOutgoing` already encodes "sent, unread, not Saved Messages". + var outgoingStatus: OutgoingStatus { + guard isOutgoing else { return .none } + switch sendingState { + case .pending: return .pending + case .failed: return .failed + case .sent: return isUnreadOutgoing ? .unread : .none + } + } +} + +/// Collapses an outgoing message's delivery state into the single indicator the +/// chat UI shows at the bubble's bottom-leading corner. Incoming messages — and +/// outgoing messages that are sent and already read — show nothing. +enum OutgoingStatus: Equatable { + case none + case pending + case failed + case unread } struct ServiceLine: Equatable, Hashable { @@ -106,12 +125,6 @@ func messageRows( dayKeyFormatter.timeZone = calendar.timeZone dayKeyFormatter.dateFormat = "yyyy-MM-dd" - let timeFormatter = DateFormatter() - timeFormatter.calendar = calendar - timeFormatter.timeZone = calendar.timeZone - timeFormatter.locale = locale - timeFormatter.dateFormat = "HH:mm" - var rows: [MessageRow] = [] var lastDayKey: String? = nil var dividerPlaced = false @@ -148,7 +161,6 @@ func messageRows( isOutgoing: msg.isOutgoing, senderName: sender?.name, body: messageBody(msg.content), - time: timeFormatter.string(from: date), photo: photoVisual(for: msg.content, fileLocals: fileLocals), video: videoVisual(for: msg.content, fileLocals: fileLocals), videoNote: videoNoteVisual(for: msg.content, fileLocals: fileLocals), diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift index 1826687921..63a51c47cd 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift @@ -3,7 +3,7 @@ import UIKit /// Renders one photo bubble: aspect-fit image (full-resolution when downloaded, /// minithumbnail otherwise, gray placeholder if neither), optional caption below the -/// image, time stamp footer. +/// image. /// /// Drives viewport-based download via `.onScrollVisibilityChange` on the store — /// `.onAppear` would fire for every off-screen bubble too, because the enclosing @@ -13,7 +13,6 @@ import UIKit struct PhotoBubbleView: View { let photo: PhotoVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: (PhotoVisual) -> Void @@ -45,9 +44,8 @@ struct PhotoBubbleView: View { private var bareImage: some View { imageView .frame(width: displaySize.width, height: displaySize.height) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .bottomTrailing) { timePill } - .contentShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) + .contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) .onTapGesture { if photo.localPath != nil { onTap(photo) } } } @@ -69,25 +67,10 @@ struct PhotoBubbleView: View { .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 8) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - .padding(.horizontal, 8) - .padding(.bottom, 4) } .frame(width: displaySize.width, alignment: .leading) .background(style.fill) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - - private var timePill: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } @ViewBuilder @@ -154,7 +137,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "look at this", - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", @@ -172,7 +154,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: { _ in } @@ -185,7 +166,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "Benji athlete today!", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: { _ in } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift index def98f8d13..44344fdc9e 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift @@ -2,12 +2,11 @@ import SwiftUI /// Renders one poll / quiz bubble inside the standard gray-incoming / accent-outgoing /// chrome. Shows the question, option rows (with result bars + percentages once -/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count + time -/// footer, and a "Vote" button when the poll is still actionable. Tapping Vote calls +/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count footer, +/// and a "Vote" button when the poll is still actionable. Tapping Vote calls /// `onVote`, which opens the dedicated `PollVoteView`. struct PollBubbleView: View { let poll: PollVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onVote: () -> Void @@ -63,18 +62,14 @@ struct PollBubbleView: View { .buttonStyle(.bordered) .controlSize(.mini) } - HStack(spacing: 4) { - Text(voterCountLabel) - Spacer(minLength: 0) - Text(time) - } - .font(.system(size: 9)) - .foregroundStyle(secondaryColor) + Text(voterCountLabel) + .font(.system(size: 9)) + .foregroundStyle(secondaryColor) } .padding(8) - .frame(maxWidth: maxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: maxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -147,7 +142,7 @@ private func previewOption( totalVoterCount: 0, hasVoted: false, explanation: nil, options: [previewOption("Red", position: 0), previewOption("Green", position: 1), previewOption("Blue", position: 2)] ), - time: "12:34", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -160,7 +155,7 @@ private func previewOption( options: [previewOption("Red", position: 0, pct: 75, chosen: true), previewOption("Green", position: 1, pct: 25)] ), - time: "12:35", isOutgoing: true, replyHeader: nil, onVote: {} + isOutgoing: true, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -174,7 +169,7 @@ private func previewOption( previewOption("Paris", position: 1, pct: 60, correct: true), previewOption("Rome", position: 2, pct: 10, correct: false)] ), - time: "12:36", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -186,7 +181,7 @@ private func previewOption( totalVoterCount: 12, hasVoted: false, explanation: nil, options: [previewOption("Sat", position: 0, pct: 58), previewOption("Sun", position: 1, pct: 42)] ), - time: "09:00", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } #endif diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift index 5707ad0119..5aa818ff17 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift @@ -30,7 +30,7 @@ struct ReviewVoiceBubble: View { } .padding(.horizontal, 8).padding(.vertical, 6) .frame(maxWidth: 200, alignment: .leading) - .background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor)) + .background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(Color.accentColor)) .foregroundStyle(.white) .contentShape(Rectangle()) .onTapGesture { controller.toggle(note: note) } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift index 7e7503ac0e..629361ceae 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift @@ -3,7 +3,7 @@ import UIKit /// Renders one video bubble: aspect-fit preview thumbnail (downloaded preview, blurred /// minithumbnail, or gray placeholder), centered play overlay, top-trailing duration -/// pill, optional caption below the image, time stamp footer. +/// pill, optional caption below the image. /// /// Drives viewport-based download of the *preview* file id (not the full video) via /// `.onScrollVisibilityChange` on the store — `.onAppear` would fire for every @@ -15,7 +15,6 @@ import UIKit struct VideoBubbleView: View { let video: VideoVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: () -> Void @@ -46,8 +45,8 @@ struct VideoBubbleView: View { } private var bareVideo: some View { - videoContent(clipImage: true, showTimePill: true) - .contentShape(RoundedRectangle(cornerRadius: 12)) + videoContent(clipImage: true) + .contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) .onTapGesture { onTap() } } @@ -58,7 +57,7 @@ struct VideoBubbleView: View { .padding(.horizontal, 8) .padding(.top, 6) } - videoContent(clipImage: false, showTimePill: false) + videoContent(clipImage: false) .contentShape(Rectangle()) .onTapGesture { onTap() } if !caption.isEmpty { @@ -68,22 +67,17 @@ struct VideoBubbleView: View { .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 8) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - .padding(.horizontal, 8) - .padding(.bottom, 4) } .frame(width: displaySize.width, alignment: .leading) .background(style.fill) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } - private func videoContent(clipImage: Bool, showTimePill: Bool) -> some View { + private func videoContent(clipImage: Bool) -> some View { ZStack { imageView .frame(width: displaySize.width, height: displaySize.height) - .clipShape(RoundedRectangle(cornerRadius: clipImage ? 12 : 0)) + .clipShape(RoundedRectangle(cornerRadius: clipImage ? BubbleShape.cornerRadius : 0)) .overlay(alignment: .topTrailing) { Text(formatDuration(video.duration)) .font(.system(size: 9)) @@ -93,23 +87,10 @@ struct VideoBubbleView: View { .background(Capsule().fill(.black.opacity(0.5))) .padding(4) } - .overlay(alignment: .bottomTrailing) { - if showTimePill { timePill } - } playOverlay } } - private var timePill: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) - } - @ViewBuilder private var imageView: some View { if let path = video.preview.previewLocalPath, let img = UIImage(contentsOfFile: path) { @@ -186,7 +167,6 @@ private func videoPreviewStore() -> ChatHistoryStore { videoLocalPath: nil ), caption: "check this clip", - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", @@ -209,7 +189,6 @@ private func videoPreviewStore() -> ChatHistoryStore { videoLocalPath: nil ), caption: "", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: {} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift index f42a24ce15..0e1b41ffad 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift @@ -4,8 +4,7 @@ import UIKit /// Renders one round-video-note bubble: a chrome-less 150 pt circle with the /// preview image (downloaded thumb → blurred minithumbnail → gray placeholder) /// behind a centered play overlay and a bottom-inside duration pill. Optional -/// reply mini-card (`StickerBubbleView` precedent) sits above. Timestamp footer -/// renders below as plain text on the system background. +/// reply mini-card (`StickerBubbleView` precedent) sits above. /// /// Drives viewport-based download of the thumbnail file id (not the playable /// video file) via `.onScrollVisibilityChange`. The playable file is downloaded @@ -14,7 +13,6 @@ import UIKit /// Tap is unconditional — the viewer handles the not-yet-downloaded state. struct VideoNoteBubbleView: View { let note: VideoNoteVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: () -> Void @@ -61,10 +59,6 @@ struct VideoNoteBubbleView: View { store.cancelFileDownload(fileId: id) } } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - .padding(.horizontal, 4) } } @@ -126,7 +120,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: {} @@ -142,7 +135,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:36", isOutgoing: true, replyHeader: nil, onTap: {} @@ -158,7 +150,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift index 67585cb7a0..881516901a 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift @@ -4,7 +4,6 @@ import SwiftUI /// outgoing) containing a play/pause glyph + 32-bar waveform + duration /// label. Optional reply header sits inside the chrome above the row. /// Caption (when non-empty) renders below the waveform inside the chrome. -/// Time footer renders below the chrome. /// /// Drives priority-1 viewport download via `.onScrollVisibilityChange`. /// Tap goes through `ChatHistoryStore.togglePlayback(_:)`; the store kicks @@ -13,10 +12,8 @@ import SwiftUI struct VoiceNoteBubbleView: View { let note: VoiceNoteVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? - let sendingState: SendingState @Environment(ChatHistoryStore.self) private var store @Environment(\.bubbleMetrics) private var metrics @@ -58,13 +55,12 @@ struct VoiceNoteBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - timeRow } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -113,28 +109,6 @@ struct VoiceNoteBubbleView: View { .fixedSize(horizontal: true, vertical: false) } - // Time + send-state inline at the bottom-right inside the chrome, matching the - // text-bubble convention. - private var timeRow: some View { - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if isOutgoing { sendStateGlyph } - } - } - - @ViewBuilder - private var sendStateGlyph: some View { - switch sendingState { - case .sent: EmptyView() - case .pending: Image(systemName: "clock").font(.system(size: 8)) - .foregroundStyle(Color.white.opacity(0.7)) - case .failed: Image(systemName: "exclamationmark.circle.fill").font(.system(size: 8)) - .foregroundStyle(.red) - } - } } #if DEBUG @@ -178,8 +152,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — incoming, idle") { VoiceNoteBubbleView( note: previewVoice(), caption: "", - time: "12:34", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -188,8 +162,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — outgoing, idle") { VoiceNoteBubbleView( note: previewVoice(id: 2), caption: "", - time: "12:35", isOutgoing: true, - replyHeader: nil, sendingState: .sent + isOutgoing: true, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -198,13 +172,12 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — incoming with reply") { VoiceNoteBubbleView( note: previewVoice(id: 3), caption: "", - time: "12:36", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "anchor message earlier in the chat", minithumbnail: nil, isOutgoing: false - ), - sendingState: .sent + ) ) .bubblePreview() .environment(previewStore()) @@ -214,8 +187,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { VoiceNoteBubbleView( note: previewVoice(id: 4), caption: "Listen to this part carefully", - time: "12:37", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -227,8 +200,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { voiceFileId: 5, duration: 5, mimeType: "audio/ogg", waveform: Data(), caption: "", localPath: nil ), - caption: "", time: "12:38", isOutgoing: false, - replyHeader: nil, sendingState: .sent + caption: "", isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) diff --git a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift index eef28ef49d..d07c38fb5b 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift @@ -2,13 +2,12 @@ import SwiftUI import TDLibKit /// One sticker bubble. No rounded-rect chat-bubble chrome — sticker renders directly -/// against the chat background, with sender name above (incoming groups only) and time -/// stamp below. Drives the same visibility-based download flow as PhotoBubbleView. +/// against the chat background, with sender name above (incoming groups only). +/// Drives the same visibility-based download flow as PhotoBubbleView. struct StickerBubbleView: View { let sticker: StickerVisual let senderName: String? var senderColorIndex: Int? = nil - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -47,10 +46,6 @@ struct StickerBubbleView: View { .onScrollVisibilityChange(threshold: 0.01) { visible in handleVisibility(visible) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - .padding(.horizontal, 4) } } @@ -76,8 +71,9 @@ struct StickerBubbleView: View { .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) + .frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -182,7 +178,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloaded") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -191,7 +187,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloading, thumbnail available") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: false, withThumb: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -200,7 +196,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloading, no thumbnail") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: false), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -209,7 +205,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("TGS — downloaded") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -218,7 +214,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("TGS — downloading, thumbnail available") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: false, withThumb: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -227,7 +223,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBM — unsupported fallback") { StickerBubbleView( sticker: previewSticker(format: .unsupported, withFile: false), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -236,7 +232,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("Group sticker — sender name above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: "Alice", time: "12:34", isOutgoing: false, replyHeader: nil + senderName: "Alice", isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -246,7 +242,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), senderName: "Alice", senderColorIndex: paletteIndex(for: 200), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -255,7 +251,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("Outgoing sticker") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: true, replyHeader: nil + senderName: nil, isOutgoing: true, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -275,7 +271,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker WEBP — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, + senderName: nil, isOutgoing: false, replyHeader: previewReplyHeader(toText: "anchor message") ) .bubblePreview() @@ -285,7 +281,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker TGS — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, + senderName: nil, isOutgoing: false, replyHeader: previewReplyHeader(toText: "look at this clip") ) .bubblePreview() @@ -295,7 +291,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker outgoing — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: true, + senderName: nil, isOutgoing: true, replyHeader: previewReplyHeader(toText: "thanks!", isOutgoing: true) ) .bubblePreview() diff --git a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj index 9b0fe1557e..6514e9d26f 100644 --- a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj +++ b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ C62226963E54795B2808A52C /* MapSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFF41E60C5477C20897CD00 /* MapSnapshotRenderer.swift */; }; C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */; }; C94BD040D985FC0090CDBF84 /* StickerPickerLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC230FDDDFAA063019744A0E /* StickerPickerLoader.swift */; }; + CBA3FEDEEA413559F700B6E3 /* BubbleShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */; }; D1082945B5785B08F820FB78 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93535C2021AB2E7B17F593AA /* LoadingView.swift */; }; D1E96E63A3AE93646AD3F532 /* AudioBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C3B71804A398A0100375E86 /* AudioBubbleView.swift */; }; D5515C5A9286E0372DBFB7F9 /* QRCodeGenerator in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A04697FEA6C4614973F19 /* QRCodeGenerator */; }; @@ -206,6 +207,7 @@ D4AF5A180BB3A37832AC5349 /* VideoNoteBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteBubbleView.swift; sourceTree = ""; }; D50662DC1777F2BEE53A0129 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D9CD96277A18D97BD335BBC0 /* StickerSetDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSetDetailView.swift; sourceTree = ""; }; + DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleShape.swift; sourceTree = ""; }; DDCE599C8D50EE8832B4A797 /* ReplyFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyFormatters.swift; sourceTree = ""; }; DF0F4F15CB43635717B31B92 /* PollVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteView.swift; sourceTree = ""; }; DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceMessageView.swift; sourceTree = ""; }; @@ -335,6 +337,7 @@ CBAFC996C8EF743C7C598389 /* AudioVisual.swift */, 0F9127473C7DAD18736234B3 /* AVRecordingBackend.swift */, 674916153E803CA73D43BE6C /* BubbleMetrics.swift */, + DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */, 3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */, C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */, 182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */, @@ -520,6 +523,7 @@ 5E9A6B94F1B917CB849B3EBC /* AvatarView.swift in Sources */, 03074928DC54019BE6106ECD /* AvatarVisual.swift in Sources */, 4236CA9B5AA90ACEB17814DC /* BubbleMetrics.swift in Sources */, + CBA3FEDEEA413559F700B6E3 /* BubbleShape.swift in Sources */, 868426A2768FE7349CD4AAAD /* BubbleStyle.swift in Sources */, 4505C5628AF9258938D623D7 /* CachedChat.swift in Sources */, C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */,