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 diff --git a/CLAUDE.md b/CLAUDE.md index a73f8e41c7..c0a8abfcd8 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`. @@ -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/Telegram/BUILD b/Telegram/BUILD index 0ed745668f..59cc632763 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1746,6 +1746,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/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 0000000000..92c90ecdf9 Binary files /dev/null and b/Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png differ 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/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..5f41b221b7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Chats/ChatListView.swift @@ -13,39 +13,35 @@ 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) } + .navigationDestination(for: ChatRow.self) { row in + MessageListView(row: row) + } + .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) } - // 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") } @@ -96,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 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/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/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/MessageListView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageListView.swift index cd691449f0..d653925466 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 @@ -57,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) } @@ -79,18 +89,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 +241,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 +261,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/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/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 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 ab950ef6f3..6514e9d26f 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 */; }; @@ -93,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 */; }; @@ -149,7 +149,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 = ""; }; @@ -208,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 = ""; }; @@ -337,10 +337,10 @@ CBAFC996C8EF743C7C598389 /* AudioVisual.swift */, 0F9127473C7DAD18736234B3 /* AVRecordingBackend.swift */, 674916153E803CA73D43BE6C /* BubbleMetrics.swift */, + DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */, 3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */, C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */, 182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */, - 54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */, E855E4C62401AD1CF41D1835 /* DaySeparatorView.swift */, BDBE08BBB88597B10CDF0142 /* DocumentBubbleView.swift */, FF2515F0EDA29D5270610A69 /* DocumentVisual.swift */, @@ -523,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 */, @@ -536,7 +537,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 */, diff --git a/Telegram/prebuilt_watchos.bzl b/Telegram/prebuilt_watchos.bzl index a72b955dd5..69f25f04f1 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 @@ -63,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", @@ -84,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, ) @@ -104,12 +143,14 @@ 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], 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, ) 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 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). 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 6a5125be13..8875493226 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -849,7 +849,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/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: diff --git a/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift b/submodules/InstantPageUI/Sources/InstantPageV2Layout.swift index 5d548a2b80..2b55936e95 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 @@ -585,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) { @@ -1892,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, @@ -2005,21 +2094,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 } } @@ -2037,12 +2122,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 @@ -2076,13 +2167,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] = [] @@ -2134,8 +2232,6 @@ private func layoutList( naturalWidth: markerInfo.naturalWidth, maxIndexWidth: maxIndexWidth, horizontalInset: horizontalInset, - indexSpacing: indexSpacing, - ordered: ordered, checklistMarkerSize: checklistMarkerSize, lineMidY: lineMidY, rtl: context.rtl, @@ -2198,22 +2294,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, @@ -2235,57 +2331,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) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index b187a883b4..63c7349d2d 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -146,7 +146,7 @@ public final class LegacyJoinLinkPreviewController: ViewController { 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 let peer { strongSelf.navigateToPeer(peer, nil) } } diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift index d1825cb09f..d740cba975 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift @@ -342,6 +342,13 @@ private final class OpenInOptionsSheetContentComponent: Component { } let optionInset: CGFloat = 2.0 + var optionSpacing: CGFloat = 8.0 + if component.options.count == 3 { + optionSpacing = 32.0 + } else if component.options.count == 2 { + optionSpacing = 64.0 + } + let optionSize = CGSize(width: 80.0, height: 112.0) let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: 82.0), size: CGSize(width: availableSize.width, height: optionSize.height)) transition.setFrame(view: self.scrollView, frame: scrollFrame) @@ -366,9 +373,9 @@ private final class OpenInOptionsSheetContentComponent: Component { let optionOriginX: CGFloat if component.options.count < 5 { - optionOriginX = floorToScreenPixels(max(0.0, (availableSize.width - optionSize.width * CGFloat(component.options.count)) / 2.0)) + optionOriginX = floorToScreenPixels(max(0.0, (availableSize.width - optionSize.width * CGFloat(component.options.count) - optionSpacing * CGFloat(component.options.count - 1)) / 2.0)) + CGFloat(index) * (optionSize.width + optionSpacing) } else { - optionOriginX = optionInset + CGFloat(index) * optionSize.width + optionOriginX = optionInset + CGFloat(index) * (optionSize.width + optionSpacing) } transition.setFrame(view: optionView, frame: CGRect( @@ -388,7 +395,7 @@ private final class OpenInOptionsSheetContentComponent: Component { if component.options.isEmpty { optionsContentWidth = availableSize.width } else { - optionsContentWidth = optionInset * 2.0 + CGFloat(component.options.count) * optionSize.width + optionsContentWidth = optionInset * 2.0 + CGFloat(component.options.count) * (optionSize.width + optionSpacing) - optionSpacing } self.scrollView.contentSize = CGSize(width: max(availableSize.width, optionsContentWidth), height: optionSize.height) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index e8891a9c64..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) } @@ -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/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/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/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 211951e578..4241837632 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)) } } } @@ -251,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)) @@ -331,7 +328,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 } } @@ -371,12 +368,18 @@ 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() })) } - 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/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/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(_, _, _, _): 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 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)) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index a2e170fa23..61bfacc05c 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, case let .joined(updated) = result, let updated { - strongSelf.impl.with { impl in + if let self { + self.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 + } } } } 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