mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
4b2a826682
59 changed files with 2031 additions and 1350 deletions
|
|
@ -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"
|
- 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-workdir
|
||||||
- rm -rf build-input/configuration-repository
|
- 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"
|
- 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:
|
environment:
|
||||||
name: internal
|
name: internal
|
||||||
|
|
|
||||||
30
CLAUDE.md
30
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.
|
**`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`.
|
**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.
|
- **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.
|
- **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)
|
## Postbox → TelegramEngine refactor (in progress)
|
||||||
|
|
||||||
A gradual migration is underway to eliminate direct `import Postbox` from consumer submodules in favor of `TelegramEngine`.
|
A gradual migration is underway to eliminate direct `import Postbox` from consumer submodules in favor of `TelegramEngine`.
|
||||||
|
|
|
||||||
|
|
@ -1746,6 +1746,14 @@ xcode_provisioning_profile(
|
||||||
|
|
||||||
apple_prebuilt_watchos_application(
|
apple_prebuilt_watchos_application(
|
||||||
name = "TelegramWatchApp",
|
name = "TelegramWatchApp",
|
||||||
|
# The watch app's bundle id must be `<host>.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"],
|
tags = ["manual"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
BIN
Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png
vendored
Normal file
BIN
Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/ChatBG.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
13
Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json
vendored
Normal file
13
Telegram/WatchApp/tgwatch Watch App/Assets.xcassets/ChatBG.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "ChatBG.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
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
|
/// crisp downloaded photo; blurred minithumbnail placeholder; initials on a
|
||||||
/// per-chat color. Drives viewport-based download of the small photo via
|
/// per-chat color. When embedded in a `ScrollView`, drives viewport-based download
|
||||||
/// `.onScrollVisibilityChange` (same as `PhotoBubbleView`) — only while the photo
|
/// of the small photo via `.onScrollVisibilityChange` (same as `PhotoBubbleView`) —
|
||||||
/// exists and hasn't downloaded yet.
|
/// 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 {
|
struct AvatarView: View {
|
||||||
let avatar: AvatarVisual
|
let avatar: AvatarVisual
|
||||||
var onRequestDownload: (Int) -> Void = { _ in }
|
var onRequestDownload: (Int) -> Void = { _ in }
|
||||||
var onCancelDownload: (Int) -> Void = { _ in }
|
var onCancelDownload: (Int) -> Void = { _ in }
|
||||||
|
|
||||||
private let size: CGFloat = 36
|
var size: CGFloat = 36
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content
|
content
|
||||||
|
|
|
||||||
|
|
@ -13,39 +13,35 @@ struct ChatListView: View {
|
||||||
@FocusState private var listFocused: Bool
|
@FocusState private var listFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
NavigationStack {
|
||||||
NavigationStack {
|
ScrollViewReader { proxy in
|
||||||
ScrollViewReader { proxy in
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
if case .failed(let message) = store.loadState(for: store.currentFolder) {
|
||||||
if case .failed(let message) = store.loadState(for: store.currentFolder) {
|
banner(text: message, kind: .retry)
|
||||||
banner(text: message, kind: .retry)
|
|
||||||
}
|
|
||||||
if let err = client.lastError, dismissedLastError != err {
|
|
||||||
banner(text: err, kind: .dismiss)
|
|
||||||
}
|
|
||||||
content(proxy: proxy)
|
|
||||||
}
|
}
|
||||||
.navigationDestination(for: ChatRow.self) { row in
|
if let err = client.lastError, dismissedLastError != err {
|
||||||
MessageListView(row: row)
|
banner(text: err, kind: .dismiss)
|
||||||
}
|
}
|
||||||
.navigationTitle("")
|
content(proxy: proxy)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
// Must live INSIDE the NavigationStack so the modifier's
|
|
||||||
// .navigationDestination(isPresented:) attaches to it.
|
|
||||||
.accountSwitcherSheet(presentation: .push, logoutAffordance: .allowed)
|
|
||||||
}
|
}
|
||||||
|
.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")
|
.accessibilityIdentifier("chatListView")
|
||||||
}
|
}
|
||||||
|
|
@ -96,18 +92,13 @@ struct ChatListView: View {
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.focused($listFocused)
|
.focused($listFocused)
|
||||||
.onAppear { listFocused = true }
|
.onAppear { listFocused = true }
|
||||||
// Hide the empty title bar so the chat list starts directly under the
|
// Bar is intentionally visible (forced by `.toolbar(.visible, for:
|
||||||
// system clock. Pill bar is the first List row, so it scrolls with content.
|
// .navigationBar)` on the body chain). When folder pills are present,
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
// pull the pill row up by 24pt to tuck it under the bar's bottom edge
|
||||||
// `.toolbar(.hidden)` hides the bar visuals but watchOS still reserves
|
// — without that, the natural top-of-list inset leaves an awkward gap
|
||||||
// ~30pt of layout. With a pill bar present, pull up by 38 (slot + 8)
|
// between the bar and the pill row. Plain chat rows (no pills) sit at
|
||||||
// so the pill row sits ~8pt under the clock — the gradient masks any
|
// the natural top.
|
||||||
// overlap, and the negative bottom inset on the pill row tightens the
|
.padding(.top, store.pills.count > 1 ? -20 : 0)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import TDLibKit
|
||||||
/// is summed across all chats whose `positions` include `chatList`.
|
/// is summed across all chats whose `positions` include `chatList`.
|
||||||
struct FolderPill: Identifiable, Equatable, Hashable {
|
struct FolderPill: Identifiable, Equatable, Hashable {
|
||||||
static let allChatsId: Int = -1
|
static let allChatsId: Int = -1
|
||||||
static let allChatsName = "All chats"
|
static let allChatsName = "All"
|
||||||
|
|
||||||
let id: Int
|
let id: Int
|
||||||
let chatList: ChatList
|
let chatList: ChatList
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,8 @@ import SwiftUI
|
||||||
struct AudioBubbleView: View {
|
struct AudioBubbleView: View {
|
||||||
let audio: AudioVisual
|
let audio: AudioVisual
|
||||||
let caption: String
|
let caption: String
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let sendingState: SendingState
|
|
||||||
|
|
||||||
@Environment(ChatHistoryStore.self) private var store
|
@Environment(ChatHistoryStore.self) private var store
|
||||||
@Environment(\.bubbleMetrics) private var metrics
|
@Environment(\.bubbleMetrics) private var metrics
|
||||||
|
|
@ -73,13 +71,12 @@ struct AudioBubbleView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
timeRow
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading)
|
.frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)
|
||||||
.fill(style.fill)
|
.fill(style.fill)
|
||||||
)
|
)
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
|
|
@ -118,28 +115,6 @@ struct AudioBubbleView: View {
|
||||||
.shadow(radius: audio.albumArt != nil ? 1 : 0)
|
.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
|
#if DEBUG
|
||||||
|
|
@ -186,8 +161,8 @@ private func previewAudio(
|
||||||
#Preview("Audio — incoming, idle") {
|
#Preview("Audio — incoming, idle") {
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(), caption: "",
|
audio: previewAudio(), caption: "",
|
||||||
time: "12:34", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -196,8 +171,8 @@ private func previewAudio(
|
||||||
#Preview("Audio — outgoing, idle") {
|
#Preview("Audio — outgoing, idle") {
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(id: 2), caption: "",
|
audio: previewAudio(id: 2), caption: "",
|
||||||
time: "12:35", isOutgoing: true,
|
isOutgoing: true,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -207,8 +182,8 @@ private func previewAudio(
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(id: 3, title: "A Very Long Track Title That Will Truncate", performer: "Some Long Performer Name"),
|
audio: previewAudio(id: 3, title: "A Very Long Track Title That Will Truncate", performer: "Some Long Performer Name"),
|
||||||
caption: "",
|
caption: "",
|
||||||
time: "12:36", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -218,8 +193,8 @@ private func previewAudio(
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(id: 4, title: "Untitled", performer: ""),
|
audio: previewAudio(id: 4, title: "Untitled", performer: ""),
|
||||||
caption: "",
|
caption: "",
|
||||||
time: "12:37", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -229,8 +204,8 @@ private func previewAudio(
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(id: 5),
|
audio: previewAudio(id: 5),
|
||||||
caption: "this song goes hard",
|
caption: "this song goes hard",
|
||||||
time: "12:38", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -239,13 +214,12 @@ private func previewAudio(
|
||||||
#Preview("Audio — incoming with reply") {
|
#Preview("Audio — incoming with reply") {
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: previewAudio(id: 6), caption: "",
|
audio: previewAudio(id: 6), caption: "",
|
||||||
time: "12:39", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
snippet: "anchor message earlier in the chat",
|
snippet: "anchor message earlier in the chat",
|
||||||
minithumbnail: nil, isOutgoing: false
|
minithumbnail: nil, isOutgoing: false
|
||||||
),
|
)
|
||||||
sendingState: .sent
|
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Resolved colors for a message-bubble surface. Incoming bubbles are a fixed light (white)
|
/// Resolved colors for a message-bubble surface. Both directions use a dark fixed surface
|
||||||
/// surface with dark content; outgoing are the accent surface with white content. Colors are
|
/// with white content. Colors are FIXED (non-adaptive) on purpose: watchOS runs this app in
|
||||||
/// FIXED (non-adaptive) on purpose: watchOS runs this app in permanent dark mode, so `.primary`
|
/// permanent dark mode.
|
||||||
/// would resolve to white and vanish on the white incoming surface.
|
|
||||||
struct BubbleStyle: Equatable {
|
struct BubbleStyle: Equatable {
|
||||||
let fill: Color
|
let fill: Color
|
||||||
let content: Color
|
let content: Color
|
||||||
|
|
@ -13,15 +12,15 @@ struct BubbleStyle: Equatable {
|
||||||
let playIcon: Color
|
let playIcon: Color
|
||||||
|
|
||||||
static let incoming = BubbleStyle(
|
static let incoming = BubbleStyle(
|
||||||
fill: .white,
|
fill: Color(red: 40 / 255, green: 40 / 255, blue: 40 / 255),
|
||||||
content: .black,
|
content: .white,
|
||||||
secondary: Color.black.opacity(0.5),
|
secondary: Color.white.opacity(0.7),
|
||||||
replyBar: .accentColor,
|
replyBar: .accentColor,
|
||||||
playFill: .accentColor,
|
playFill: .accentColor,
|
||||||
playIcon: .white
|
playIcon: .white
|
||||||
)
|
)
|
||||||
static let outgoing = BubbleStyle(
|
static let outgoing = BubbleStyle(
|
||||||
fill: .accentColor,
|
fill: Color(red: 19 / 255, green: 44 / 255, blue: 73 / 255),
|
||||||
content: .white,
|
content: .white,
|
||||||
secondary: Color.white.opacity(0.7),
|
secondary: Color.white.opacity(0.7),
|
||||||
replyBar: Color.white.opacity(0.7),
|
replyBar: Color.white.opacity(0.7),
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Renders one generic-document bubble: BubbleStyle chrome with a rounded-square doc icon,
|
/// 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).
|
/// watch in milestone #4).
|
||||||
struct DocumentBubbleView: View {
|
struct DocumentBubbleView: View {
|
||||||
let document: DocumentVisual
|
let document: DocumentVisual
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
|
|
||||||
|
|
@ -35,15 +34,11 @@ struct DocumentBubbleView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
HStack(spacing: 2) {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
Text(time).font(.system(size: 8)).foregroundStyle(style.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 6)
|
.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).fill(style.fill))
|
.background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(style.fill))
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +56,7 @@ struct DocumentBubbleView: View {
|
||||||
DocumentBubbleView(
|
DocumentBubbleView(
|
||||||
document: DocumentVisual(documentFileId: 1, fileName: "report.pdf",
|
document: DocumentVisual(documentFileId: 1, fileName: "report.pdf",
|
||||||
sizeBytes: 2_400_000, localPath: nil, caption: ""),
|
sizeBytes: 2_400_000, localPath: nil, caption: ""),
|
||||||
time: "12:34", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +64,7 @@ struct DocumentBubbleView: View {
|
||||||
DocumentBubbleView(
|
DocumentBubbleView(
|
||||||
document: DocumentVisual(documentFileId: 2, fileName: "quarterly-financial-summary-2026.xlsx",
|
document: DocumentVisual(documentFileId: 2, fileName: "quarterly-financial-summary-2026.xlsx",
|
||||||
sizeBytes: 52_428_800, localPath: nil, caption: ""),
|
sizeBytes: 52_428_800, localPath: nil, caption: ""),
|
||||||
time: "12:35", isOutgoing: true, replyHeader: nil
|
isOutgoing: true, replyHeader: nil
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +72,7 @@ struct DocumentBubbleView: View {
|
||||||
DocumentBubbleView(
|
DocumentBubbleView(
|
||||||
document: DocumentVisual(documentFileId: 3, fileName: "archive.zip",
|
document: DocumentVisual(documentFileId: 3, fileName: "archive.zip",
|
||||||
sizeBytes: 52_428_800, localPath: nil, caption: "here are the files"),
|
sizeBytes: 52_428_800, localPath: nil, caption: "here are the files"),
|
||||||
time: "12:36", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +80,7 @@ struct DocumentBubbleView: View {
|
||||||
DocumentBubbleView(
|
DocumentBubbleView(
|
||||||
document: DocumentVisual(documentFileId: 4, fileName: "notes.txt",
|
document: DocumentVisual(documentFileId: 4, fileName: "notes.txt",
|
||||||
sizeBytes: 1024, localPath: nil, caption: ""),
|
sizeBytes: 1024, localPath: nil, caption: ""),
|
||||||
time: "12:37", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(senderName: "Bob", snippet: "the doc", minithumbnail: nil, isOutgoing: false)
|
replyHeader: ReplyHeader(senderName: "Bob", snippet: "the doc", minithumbnail: nil, isOutgoing: false)
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import UIKit
|
||||||
|
|
||||||
/// Renders one location / venue bubble: a rounded static map image
|
/// Renders one location / venue bubble: a rounded static map image
|
||||||
/// (`MKMapSnapshotter`) with a centered pin, a "LIVE" badge for live
|
/// (`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
|
/// keyed by the cache key, so live-location coordinate changes re-render
|
||||||
/// automatically.
|
/// automatically.
|
||||||
///
|
///
|
||||||
|
|
@ -17,7 +17,6 @@ import UIKit
|
||||||
/// Tap builds an `MKMapItem` and hands the coordinate to the system Maps app.
|
/// Tap builds an `MKMapItem` and hands the coordinate to the system Maps app.
|
||||||
struct LocationBubbleView: View {
|
struct LocationBubbleView: View {
|
||||||
let location: LocationVisual
|
let location: LocationVisual
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
|
|
||||||
|
|
@ -91,7 +90,7 @@ struct LocationBubbleView: View {
|
||||||
.frame(width: mapWidth, alignment: .leading)
|
.frame(width: mapWidth, alignment: .leading)
|
||||||
.background(style.fill)
|
.background(style.fill)
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -107,9 +106,8 @@ struct LocationBubbleView: View {
|
||||||
private func mapView(roundedCorners: Bool) -> some View {
|
private func mapView(roundedCorners: Bool) -> some View {
|
||||||
mapImage
|
mapImage
|
||||||
.frame(width: mapWidth, height: mapHeight)
|
.frame(width: mapWidth, height: mapHeight)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: roundedCorners ? 12 : 0))
|
.clipShape(RoundedRectangle(cornerRadius: roundedCorners ? BubbleShape.cornerRadius : 0))
|
||||||
.overlay { pin }
|
.overlay { pin }
|
||||||
.overlay(alignment: .bottomTrailing) { timeFooter }
|
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { openInMaps() }
|
.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() {
|
private func openInMaps() {
|
||||||
let mapItem = MKMapItem(
|
let mapItem = MKMapItem(
|
||||||
location: CLLocation(latitude: location.latitude, longitude: location.longitude),
|
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,
|
title: nil, address: nil, isLive: false, heading: 0, isExpired: false,
|
||||||
liveUpdatedAt: nil
|
liveUpdatedAt: nil
|
||||||
),
|
),
|
||||||
time: "12:34", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +178,7 @@ struct LocationBubbleView: View {
|
||||||
title: "Eiffel Tower", address: "Champ de Mars, 75007 Paris",
|
title: "Eiffel Tower", address: "Champ de Mars, 75007 Paris",
|
||||||
isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil
|
isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil
|
||||||
),
|
),
|
||||||
time: "12:35", isOutgoing: true, replyHeader: nil
|
isOutgoing: true, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +190,7 @@ struct LocationBubbleView: View {
|
||||||
title: nil, address: nil, isLive: true, heading: 90, isExpired: false,
|
title: nil, address: nil, isLive: true, heading: 90, isExpired: false,
|
||||||
liveUpdatedAt: Date().addingTimeInterval(-90)
|
liveUpdatedAt: Date().addingTimeInterval(-90)
|
||||||
),
|
),
|
||||||
time: "12:36", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +202,7 @@ struct LocationBubbleView: View {
|
||||||
title: nil, address: nil, isLive: true, heading: 0, isExpired: true,
|
title: nil, address: nil, isLive: true, heading: 0, isExpired: true,
|
||||||
liveUpdatedAt: Date().addingTimeInterval(-7200)
|
liveUpdatedAt: Date().addingTimeInterval(-7200)
|
||||||
),
|
),
|
||||||
time: "12:30", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +214,7 @@ struct LocationBubbleView: View {
|
||||||
title: nil, address: nil, isLive: false, heading: 0, isExpired: false,
|
title: nil, address: nil, isLive: false, heading: 0, isExpired: false,
|
||||||
liveUpdatedAt: nil
|
liveUpdatedAt: nil
|
||||||
),
|
),
|
||||||
time: "12:34", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
snippet: "where are you?",
|
snippet: "where are you?",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ struct MessageBubbleView: View {
|
||||||
sticker: sticker,
|
sticker: sticker,
|
||||||
senderName: bubble.senderName,
|
senderName: bubble.senderName,
|
||||||
senderColorIndex: bubble.senderColorIndex,
|
senderColorIndex: bubble.senderColorIndex,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader
|
replyHeader: bubble.replyHeader
|
||||||
)
|
)
|
||||||
|
|
@ -32,7 +31,6 @@ struct MessageBubbleView: View {
|
||||||
PhotoBubbleView(
|
PhotoBubbleView(
|
||||||
photo: photo,
|
photo: photo,
|
||||||
caption: bubble.body,
|
caption: bubble.body,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader,
|
||||||
onTap: onPhotoTap
|
onTap: onPhotoTap
|
||||||
|
|
@ -41,7 +39,6 @@ struct MessageBubbleView: View {
|
||||||
VideoBubbleView(
|
VideoBubbleView(
|
||||||
video: video,
|
video: video,
|
||||||
caption: bubble.body,
|
caption: bubble.body,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader,
|
||||||
onTap: { onVideoTap(video) }
|
onTap: { onVideoTap(video) }
|
||||||
|
|
@ -49,7 +46,6 @@ struct MessageBubbleView: View {
|
||||||
} else if let note = bubble.videoNote {
|
} else if let note = bubble.videoNote {
|
||||||
VideoNoteBubbleView(
|
VideoNoteBubbleView(
|
||||||
note: note,
|
note: note,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader,
|
||||||
onTap: { onVideoNoteTap(note) }
|
onTap: { onVideoNoteTap(note) }
|
||||||
|
|
@ -58,38 +54,31 @@ struct MessageBubbleView: View {
|
||||||
VoiceNoteBubbleView(
|
VoiceNoteBubbleView(
|
||||||
note: voice,
|
note: voice,
|
||||||
caption: bubble.body,
|
caption: bubble.body,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader
|
||||||
sendingState: bubble.sendingState
|
|
||||||
)
|
)
|
||||||
} else if let audio = bubble.audio {
|
} else if let audio = bubble.audio {
|
||||||
AudioBubbleView(
|
AudioBubbleView(
|
||||||
audio: audio,
|
audio: audio,
|
||||||
caption: bubble.body,
|
caption: bubble.body,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader
|
||||||
sendingState: bubble.sendingState
|
|
||||||
)
|
)
|
||||||
} else if let document = bubble.document {
|
} else if let document = bubble.document {
|
||||||
DocumentBubbleView(
|
DocumentBubbleView(
|
||||||
document: document,
|
document: document,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader
|
replyHeader: bubble.replyHeader
|
||||||
)
|
)
|
||||||
} else if let location = bubble.location {
|
} else if let location = bubble.location {
|
||||||
LocationBubbleView(
|
LocationBubbleView(
|
||||||
location: location,
|
location: location,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader
|
replyHeader: bubble.replyHeader
|
||||||
)
|
)
|
||||||
} else if let poll = bubble.poll {
|
} else if let poll = bubble.poll {
|
||||||
PollBubbleView(
|
PollBubbleView(
|
||||||
poll: poll,
|
poll: poll,
|
||||||
time: bubble.time,
|
|
||||||
isOutgoing: bubble.isOutgoing,
|
isOutgoing: bubble.isOutgoing,
|
||||||
replyHeader: bubble.replyHeader,
|
replyHeader: bubble.replyHeader,
|
||||||
onVote: { onPollTap(bubble.messageId, poll) }
|
onVote: { onPollTap(bubble.messageId, poll) }
|
||||||
|
|
@ -101,14 +90,7 @@ struct MessageBubbleView: View {
|
||||||
textBubbleContent
|
textBubbleContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) { statusIndicator }
|
||||||
if bubble.isUnreadOutgoing {
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
.frame(width: 7, height: 7)
|
|
||||||
.offset(x: -11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !bubble.isOutgoing { Spacer(minLength: 16) }
|
if !bubble.isOutgoing { Spacer(minLength: 16) }
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier("bubble.\(bubble.messageId)")
|
.accessibilityIdentifier("bubble.\(bubble.messageId)")
|
||||||
|
|
@ -127,42 +109,42 @@ struct MessageBubbleView: View {
|
||||||
if let header = bubble.replyHeader {
|
if let header = bubble.replyHeader {
|
||||||
ReplyHeaderView(header: header, style: BubbleStyle.resolve(isOutgoing: bubble.isOutgoing))
|
ReplyHeaderView(header: header, style: BubbleStyle.resolve(isOutgoing: bubble.isOutgoing))
|
||||||
}
|
}
|
||||||
HStack(alignment: .lastTextBaseline, spacing: 4) {
|
Text(bubble.body)
|
||||||
Text(bubble.body)
|
.font(.caption)
|
||||||
.font(.caption)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text(bubble.time)
|
|
||||||
.font(.system(size: 8))
|
|
||||||
.foregroundStyle(style.secondary)
|
|
||||||
if bubble.isOutgoing {
|
|
||||||
sendStateGlyph
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 9)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
|
.frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)
|
||||||
.fill(style.fill)
|
.fill(style.fill)
|
||||||
)
|
)
|
||||||
.foregroundStyle(style.content)
|
.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
|
@ViewBuilder
|
||||||
private var sendStateGlyph: some View {
|
private var statusIndicator: some View {
|
||||||
switch bubble.sendingState {
|
switch bubble.outgoingStatus {
|
||||||
case .sent:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
case .unread:
|
||||||
|
Circle()
|
||||||
|
.fill(Color.accentColor)
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
.offset(x: -11)
|
||||||
case .pending:
|
case .pending:
|
||||||
Image(systemName: "clock")
|
Image(systemName: "clock")
|
||||||
.font(.system(size: 8))
|
.font(.system(size: 8))
|
||||||
.foregroundStyle(Color.white.opacity(0.7))
|
.foregroundStyle(Color.white.opacity(0.7))
|
||||||
|
.offset(x: -13)
|
||||||
case .failed:
|
case .failed:
|
||||||
Image(systemName: "exclamationmark.circle.fill")
|
Image(systemName: "exclamationmark.circle.fill")
|
||||||
.font(.system(size: 8))
|
.font(.system(size: 8))
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
|
.offset(x: -13)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +153,6 @@ struct MessageBubbleView: View {
|
||||||
private let previewSampleBubble = MessageBubble(
|
private let previewSampleBubble = MessageBubble(
|
||||||
messageId: 1, isOutgoing: false, senderName: "Alice",
|
messageId: 1, isOutgoing: false, senderName: "Alice",
|
||||||
body: "this is a reply to text",
|
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,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil,
|
||||||
sendingState: .sent,
|
sendingState: .sent,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
|
|
@ -197,7 +178,6 @@ private let previewSampleBubble = MessageBubble(
|
||||||
bubble: MessageBubble(
|
bubble: MessageBubble(
|
||||||
messageId: 2, isOutgoing: true, senderName: nil,
|
messageId: 2, isOutgoing: true, senderName: nil,
|
||||||
body: "ok",
|
body: "ok",
|
||||||
time: "12:35",
|
|
||||||
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil,
|
||||||
sendingState: .sent,
|
sendingState: .sent,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
|
|
@ -219,7 +199,6 @@ private let previewSampleBubble = MessageBubble(
|
||||||
bubble: MessageBubble(
|
bubble: MessageBubble(
|
||||||
messageId: 3, isOutgoing: false, senderName: "Alice",
|
messageId: 3, isOutgoing: false, senderName: "Alice",
|
||||||
body: "colored name above me",
|
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,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil,
|
||||||
sendingState: .sent,
|
sendingState: .sent,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
|
|
@ -236,7 +215,7 @@ private let previewSampleBubble = MessageBubble(
|
||||||
#Preview("Emoji only — incoming 1") {
|
#Preview("Emoji only — incoming 1") {
|
||||||
MessageBubbleView(
|
MessageBubbleView(
|
||||||
bubble: MessageBubble(
|
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,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil,
|
||||||
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil
|
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil
|
||||||
),
|
),
|
||||||
|
|
@ -250,7 +229,6 @@ private let previewSampleBubble = MessageBubble(
|
||||||
bubble: MessageBubble(
|
bubble: MessageBubble(
|
||||||
messageId: 12, isOutgoing: true, senderName: nil,
|
messageId: 12, isOutgoing: true, senderName: nil,
|
||||||
body: "delivered but not yet read",
|
body: "delivered but not yet read",
|
||||||
time: "12:42",
|
|
||||||
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil,
|
||||||
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil,
|
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil,
|
||||||
isUnreadOutgoing: true
|
isUnreadOutgoing: true
|
||||||
|
|
@ -263,7 +241,7 @@ private let previewSampleBubble = MessageBubble(
|
||||||
#Preview("Emoji only — outgoing 3") {
|
#Preview("Emoji only — outgoing 3") {
|
||||||
MessageBubbleView(
|
MessageBubbleView(
|
||||||
bubble: MessageBubble(
|
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,
|
photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil,
|
||||||
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil
|
sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ struct MessageListView: View {
|
||||||
@State private var presentedVideo: VideoVisual?
|
@State private var presentedVideo: VideoVisual?
|
||||||
@State private var presentedVideoNote: VideoNoteVisual?
|
@State private var presentedVideoNote: VideoNoteVisual?
|
||||||
@State private var presentedPoll: PollVoteTarget?
|
@State private var presentedPoll: PollVoteTarget?
|
||||||
@State private var showCompose: Bool = false
|
|
||||||
@State private var showAttachment: Bool = false
|
@State private var showAttachment: Bool = false
|
||||||
@State private var stickerPickerStore: StickerPickerStore?
|
@State private var stickerPickerStore: StickerPickerStore?
|
||||||
// True when the user is parked within slop of the bottom edge. Updated only on
|
// 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)
|
.navigationTitle(row.title)
|
||||||
.accessibilityIdentifier("messageListView")
|
.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
|
.sheet(item: $presentedPhoto) { photo in
|
||||||
PhotoViewerView(photo: photo)
|
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) {
|
.sheet(isPresented: $showAttachment) {
|
||||||
if let store {
|
if let store {
|
||||||
AttachmentSheet(
|
AttachmentSheet(
|
||||||
|
|
@ -243,7 +241,9 @@ struct MessageListView: View {
|
||||||
if row.canSend {
|
if row.canSend {
|
||||||
ReplyBar(
|
ReplyBar(
|
||||||
onAttachTap: { showAttachment = true },
|
onAttachTap: { showAttachment = true },
|
||||||
onTextTap: { showCompose = true }
|
onSend: { snapshot in
|
||||||
|
Task { await store.sendText(snapshot) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.id("composeAnchor")
|
.id("composeAnchor")
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
|
|
@ -261,6 +261,14 @@ struct MessageListView: View {
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(edges: .bottom)
|
.ignoresSafeArea(edges: .bottom)
|
||||||
.defaultScrollAnchor(.bottom)
|
.defaultScrollAnchor(.bottom)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background {
|
||||||
|
Image("ChatBG")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.clipped()
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
.environment(store)
|
.environment(store)
|
||||||
.task {
|
.task {
|
||||||
// Default branch only appears once `loadState == .loaded`
|
// Default branch only appears once `loadState == .loaded`
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ struct MessageBubble: Equatable, Hashable {
|
||||||
let senderName: String?
|
let senderName: String?
|
||||||
/// Body text — for text bubbles, the message body; for photo / video bubbles, the caption (may be "").
|
/// Body text — for text bubbles, the message body; for photo / video bubbles, the caption (may be "").
|
||||||
let body: String
|
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.
|
/// Photo metadata for `messagePhoto` content; nil for non-photo bubbles.
|
||||||
let photo: PhotoVisual?
|
let photo: PhotoVisual?
|
||||||
/// Video metadata for `messageVideo` content; nil for non-video bubbles.
|
/// Video metadata for `messageVideo` content; nil for non-video bubbles.
|
||||||
|
|
@ -57,6 +55,27 @@ struct MessageBubble: Equatable, Hashable {
|
||||||
var senderColorIndex: Int? = nil
|
var senderColorIndex: Int? = nil
|
||||||
/// True for a sent outgoing message the recipient hasn't read yet (`id > lastReadOutboxMessageId`).
|
/// True for a sent outgoing message the recipient hasn't read yet (`id > lastReadOutboxMessageId`).
|
||||||
var isUnreadOutgoing: Bool = false
|
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 {
|
struct ServiceLine: Equatable, Hashable {
|
||||||
|
|
@ -106,12 +125,6 @@ func messageRows(
|
||||||
dayKeyFormatter.timeZone = calendar.timeZone
|
dayKeyFormatter.timeZone = calendar.timeZone
|
||||||
dayKeyFormatter.dateFormat = "yyyy-MM-dd"
|
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 rows: [MessageRow] = []
|
||||||
var lastDayKey: String? = nil
|
var lastDayKey: String? = nil
|
||||||
var dividerPlaced = false
|
var dividerPlaced = false
|
||||||
|
|
@ -148,7 +161,6 @@ func messageRows(
|
||||||
isOutgoing: msg.isOutgoing,
|
isOutgoing: msg.isOutgoing,
|
||||||
senderName: sender?.name,
|
senderName: sender?.name,
|
||||||
body: messageBody(msg.content),
|
body: messageBody(msg.content),
|
||||||
time: timeFormatter.string(from: date),
|
|
||||||
photo: photoVisual(for: msg.content, fileLocals: fileLocals),
|
photo: photoVisual(for: msg.content, fileLocals: fileLocals),
|
||||||
video: videoVisual(for: msg.content, fileLocals: fileLocals),
|
video: videoVisual(for: msg.content, fileLocals: fileLocals),
|
||||||
videoNote: videoNoteVisual(for: msg.content, fileLocals: fileLocals),
|
videoNote: videoNoteVisual(for: msg.content, fileLocals: fileLocals),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import UIKit
|
||||||
|
|
||||||
/// Renders one photo bubble: aspect-fit image (full-resolution when downloaded,
|
/// Renders one photo bubble: aspect-fit image (full-resolution when downloaded,
|
||||||
/// minithumbnail otherwise, gray placeholder if neither), optional caption below the
|
/// minithumbnail otherwise, gray placeholder if neither), optional caption below the
|
||||||
/// image, time stamp footer.
|
/// image.
|
||||||
///
|
///
|
||||||
/// Drives viewport-based download via `.onScrollVisibilityChange` on the store —
|
/// Drives viewport-based download via `.onScrollVisibilityChange` on the store —
|
||||||
/// `.onAppear` would fire for every off-screen bubble too, because the enclosing
|
/// `.onAppear` would fire for every off-screen bubble too, because the enclosing
|
||||||
|
|
@ -13,7 +13,6 @@ import UIKit
|
||||||
struct PhotoBubbleView: View {
|
struct PhotoBubbleView: View {
|
||||||
let photo: PhotoVisual
|
let photo: PhotoVisual
|
||||||
let caption: String
|
let caption: String
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let onTap: (PhotoVisual) -> Void
|
let onTap: (PhotoVisual) -> Void
|
||||||
|
|
@ -45,9 +44,8 @@ struct PhotoBubbleView: View {
|
||||||
private var bareImage: some View {
|
private var bareImage: some View {
|
||||||
imageView
|
imageView
|
||||||
.frame(width: displaySize.width, height: displaySize.height)
|
.frame(width: displaySize.width, height: displaySize.height)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius))
|
||||||
.overlay(alignment: .bottomTrailing) { timePill }
|
.contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius))
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
|
||||||
.onTapGesture { if photo.localPath != nil { onTap(photo) } }
|
.onTapGesture { if photo.localPath != nil { onTap(photo) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,25 +67,10 @@ struct PhotoBubbleView: View {
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
}
|
}
|
||||||
Text(time)
|
|
||||||
.font(.system(size: 8))
|
|
||||||
.foregroundStyle(style.secondary)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.bottom, 4)
|
|
||||||
}
|
}
|
||||||
.frame(width: displaySize.width, alignment: .leading)
|
.frame(width: displaySize.width, alignment: .leading)
|
||||||
.background(style.fill)
|
.background(style.fill)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius))
|
||||||
}
|
|
||||||
|
|
||||||
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
|
@ViewBuilder
|
||||||
|
|
@ -154,7 +137,6 @@ private func photoPreviewStore() -> ChatHistoryStore {
|
||||||
PhotoBubbleView(
|
PhotoBubbleView(
|
||||||
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
||||||
caption: "look at this",
|
caption: "look at this",
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
|
|
@ -172,7 +154,6 @@ private func photoPreviewStore() -> ChatHistoryStore {
|
||||||
PhotoBubbleView(
|
PhotoBubbleView(
|
||||||
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
||||||
caption: "",
|
caption: "",
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
onTap: { _ in }
|
onTap: { _ in }
|
||||||
|
|
@ -185,7 +166,6 @@ private func photoPreviewStore() -> ChatHistoryStore {
|
||||||
PhotoBubbleView(
|
PhotoBubbleView(
|
||||||
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil),
|
||||||
caption: "Benji athlete today!",
|
caption: "Benji athlete today!",
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
onTap: { _ in }
|
onTap: { _ in }
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ import SwiftUI
|
||||||
|
|
||||||
/// Renders one poll / quiz bubble inside the standard gray-incoming / accent-outgoing
|
/// Renders one poll / quiz bubble inside the standard gray-incoming / accent-outgoing
|
||||||
/// chrome. Shows the question, option rows (with result bars + percentages once
|
/// chrome. Shows the question, option rows (with result bars + percentages once
|
||||||
/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count + time
|
/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count footer,
|
||||||
/// footer, and a "Vote" button when the poll is still actionable. Tapping Vote calls
|
/// and a "Vote" button when the poll is still actionable. Tapping Vote calls
|
||||||
/// `onVote`, which opens the dedicated `PollVoteView`.
|
/// `onVote`, which opens the dedicated `PollVoteView`.
|
||||||
struct PollBubbleView: View {
|
struct PollBubbleView: View {
|
||||||
let poll: PollVisual
|
let poll: PollVisual
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let onVote: () -> Void
|
let onVote: () -> Void
|
||||||
|
|
@ -63,18 +62,14 @@ struct PollBubbleView: View {
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.mini)
|
.controlSize(.mini)
|
||||||
}
|
}
|
||||||
HStack(spacing: 4) {
|
Text(voterCountLabel)
|
||||||
Text(voterCountLabel)
|
.font(.system(size: 9))
|
||||||
Spacer(minLength: 0)
|
.foregroundStyle(secondaryColor)
|
||||||
Text(time)
|
|
||||||
}
|
|
||||||
.font(.system(size: 9))
|
|
||||||
.foregroundStyle(secondaryColor)
|
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
.frame(minWidth: BubbleShape.minSize, maxWidth: maxWidth, minHeight: BubbleShape.minSize, alignment: .leading)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)
|
||||||
.fill(style.fill)
|
.fill(style.fill)
|
||||||
)
|
)
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
|
|
@ -147,7 +142,7 @@ private func previewOption(
|
||||||
totalVoterCount: 0, hasVoted: false, explanation: nil,
|
totalVoterCount: 0, hasVoted: false, explanation: nil,
|
||||||
options: [previewOption("Red", position: 0), previewOption("Green", position: 1), previewOption("Blue", position: 2)]
|
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()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,7 +155,7 @@ private func previewOption(
|
||||||
options: [previewOption("Red", position: 0, pct: 75, chosen: true),
|
options: [previewOption("Red", position: 0, pct: 75, chosen: true),
|
||||||
previewOption("Green", position: 1, pct: 25)]
|
previewOption("Green", position: 1, pct: 25)]
|
||||||
),
|
),
|
||||||
time: "12:35", isOutgoing: true, replyHeader: nil, onVote: {}
|
isOutgoing: true, replyHeader: nil, onVote: {}
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +169,7 @@ private func previewOption(
|
||||||
previewOption("Paris", position: 1, pct: 60, correct: true),
|
previewOption("Paris", position: 1, pct: 60, correct: true),
|
||||||
previewOption("Rome", position: 2, pct: 10, correct: false)]
|
previewOption("Rome", position: 2, pct: 10, correct: false)]
|
||||||
),
|
),
|
||||||
time: "12:36", isOutgoing: false, replyHeader: nil, onVote: {}
|
isOutgoing: false, replyHeader: nil, onVote: {}
|
||||||
).bubblePreview()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,7 +181,7 @@ private func previewOption(
|
||||||
totalVoterCount: 12, hasVoted: false, explanation: nil,
|
totalVoterCount: 12, hasVoted: false, explanation: nil,
|
||||||
options: [previewOption("Sat", position: 0, pct: 58), previewOption("Sun", position: 1, pct: 42)]
|
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()
|
).bubblePreview()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct ReplyBar: View {
|
struct ReplyBar: View {
|
||||||
let onAttachTap: () -> Void
|
let onAttachTap: () -> Void
|
||||||
let onTextTap: () -> Void
|
let onSend: (String) -> Void
|
||||||
|
|
||||||
// Circular "+" button and reply-field capsule share a single pill
|
// Circular "+" button and reply-field capsule share a single pill
|
||||||
// diameter so they line up vertically and visually mirror the
|
// diameter so they line up vertically and visually mirror the
|
||||||
|
|
@ -25,17 +25,31 @@ struct ReplyBar: View {
|
||||||
.padding(.leading, 11)
|
.padding(.leading, 11)
|
||||||
.accessibilityIdentifier("replyBarAttach")
|
.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) {
|
HStack(spacing: 0) {
|
||||||
Text("Reply…")
|
Text("Reply…")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.frame(height: Self.pillHeight)
|
.frame(height: Self.pillHeight)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.glassEffect()
|
.glassEffect()
|
||||||
|
} onSubmit: { newText in
|
||||||
|
let snapshot = newText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !snapshot.isEmpty else { return }
|
||||||
|
onSend(snapshot)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
|
|
@ -64,7 +78,7 @@ private struct ReplyBarGeometryPreviewHost: View {
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
|
||||||
ReplyBar(onAttachTap: {}, onTextTap: {})
|
ReplyBar(onAttachTap: {}, onSend: { _ in })
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
.padding(.bottom, 19)
|
.padding(.bottom, 19)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ struct ReviewVoiceBubble: View {
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8).padding(.vertical, 6)
|
.padding(.horizontal, 8).padding(.vertical, 6)
|
||||||
.frame(maxWidth: 200, alignment: .leading)
|
.frame(maxWidth: 200, alignment: .leading)
|
||||||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor))
|
.background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(Color.accentColor))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { controller.toggle(note: note) }
|
.onTapGesture { controller.toggle(note: note) }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import UIKit
|
||||||
|
|
||||||
/// Renders one video bubble: aspect-fit preview thumbnail (downloaded preview, blurred
|
/// Renders one video bubble: aspect-fit preview thumbnail (downloaded preview, blurred
|
||||||
/// minithumbnail, or gray placeholder), centered play overlay, top-trailing duration
|
/// 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
|
/// Drives viewport-based download of the *preview* file id (not the full video) via
|
||||||
/// `.onScrollVisibilityChange` on the store — `.onAppear` would fire for every
|
/// `.onScrollVisibilityChange` on the store — `.onAppear` would fire for every
|
||||||
|
|
@ -15,7 +15,6 @@ import UIKit
|
||||||
struct VideoBubbleView: View {
|
struct VideoBubbleView: View {
|
||||||
let video: VideoVisual
|
let video: VideoVisual
|
||||||
let caption: String
|
let caption: String
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
|
@ -46,8 +45,8 @@ struct VideoBubbleView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var bareVideo: some View {
|
private var bareVideo: some View {
|
||||||
videoContent(clipImage: true, showTimePill: true)
|
videoContent(clipImage: true)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
.contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius))
|
||||||
.onTapGesture { onTap() }
|
.onTapGesture { onTap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ struct VideoBubbleView: View {
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
}
|
}
|
||||||
videoContent(clipImage: false, showTimePill: false)
|
videoContent(clipImage: false)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { onTap() }
|
.onTapGesture { onTap() }
|
||||||
if !caption.isEmpty {
|
if !caption.isEmpty {
|
||||||
|
|
@ -68,22 +67,17 @@ struct VideoBubbleView: View {
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
}
|
}
|
||||||
Text(time)
|
|
||||||
.font(.system(size: 8))
|
|
||||||
.foregroundStyle(style.secondary)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.bottom, 4)
|
|
||||||
}
|
}
|
||||||
.frame(width: displaySize.width, alignment: .leading)
|
.frame(width: displaySize.width, alignment: .leading)
|
||||||
.background(style.fill)
|
.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 {
|
ZStack {
|
||||||
imageView
|
imageView
|
||||||
.frame(width: displaySize.width, height: displaySize.height)
|
.frame(width: displaySize.width, height: displaySize.height)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: clipImage ? 12 : 0))
|
.clipShape(RoundedRectangle(cornerRadius: clipImage ? BubbleShape.cornerRadius : 0))
|
||||||
.overlay(alignment: .topTrailing) {
|
.overlay(alignment: .topTrailing) {
|
||||||
Text(formatDuration(video.duration))
|
Text(formatDuration(video.duration))
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
|
|
@ -93,23 +87,10 @@ struct VideoBubbleView: View {
|
||||||
.background(Capsule().fill(.black.opacity(0.5)))
|
.background(Capsule().fill(.black.opacity(0.5)))
|
||||||
.padding(4)
|
.padding(4)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomTrailing) {
|
|
||||||
if showTimePill { timePill }
|
|
||||||
}
|
|
||||||
playOverlay
|
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
|
@ViewBuilder
|
||||||
private var imageView: some View {
|
private var imageView: some View {
|
||||||
if let path = video.preview.previewLocalPath, let img = UIImage(contentsOfFile: path) {
|
if let path = video.preview.previewLocalPath, let img = UIImage(contentsOfFile: path) {
|
||||||
|
|
@ -186,7 +167,6 @@ private func videoPreviewStore() -> ChatHistoryStore {
|
||||||
videoLocalPath: nil
|
videoLocalPath: nil
|
||||||
),
|
),
|
||||||
caption: "check this clip",
|
caption: "check this clip",
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
|
|
@ -209,7 +189,6 @@ private func videoPreviewStore() -> ChatHistoryStore {
|
||||||
videoLocalPath: nil
|
videoLocalPath: nil
|
||||||
),
|
),
|
||||||
caption: "",
|
caption: "",
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
onTap: {}
|
onTap: {}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import UIKit
|
||||||
/// Renders one round-video-note bubble: a chrome-less 150 pt circle with the
|
/// Renders one round-video-note bubble: a chrome-less 150 pt circle with the
|
||||||
/// preview image (downloaded thumb → blurred minithumbnail → gray placeholder)
|
/// preview image (downloaded thumb → blurred minithumbnail → gray placeholder)
|
||||||
/// behind a centered play overlay and a bottom-inside duration pill. Optional
|
/// behind a centered play overlay and a bottom-inside duration pill. Optional
|
||||||
/// reply mini-card (`StickerBubbleView` precedent) sits above. Timestamp footer
|
/// reply mini-card (`StickerBubbleView` precedent) sits above.
|
||||||
/// renders below as plain text on the system background.
|
|
||||||
///
|
///
|
||||||
/// Drives viewport-based download of the thumbnail file id (not the playable
|
/// Drives viewport-based download of the thumbnail file id (not the playable
|
||||||
/// video file) via `.onScrollVisibilityChange`. The playable file is downloaded
|
/// 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.
|
/// Tap is unconditional — the viewer handles the not-yet-downloaded state.
|
||||||
struct VideoNoteBubbleView: View {
|
struct VideoNoteBubbleView: View {
|
||||||
let note: VideoNoteVisual
|
let note: VideoNoteVisual
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
|
@ -61,10 +59,6 @@ struct VideoNoteBubbleView: View {
|
||||||
store.cancelFileDownload(fileId: id)
|
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,
|
thumbFileId: nil, minithumbnail: nil,
|
||||||
thumbLocalPath: nil, videoLocalPath: nil
|
thumbLocalPath: nil, videoLocalPath: nil
|
||||||
),
|
),
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
onTap: {}
|
onTap: {}
|
||||||
|
|
@ -142,7 +135,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore {
|
||||||
thumbFileId: nil, minithumbnail: nil,
|
thumbFileId: nil, minithumbnail: nil,
|
||||||
thumbLocalPath: nil, videoLocalPath: nil
|
thumbLocalPath: nil, videoLocalPath: nil
|
||||||
),
|
),
|
||||||
time: "12:36",
|
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
replyHeader: nil,
|
replyHeader: nil,
|
||||||
onTap: {}
|
onTap: {}
|
||||||
|
|
@ -158,7 +150,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore {
|
||||||
thumbFileId: nil, minithumbnail: nil,
|
thumbFileId: nil, minithumbnail: nil,
|
||||||
thumbLocalPath: nil, videoLocalPath: nil
|
thumbLocalPath: nil, videoLocalPath: nil
|
||||||
),
|
),
|
||||||
time: "12:34",
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import SwiftUI
|
||||||
/// outgoing) containing a play/pause glyph + 32-bar waveform + duration
|
/// outgoing) containing a play/pause glyph + 32-bar waveform + duration
|
||||||
/// label. Optional reply header sits inside the chrome above the row.
|
/// label. Optional reply header sits inside the chrome above the row.
|
||||||
/// Caption (when non-empty) renders below the waveform inside the chrome.
|
/// Caption (when non-empty) renders below the waveform inside the chrome.
|
||||||
/// Time footer renders below the chrome.
|
|
||||||
///
|
///
|
||||||
/// Drives priority-1 viewport download via `.onScrollVisibilityChange`.
|
/// Drives priority-1 viewport download via `.onScrollVisibilityChange`.
|
||||||
/// Tap goes through `ChatHistoryStore.togglePlayback(_:)`; the store kicks
|
/// Tap goes through `ChatHistoryStore.togglePlayback(_:)`; the store kicks
|
||||||
|
|
@ -13,10 +12,8 @@ import SwiftUI
|
||||||
struct VoiceNoteBubbleView: View {
|
struct VoiceNoteBubbleView: View {
|
||||||
let note: VoiceNoteVisual
|
let note: VoiceNoteVisual
|
||||||
let caption: String
|
let caption: String
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
let sendingState: SendingState
|
|
||||||
|
|
||||||
@Environment(ChatHistoryStore.self) private var store
|
@Environment(ChatHistoryStore.self) private var store
|
||||||
@Environment(\.bubbleMetrics) private var metrics
|
@Environment(\.bubbleMetrics) private var metrics
|
||||||
|
|
@ -58,13 +55,12 @@ struct VoiceNoteBubbleView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
timeRow
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading)
|
.frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)
|
||||||
.fill(style.fill)
|
.fill(style.fill)
|
||||||
)
|
)
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
|
|
@ -113,28 +109,6 @@ struct VoiceNoteBubbleView: View {
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.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
|
#if DEBUG
|
||||||
|
|
@ -178,8 +152,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual {
|
||||||
#Preview("Voice — incoming, idle") {
|
#Preview("Voice — incoming, idle") {
|
||||||
VoiceNoteBubbleView(
|
VoiceNoteBubbleView(
|
||||||
note: previewVoice(), caption: "",
|
note: previewVoice(), caption: "",
|
||||||
time: "12:34", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -188,8 +162,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual {
|
||||||
#Preview("Voice — outgoing, idle") {
|
#Preview("Voice — outgoing, idle") {
|
||||||
VoiceNoteBubbleView(
|
VoiceNoteBubbleView(
|
||||||
note: previewVoice(id: 2), caption: "",
|
note: previewVoice(id: 2), caption: "",
|
||||||
time: "12:35", isOutgoing: true,
|
isOutgoing: true,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -198,13 +172,12 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual {
|
||||||
#Preview("Voice — incoming with reply") {
|
#Preview("Voice — incoming with reply") {
|
||||||
VoiceNoteBubbleView(
|
VoiceNoteBubbleView(
|
||||||
note: previewVoice(id: 3), caption: "",
|
note: previewVoice(id: 3), caption: "",
|
||||||
time: "12:36", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: ReplyHeader(
|
replyHeader: ReplyHeader(
|
||||||
senderName: "Bob",
|
senderName: "Bob",
|
||||||
snippet: "anchor message earlier in the chat",
|
snippet: "anchor message earlier in the chat",
|
||||||
minithumbnail: nil, isOutgoing: false
|
minithumbnail: nil, isOutgoing: false
|
||||||
),
|
)
|
||||||
sendingState: .sent
|
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -214,8 +187,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual {
|
||||||
VoiceNoteBubbleView(
|
VoiceNoteBubbleView(
|
||||||
note: previewVoice(id: 4),
|
note: previewVoice(id: 4),
|
||||||
caption: "Listen to this part carefully",
|
caption: "Listen to this part carefully",
|
||||||
time: "12:37", isOutgoing: false,
|
isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -227,8 +200,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual {
|
||||||
voiceFileId: 5, duration: 5, mimeType: "audio/ogg",
|
voiceFileId: 5, duration: 5, mimeType: "audio/ogg",
|
||||||
waveform: Data(), caption: "", localPath: nil
|
waveform: Data(), caption: "", localPath: nil
|
||||||
),
|
),
|
||||||
caption: "", time: "12:38", isOutgoing: false,
|
caption: "", isOutgoing: false,
|
||||||
replyHeader: nil, sendingState: .sent
|
replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ import SwiftUI
|
||||||
import TDLibKit
|
import TDLibKit
|
||||||
|
|
||||||
/// One sticker bubble. No rounded-rect chat-bubble chrome — sticker renders directly
|
/// 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
|
/// against the chat background, with sender name above (incoming groups only).
|
||||||
/// stamp below. Drives the same visibility-based download flow as PhotoBubbleView.
|
/// Drives the same visibility-based download flow as PhotoBubbleView.
|
||||||
struct StickerBubbleView: View {
|
struct StickerBubbleView: View {
|
||||||
let sticker: StickerVisual
|
let sticker: StickerVisual
|
||||||
let senderName: String?
|
let senderName: String?
|
||||||
var senderColorIndex: Int? = nil
|
var senderColorIndex: Int? = nil
|
||||||
let time: String
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let replyHeader: ReplyHeader?
|
let replyHeader: ReplyHeader?
|
||||||
|
|
||||||
|
|
@ -47,10 +46,6 @@ struct StickerBubbleView: View {
|
||||||
.onScrollVisibilityChange(threshold: 0.01) { visible in
|
.onScrollVisibilityChange(threshold: 0.01) { visible in
|
||||||
handleVisibility(visible)
|
handleVisibility(visible)
|
||||||
}
|
}
|
||||||
Text(time)
|
|
||||||
.font(.system(size: 8))
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,8 +71,9 @@ struct StickerBubbleView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
|
.frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)
|
||||||
.fill(style.fill)
|
.fill(style.fill)
|
||||||
)
|
)
|
||||||
.foregroundStyle(style.content)
|
.foregroundStyle(style.content)
|
||||||
|
|
@ -182,7 +178,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("WEBP — downloaded") {
|
#Preview("WEBP — downloaded") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -191,7 +187,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("WEBP — downloading, thumbnail available") {
|
#Preview("WEBP — downloading, thumbnail available") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: false, withThumb: true),
|
sticker: previewSticker(format: .webp, withFile: false, withThumb: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -200,7 +196,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("WEBP — downloading, no thumbnail") {
|
#Preview("WEBP — downloading, no thumbnail") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: false),
|
sticker: previewSticker(format: .webp, withFile: false),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -209,7 +205,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("TGS — downloaded") {
|
#Preview("TGS — downloaded") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .tgs, withFile: true),
|
sticker: previewSticker(format: .tgs, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -218,7 +214,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("TGS — downloading, thumbnail available") {
|
#Preview("TGS — downloading, thumbnail available") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .tgs, withFile: false, withThumb: true),
|
sticker: previewSticker(format: .tgs, withFile: false, withThumb: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -227,7 +223,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("WEBM — unsupported fallback") {
|
#Preview("WEBM — unsupported fallback") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .unsupported, withFile: false),
|
sticker: previewSticker(format: .unsupported, withFile: false),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: nil, isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -236,7 +232,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("Group sticker — sender name above") {
|
#Preview("Group sticker — sender name above") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: "Alice", time: "12:34", isOutgoing: false, replyHeader: nil
|
senderName: "Alice", isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -246,7 +242,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: "Alice", senderColorIndex: paletteIndex(for: 200),
|
senderName: "Alice", senderColorIndex: paletteIndex(for: 200),
|
||||||
time: "12:34", isOutgoing: false, replyHeader: nil
|
isOutgoing: false, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -255,7 +251,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb
|
||||||
#Preview("Outgoing sticker") {
|
#Preview("Outgoing sticker") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: true, replyHeader: nil
|
senderName: nil, isOutgoing: true, replyHeader: nil
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
.environment(previewStore())
|
.environment(previewStore())
|
||||||
|
|
@ -275,7 +271,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa
|
||||||
#Preview("Sticker WEBP — with reply card above") {
|
#Preview("Sticker WEBP — with reply card above") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false,
|
senderName: nil, isOutgoing: false,
|
||||||
replyHeader: previewReplyHeader(toText: "anchor message")
|
replyHeader: previewReplyHeader(toText: "anchor message")
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
|
|
@ -285,7 +281,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa
|
||||||
#Preview("Sticker TGS — with reply card above") {
|
#Preview("Sticker TGS — with reply card above") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .tgs, withFile: true),
|
sticker: previewSticker(format: .tgs, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: false,
|
senderName: nil, isOutgoing: false,
|
||||||
replyHeader: previewReplyHeader(toText: "look at this clip")
|
replyHeader: previewReplyHeader(toText: "look at this clip")
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
|
|
@ -295,7 +291,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa
|
||||||
#Preview("Sticker outgoing — with reply card above") {
|
#Preview("Sticker outgoing — with reply card above") {
|
||||||
StickerBubbleView(
|
StickerBubbleView(
|
||||||
sticker: previewSticker(format: .webp, withFile: true),
|
sticker: previewSticker(format: .webp, withFile: true),
|
||||||
senderName: nil, time: "12:34", isOutgoing: true,
|
senderName: nil, isOutgoing: true,
|
||||||
replyHeader: previewReplyHeader(toText: "thanks!", isOutgoing: true)
|
replyHeader: previewReplyHeader(toText: "thanks!", isOutgoing: true)
|
||||||
)
|
)
|
||||||
.bubblePreview()
|
.bubblePreview()
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@
|
||||||
A0899809BBC92D1563AC4160 /* ReplyBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7584BF25A84BAD24AB992A79 /* ReplyBar.swift */; };
|
A0899809BBC92D1563AC4160 /* ReplyBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7584BF25A84BAD24AB992A79 /* ReplyBar.swift */; };
|
||||||
A121DA7622C910B267D69B6E /* PhotoBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F7533C5A9B419B29727C86 /* PhotoBubbleView.swift */; };
|
A121DA7622C910B267D69B6E /* PhotoBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F7533C5A9B419B29727C86 /* PhotoBubbleView.swift */; };
|
||||||
A6179A26CB7FB298567682E6 /* DocumentVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2515F0EDA29D5270610A69 /* DocumentVisual.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 */; };
|
A7967495A54936FE19218DC4 /* ServiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */; };
|
||||||
A8EF6F72C78B619893C05AFE /* ChatPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448938C1F69F0D9DE679238 /* ChatPreview.swift */; };
|
A8EF6F72C78B619893C05AFE /* ChatPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448938C1F69F0D9DE679238 /* ChatPreview.swift */; };
|
||||||
AA3F067A7BC4CDA579CFFC39 /* PollVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4D691106D7EA2FEE35F37F /* PollVisual.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 */; };
|
C62226963E54795B2808A52C /* MapSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFF41E60C5477C20897CD00 /* MapSnapshotRenderer.swift */; };
|
||||||
C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */; };
|
C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */; };
|
||||||
C94BD040D985FC0090CDBF84 /* StickerPickerLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC230FDDDFAA063019744A0E /* StickerPickerLoader.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 */; };
|
D1082945B5785B08F820FB78 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93535C2021AB2E7B17F593AA /* LoadingView.swift */; };
|
||||||
D1E96E63A3AE93646AD3F532 /* AudioBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C3B71804A398A0100375E86 /* AudioBubbleView.swift */; };
|
D1E96E63A3AE93646AD3F532 /* AudioBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C3B71804A398A0100375E86 /* AudioBubbleView.swift */; };
|
||||||
D5515C5A9286E0372DBFB7F9 /* QRCodeGenerator in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A04697FEA6C4614973F19 /* QRCodeGenerator */; };
|
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 = "<group>"; };
|
4DB3C8238D41374CBAC66303 /* EmojiText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiText.swift; sourceTree = "<group>"; };
|
||||||
4E049ECC956A583A92C5C7CB /* SecretsValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsValidator.swift; sourceTree = "<group>"; };
|
4E049ECC956A583A92C5C7CB /* SecretsValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsValidator.swift; sourceTree = "<group>"; };
|
||||||
53B9831274FA56126E3F5A42 /* LocationSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSendView.swift; sourceTree = "<group>"; };
|
53B9831274FA56126E3F5A42 /* LocationSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSendView.swift; sourceTree = "<group>"; };
|
||||||
54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSheet.swift; sourceTree = "<group>"; };
|
|
||||||
596AF8BF88C38D2018397A33 /* LocationBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationBubbleView.swift; sourceTree = "<group>"; };
|
596AF8BF88C38D2018397A33 /* LocationBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationBubbleView.swift; sourceTree = "<group>"; };
|
||||||
5D3A8FBA5EBDBFDE5AA2FFEC /* QrLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrLoginView.swift; sourceTree = "<group>"; };
|
5D3A8FBA5EBDBFDE5AA2FFEC /* QrLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrLoginView.swift; sourceTree = "<group>"; };
|
||||||
5EA750417DF3AD62C7B788F6 /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = "<group>"; };
|
5EA750417DF3AD62C7B788F6 /* MessageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -208,6 +207,7 @@
|
||||||
D4AF5A180BB3A37832AC5349 /* VideoNoteBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteBubbleView.swift; sourceTree = "<group>"; };
|
D4AF5A180BB3A37832AC5349 /* VideoNoteBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteBubbleView.swift; sourceTree = "<group>"; };
|
||||||
D50662DC1777F2BEE53A0129 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
D50662DC1777F2BEE53A0129 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
D9CD96277A18D97BD335BBC0 /* StickerSetDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSetDetailView.swift; sourceTree = "<group>"; };
|
D9CD96277A18D97BD335BBC0 /* StickerSetDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSetDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleShape.swift; sourceTree = "<group>"; };
|
||||||
DDCE599C8D50EE8832B4A797 /* ReplyFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyFormatters.swift; sourceTree = "<group>"; };
|
DDCE599C8D50EE8832B4A797 /* ReplyFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyFormatters.swift; sourceTree = "<group>"; };
|
||||||
DF0F4F15CB43635717B31B92 /* PollVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteView.swift; sourceTree = "<group>"; };
|
DF0F4F15CB43635717B31B92 /* PollVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteView.swift; sourceTree = "<group>"; };
|
||||||
DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceMessageView.swift; sourceTree = "<group>"; };
|
DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceMessageView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -337,10 +337,10 @@
|
||||||
CBAFC996C8EF743C7C598389 /* AudioVisual.swift */,
|
CBAFC996C8EF743C7C598389 /* AudioVisual.swift */,
|
||||||
0F9127473C7DAD18736234B3 /* AVRecordingBackend.swift */,
|
0F9127473C7DAD18736234B3 /* AVRecordingBackend.swift */,
|
||||||
674916153E803CA73D43BE6C /* BubbleMetrics.swift */,
|
674916153E803CA73D43BE6C /* BubbleMetrics.swift */,
|
||||||
|
DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */,
|
||||||
3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */,
|
3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */,
|
||||||
C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */,
|
C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */,
|
||||||
182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */,
|
182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */,
|
||||||
54149DBDF74BC221A94FB1AD /* ComposeSheet.swift */,
|
|
||||||
E855E4C62401AD1CF41D1835 /* DaySeparatorView.swift */,
|
E855E4C62401AD1CF41D1835 /* DaySeparatorView.swift */,
|
||||||
BDBE08BBB88597B10CDF0142 /* DocumentBubbleView.swift */,
|
BDBE08BBB88597B10CDF0142 /* DocumentBubbleView.swift */,
|
||||||
FF2515F0EDA29D5270610A69 /* DocumentVisual.swift */,
|
FF2515F0EDA29D5270610A69 /* DocumentVisual.swift */,
|
||||||
|
|
@ -523,6 +523,7 @@
|
||||||
5E9A6B94F1B917CB849B3EBC /* AvatarView.swift in Sources */,
|
5E9A6B94F1B917CB849B3EBC /* AvatarView.swift in Sources */,
|
||||||
03074928DC54019BE6106ECD /* AvatarVisual.swift in Sources */,
|
03074928DC54019BE6106ECD /* AvatarVisual.swift in Sources */,
|
||||||
4236CA9B5AA90ACEB17814DC /* BubbleMetrics.swift in Sources */,
|
4236CA9B5AA90ACEB17814DC /* BubbleMetrics.swift in Sources */,
|
||||||
|
CBA3FEDEEA413559F700B6E3 /* BubbleShape.swift in Sources */,
|
||||||
868426A2768FE7349CD4AAAD /* BubbleStyle.swift in Sources */,
|
868426A2768FE7349CD4AAAD /* BubbleStyle.swift in Sources */,
|
||||||
4505C5628AF9258938D623D7 /* CachedChat.swift in Sources */,
|
4505C5628AF9258938D623D7 /* CachedChat.swift in Sources */,
|
||||||
C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */,
|
C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */,
|
||||||
|
|
@ -536,7 +537,6 @@
|
||||||
E48523AFA2E23409CC0C2C2C /* ChatRow.swift in Sources */,
|
E48523AFA2E23409CC0C2C2C /* ChatRow.swift in Sources */,
|
||||||
7BF1C89F67290EBAF50900F8 /* ChatRowView.swift in Sources */,
|
7BF1C89F67290EBAF50900F8 /* ChatRowView.swift in Sources */,
|
||||||
04CEF3F9C267D8A7CEC5EC3C /* ClosedView.swift in Sources */,
|
04CEF3F9C267D8A7CEC5EC3C /* ClosedView.swift in Sources */,
|
||||||
A73233E40B531EA2D4A81B78 /* ComposeSheet.swift in Sources */,
|
|
||||||
EF499261BE49A6B1C7B3B1F4 /* ContentView.swift in Sources */,
|
EF499261BE49A6B1C7B3B1F4 /* ContentView.swift in Sources */,
|
||||||
FD5515DB22C571F4D3E5F5FA /* DaySeparatorView.swift in Sources */,
|
FD5515DB22C571F4D3E5F5FA /* DaySeparatorView.swift in Sources */,
|
||||||
F5596A4EF4D2055B10B4A183 /* DocumentBubbleView.swift in Sources */,
|
F5596A4EF4D2055B10B4A183 /* DocumentBubbleView.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,17 @@ it through the providers that `ios_application(watch_application = ...)` consume
|
||||||
1. PrebuiltWatchosCompile (prebuilt_watchos_compile.sh) — runs `xcodebuild` against
|
1. PrebuiltWatchosCompile (prebuilt_watchos_compile.sh) — runs `xcodebuild` against
|
||||||
the exported tgwatch source tree with PLACEHOLDER version/api values, emitting an
|
the exported tgwatch source tree with PLACEHOLDER version/api values, emitting an
|
||||||
unsigned .app archive. Depends only on the source snapshot.
|
unsigned .app archive. Depends only on the source snapshot.
|
||||||
2. PrebuiltWatchosPatchSign (prebuilt_watchos_patch.sh) — rewrites the four per-build
|
2. PrebuiltWatchosPatchSign (prebuilt_watchos_patch.sh) — rewrites the six per-build
|
||||||
Info.plist keys (version, build number, api id/hash) on the compiled app, then
|
Info.plist keys (version, build number, api id/hash, watch CFBundleIdentifier,
|
||||||
optionally codesigns it.
|
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
|
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
|
version/build number/api/identity/bundle-id change — those values never reach the
|
||||||
binary, only the Info.plist.
|
compiled binary, only the Info.plist.
|
||||||
|
|
||||||
The providers exposed:
|
The providers exposed:
|
||||||
|
|
||||||
|
|
@ -44,6 +48,19 @@ def _apple_prebuilt_watchos_application_impl(ctx):
|
||||||
api_hash = ctx.var.get("watchApiHash", "placeholder")
|
api_hash = ctx.var.get("watchApiHash", "placeholder")
|
||||||
identity = ctx.var.get("watchSigningIdentity", "")
|
identity = ctx.var.get("watchSigningIdentity", "")
|
||||||
|
|
||||||
|
# The watch bundle id is `<host>.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
|
||||||
|
# `<host>.`, 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
|
# 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
|
# --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
|
# 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).
|
# separate output (resources.bzl bundle_verification crashes on a None infoplist).
|
||||||
infoplist = ctx.actions.declare_file(ctx.label.name + "_Info.plist")
|
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-sandbox": "1",
|
||||||
"no-remote": "1",
|
"no-remote": "1",
|
||||||
"local": "1",
|
"local": "1",
|
||||||
|
|
@ -84,7 +123,7 @@ def _apple_prebuilt_watchos_application_impl(ctx):
|
||||||
outputs = [compiled_archive],
|
outputs = [compiled_archive],
|
||||||
mnemonic = "PrebuiltWatchosCompile",
|
mnemonic = "PrebuiltWatchosCompile",
|
||||||
progress_message = "Compiling watch app via xcodebuild",
|
progress_message = "Compiling watch app via xcodebuild",
|
||||||
execution_requirements = exec_requirements,
|
execution_requirements = compile_exec_requirements,
|
||||||
use_default_shell_env = True,
|
use_default_shell_env = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -104,12 +143,14 @@ def _apple_prebuilt_watchos_application_impl(ctx):
|
||||||
infoplist.path,
|
infoplist.path,
|
||||||
ctx.file.versions_json.path,
|
ctx.file.versions_json.path,
|
||||||
build_number,
|
build_number,
|
||||||
|
host_bundle_id,
|
||||||
|
watch_bundle_id,
|
||||||
],
|
],
|
||||||
inputs = [ctx.file._patch_worker, compiled_archive, ctx.file.versions_json],
|
inputs = [ctx.file._patch_worker, compiled_archive, ctx.file.versions_json],
|
||||||
outputs = [archive, infoplist],
|
outputs = [archive, infoplist],
|
||||||
mnemonic = "PrebuiltWatchosPatchSign",
|
mnemonic = "PrebuiltWatchosPatchSign",
|
||||||
progress_message = "Patching%s watch app Info.plist" % (" + signing" if profile else ""),
|
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,
|
use_default_shell_env = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,49 @@
|
||||||
# Patch + sign worker for the apple_prebuilt_watchos_application Bazel rule (action 2 of 2).
|
# 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
|
# Takes the unsigned, placeholder-version watch .app archive produced by
|
||||||
# prebuilt_watchos_compile.sh, rewrites the four per-build Info.plist values
|
# prebuilt_watchos_compile.sh, rewrites the six per-build Info.plist values
|
||||||
# (CFBundleShortVersionString, CFBundleVersion, TG_API_ID, TG_API_HASH) — none of
|
# (CFBundleShortVersionString, CFBundleVersion, TG_API_ID, TG_API_HASH,
|
||||||
# which affect the compiled binary — then — if a provisioning profile is supplied —
|
# CFBundleIdentifier, WKCompanionAppBundleIdentifier) — none of which affect the
|
||||||
# codesigns the app and its nested frameworks with the watchkitapp provisioning
|
# compiled binary — then — if a provisioning profile is supplied — codesigns the
|
||||||
# profile and a matching identity, and finally zips the .app into the rule's output
|
# app and its nested frameworks with the watchkitapp provisioning profile and a
|
||||||
# archive.
|
# 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
|
# 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;
|
# 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.
|
# it does NOT re-sign the watch app, so the watch signing must happen here.
|
||||||
#
|
#
|
||||||
# Args:
|
# Args:
|
||||||
# $1 input_zip Compiled (unsigned, placeholder-version) .app archive from action 1
|
# $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
|
# $2 output_zip Path (declared by Bazel) to write the final .app archive to
|
||||||
# $3 api_id TG_API_ID Info.plist value
|
# $3 api_id TG_API_ID Info.plist value
|
||||||
# $4 api_hash TG_API_HASH Info.plist value
|
# $4 api_hash TG_API_HASH Info.plist value
|
||||||
# $5 identity Codesigning identity (SHA1 hash); empty => derived from $6's cert
|
# $5 identity Codesigning identity (SHA1 hash); empty => derived from $6's cert
|
||||||
# $6 profile Path to the watchkitapp .mobileprovision; empty => unsigned build
|
# $6 profile Path to the watchkitapp .mobileprovision; empty => unsigned build
|
||||||
# $7 infoplist_out Path (declared by Bazel) to copy the patched Info.plist to
|
# $7 infoplist_out Path (declared by Bazel) to copy the patched Info.plist to
|
||||||
# $8 versions_json versions.json (key 'app' => CFBundleShortVersionString)
|
# $8 versions_json versions.json (key 'app' => CFBundleShortVersionString)
|
||||||
# $9 build_number CFBundleVersion
|
# $9 build_number CFBundleVersion
|
||||||
|
# $10 host_bundle_id WKCompanionAppBundleIdentifier value (host app bundle id)
|
||||||
|
# $11 watch_bundle_id CFBundleIdentifier value (must be "<host_bundle_id>.watchkitapp")
|
||||||
set -euo pipefail
|
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
|
# Match the host app's version (rules_apple requires the embedded watch app's
|
||||||
# CFBundleShortVersionString/CFBundleVersion to equal the parent's).
|
# CFBundleShortVersionString/CFBundleVersion to equal the parent's).
|
||||||
|
|
@ -46,15 +63,20 @@ if [ -z "$APP" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# exist in the compiled (binary-format) Info.plist, so PlistBuddy Set preserves their
|
||||||
# (string) type — matching what $(...) substitution produced and what Secrets.swift
|
# (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"
|
PLIST="$APP/Info.plist"
|
||||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $MARKETING_VERSION" "$PLIST"
|
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $MARKETING_VERSION" "$PLIST"
|
||||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$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_ID $API_ID" "$PLIST"
|
||||||
/usr/libexec/PlistBuddy -c "Set :TG_API_HASH $API_HASH" "$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
|
# 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
|
# bundle-id linkage and the child version). Codesigning does not alter Info.plist
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -1116,8 +1116,14 @@ private func blockIsEntityExpressible(_ block: InstantPageBlock) -> Bool {
|
||||||
return richTextIsEntityExpressible(text)
|
return richTextIsEntityExpressible(text)
|
||||||
case .preformatted(let text, _):
|
case .preformatted(let text, _):
|
||||||
return richTextIsEntityExpressible(text)
|
return richTextIsEntityExpressible(text)
|
||||||
case .blockQuote(let text, let caption):
|
case .blockQuote(let blocks, let caption):
|
||||||
return isEmptyRichText(caption) && richTextIsEntityExpressible(text)
|
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:
|
case .anchor, .unsupported:
|
||||||
return true
|
return true
|
||||||
case .divider:
|
case .divider:
|
||||||
|
|
@ -1392,24 +1398,17 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
|
||||||
case .thematicBreak:
|
case .thematicBreak:
|
||||||
return [.divider]
|
return [.divider]
|
||||||
case .blockQuote:
|
case .blockQuote:
|
||||||
var result: [InstantPageBlock] = []
|
var childBlocks: [InstantPageBlock] = []
|
||||||
for child in node.children {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
for childBlock in childBlocks {
|
childBlocks.append(contentsOf: parsed)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
guard !childBlocks.isEmpty else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [.blockQuote(blocks: childBlocks, caption: .empty)]
|
||||||
case .orderedList:
|
case .orderedList:
|
||||||
guard let items = markdownListItems(from: node.children, ordered: true, context: context, depth: depth + 1) else {
|
guard let items = markdownListItems(from: node.children, ordered: true, context: context, depth: depth + 1) else {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -2213,8 +2212,9 @@ private func markdownPlainText(from block: InstantPageBlock, depth: Int = 0) ->
|
||||||
return text.plainText
|
return text.plainText
|
||||||
case let .footer(text):
|
case let .footer(text):
|
||||||
return text.plainText
|
return text.plainText
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
return text.plainText.isEmpty ? caption.plainText : text.plainText
|
let blocksText = blocks.map { markdownPlainText(from: $0, depth: depth + 1) }.joined(separator: "\n")
|
||||||
|
return blocksText.isEmpty ? caption.plainText : blocksText
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
return text.plainText.isEmpty ? caption.plainText : text.plainText
|
return text.plainText.isEmpty ? caption.plainText : text.plainText
|
||||||
case let .kicker(text):
|
case let .kicker(text):
|
||||||
|
|
|
||||||
|
|
@ -849,7 +849,7 @@ private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [Engi
|
||||||
case "pre":
|
case "pre":
|
||||||
result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil))
|
result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil))
|
||||||
case "blockquote":
|
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":
|
case "img":
|
||||||
if let image = parseImage(item, &media) {
|
if let image = parseImage(item, &media) {
|
||||||
result.append(image)
|
result.append(image)
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ private func markdownString(from block: InstantPageBlock) -> String? {
|
||||||
case let .preformatted(text, language):
|
case let .preformatted(text, language):
|
||||||
let language = language ?? ""
|
let language = language ?? ""
|
||||||
return "```\(language)\n\(markdownInline(from: text))\n```"
|
return "```\(language)\n\(markdownInline(from: text))\n```"
|
||||||
case let .blockQuote(text, _):
|
case let .blockQuote(blocks, _):
|
||||||
return markdownBlockQuote(text)
|
return markdownBlockQuoteBlocks(blocks)
|
||||||
case let .pullQuote(text, _):
|
case let .pullQuote(text, _):
|
||||||
return markdownBlockQuote(text)
|
return markdownBlockQuote(text)
|
||||||
case let .list(items, ordered):
|
case let .list(items, ordered):
|
||||||
|
|
@ -69,6 +69,19 @@ private func markdownBlockQuote(_ text: RichText) -> String {
|
||||||
return lines.map { "> \($0)" }.joined(separator: "\n")
|
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 {
|
private func markdownInline(from richText: RichText) -> String {
|
||||||
switch richText {
|
switch richText {
|
||||||
case .empty:
|
case .empty:
|
||||||
|
|
|
||||||
|
|
@ -501,40 +501,65 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems)
|
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems)
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
let lineInset: CGFloat = 20.0
|
let lineInset: CGFloat = 20.0
|
||||||
let verticalInset: CGFloat = 4.0
|
let verticalInset: CGFloat = 4.0
|
||||||
var contentSize = CGSize(width: boundingWidth, height: verticalInset)
|
var contentSize = CGSize(width: boundingWidth, height: verticalInset)
|
||||||
|
|
||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
|
|
||||||
let styleStack = InstantPageTextStyleStack()
|
let innerBoundingWidth = boundingWidth - horizontalInset * 2.0 - lineInset
|
||||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
let innerHorizontalInset = horizontalInset + lineInset
|
||||||
styleStack.push(.italic)
|
|
||||||
|
if blocks.count == 1, case let .paragraph(text) = blocks[0] {
|
||||||
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)
|
// Legacy single-paragraph fast path: italicized body (unchanged from prior behavior).
|
||||||
|
let styleStack = InstantPageTextStyleStack()
|
||||||
contentSize.height += textContentSize.height
|
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||||
items.append(contentsOf: textItems)
|
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 {
|
if case .empty = caption {
|
||||||
} else {
|
} else {
|
||||||
contentSize.height += 14.0
|
contentSize.height += 14.0
|
||||||
|
|
||||||
let styleStack = InstantPageTextStyleStack()
|
let styleStack = InstantPageTextStyleStack()
|
||||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
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
|
contentSize.height += captionContentSize.height
|
||||||
items.append(contentsOf: captionItems)
|
items.append(contentsOf: captionItems)
|
||||||
}
|
}
|
||||||
contentSize.height += verticalInset
|
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)
|
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)
|
items.append(shapeItem)
|
||||||
|
|
||||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
let verticalInset: CGFloat = 4.0
|
let verticalInset: CGFloat = 4.0
|
||||||
|
|
|
||||||
|
|
@ -163,16 +163,28 @@ public final class InstantPageTheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes {
|
func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes {
|
||||||
let clampedLevel = max(Int32(3), min(level, Int32(6)))
|
let clampedLevel = max(Int32(1), min(level, Int32(6)))
|
||||||
let subheaderAttributes = self.textCategories.subheader
|
|
||||||
guard clampedLevel > 3 else {
|
// H1/H2 reuse the theme's existing big-text categories verbatim, so they
|
||||||
return subheaderAttributes.withUnderline(link)
|
// 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
|
let baseSize: CGFloat
|
||||||
switch clampedLevel {
|
switch clampedLevel {
|
||||||
case 4:
|
case 3:
|
||||||
baseSize = 17.0
|
baseSize = 17.0
|
||||||
|
case 4:
|
||||||
|
baseSize = 16.0
|
||||||
case 5:
|
case 5:
|
||||||
baseSize = 15.0
|
baseSize = 15.0
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,16 @@ public func lastTextLineFrameIfLastItemIsText(in layout: InstantPageV2Layout) ->
|
||||||
else {
|
else {
|
||||||
return nil
|
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 ascent: CGFloat = 0.0
|
||||||
var descent: CGFloat = 0.0
|
var descent: CGFloat = 0.0
|
||||||
var leading: CGFloat = 0.0
|
var leading: CGFloat = 0.0
|
||||||
|
|
@ -585,14 +594,14 @@ private func layoutBlock(
|
||||||
return layoutCodeBlock(text, language: language, boundingWidth: boundingWidth,
|
return layoutCodeBlock(text, language: language, boundingWidth: boundingWidth,
|
||||||
horizontalInset: horizontalInset, context: &context)
|
horizontalInset: horizontalInset, context: &context)
|
||||||
|
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
return layoutBlockQuote(text: text, caption: caption, isPull: false,
|
return layoutBlockQuote(blocks: blocks, caption: caption,
|
||||||
boundingWidth: boundingWidth, horizontalInset: horizontalInset,
|
boundingWidth: boundingWidth, horizontalInset: horizontalInset,
|
||||||
context: &context)
|
isLast: isLast, context: &context)
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
return layoutBlockQuote(text: text, caption: caption, isPull: true,
|
return layoutQuoteText(text: text, caption: caption, isPull: true,
|
||||||
boundingWidth: boundingWidth, horizontalInset: horizontalInset,
|
boundingWidth: boundingWidth, horizontalInset: horizontalInset,
|
||||||
context: &context)
|
context: &context)
|
||||||
|
|
||||||
case let .image(id, caption, url, webpageId):
|
case let .image(id, caption, url, webpageId):
|
||||||
if case let .image(image) = context.media[id], let largest = largestImageRepresentation(image.representations) {
|
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)
|
// MARK: - Block quote / pull quote layout (ported from V1 InstantPageLayout.swift lines 517–586)
|
||||||
|
|
||||||
private func layoutBlockQuote(
|
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,
|
text: RichText,
|
||||||
caption: RichText,
|
caption: RichText,
|
||||||
isPull: Bool,
|
isPull: Bool,
|
||||||
|
|
@ -2005,21 +2094,17 @@ private func layoutList(
|
||||||
) -> [InstantPageV2LaidOutItem] {
|
) -> [InstantPageV2LaidOutItem] {
|
||||||
// Determine marker characteristics.
|
// Determine marker characteristics.
|
||||||
var maxIndexWidth: CGFloat = 0.0
|
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
|
var hasNums = false
|
||||||
|
|
||||||
if ordered {
|
if ordered {
|
||||||
for item in listItems {
|
for item in listItems {
|
||||||
if item.checked != nil {
|
if item.checked == nil, let num = item.num, !num.isEmpty {
|
||||||
hasTaskMarkers = true
|
|
||||||
} else if let num = item.num, !num.isEmpty {
|
|
||||||
hasNums = true
|
hasNums = true
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for item in listItems {
|
|
||||||
if item.checked != nil {
|
|
||||||
hasTaskMarkers = true
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2037,12 +2122,18 @@ private func layoutList(
|
||||||
stroke: context.theme.pageBackgroundColor,
|
stroke: context.theme.pageBackgroundColor,
|
||||||
border: context.theme.controlColor
|
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] = []
|
var markerInfos: [MarkerInfo] = []
|
||||||
for (i, item) in listItems.enumerated() {
|
for (i, item) in listItems.enumerated() {
|
||||||
if let checked = item.checked {
|
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))
|
markerInfos.append(MarkerInfo(kind: .checklist(checked: checked, colors: checkboxColors), naturalWidth: checklistMarkerSize.width))
|
||||||
} else if ordered {
|
} else if ordered {
|
||||||
let value: String
|
let value: String
|
||||||
|
|
@ -2076,13 +2167,20 @@ private func layoutList(
|
||||||
markerInfos.append(MarkerInfo(kind: .number(value), naturalWidth: w))
|
markerInfos.append(MarkerInfo(kind: .number(value), naturalWidth: w))
|
||||||
} else {
|
} else {
|
||||||
// Bullet: 6×6 ellipse (matches V1 InstantPageShapeItem dimensions).
|
// Bullet: 6×6 ellipse (matches V1 InstantPageShapeItem dimensions).
|
||||||
|
maxIndexWidth = max(maxIndexWidth, 6.0)
|
||||||
markerInfos.append(MarkerInfo(kind: .bullet, naturalWidth: 6.0))
|
markerInfos.append(MarkerInfo(kind: .bullet, naturalWidth: 6.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexSpacing = gap between the right edge of markers and the text content.
|
// Uniform 8pt marker→text gap across all four cases (ordered/unordered × bullet/number/
|
||||||
// V1 values: ordered-task=16, ordered-num=12, unordered-task=24, unordered-bullet=20.
|
// checkbox). With markers right-aligned to a shared column of width `maxIndexWidth`, text
|
||||||
let indexSpacing: CGFloat = ordered ? (hasTaskMarkers ? 16.0 : 12.0) : (hasTaskMarkers ? 24.0 : 20.0)
|
// 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.
|
// Layout each item.
|
||||||
var result: [InstantPageV2LaidOutItem] = []
|
var result: [InstantPageV2LaidOutItem] = []
|
||||||
|
|
@ -2134,8 +2232,6 @@ private func layoutList(
|
||||||
naturalWidth: markerInfo.naturalWidth,
|
naturalWidth: markerInfo.naturalWidth,
|
||||||
maxIndexWidth: maxIndexWidth,
|
maxIndexWidth: maxIndexWidth,
|
||||||
horizontalInset: horizontalInset,
|
horizontalInset: horizontalInset,
|
||||||
indexSpacing: indexSpacing,
|
|
||||||
ordered: ordered,
|
|
||||||
checklistMarkerSize: checklistMarkerSize,
|
checklistMarkerSize: checklistMarkerSize,
|
||||||
lineMidY: lineMidY,
|
lineMidY: lineMidY,
|
||||||
rtl: context.rtl,
|
rtl: context.rtl,
|
||||||
|
|
@ -2198,22 +2294,22 @@ private func layoutList(
|
||||||
previousBlock = subBlock
|
previousBlock = subBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// V1 alignment: number and bullet markers are positioned at originY (top of
|
// Mirror the .text case above (and what .checklist already does here): use the
|
||||||
// first sub-block); only checklist markers use firstBlockLineMidY for centering.
|
// first text line's midY for centering. `originY` is the sub-block's TOP, NOT a
|
||||||
let markerLineMidY: CGFloat
|
// line midpoint — `markerFrameFor` then subtracts `size.height / 2`, so feeding
|
||||||
switch markerInfo.kind {
|
// `originY` placed the marker straddling the sub-block boundary, ½·marker-height
|
||||||
case .checklist:
|
// ABOVE the first text line. V1 hid the same arithmetic under a 6×12 shape with a
|
||||||
markerLineMidY = firstBlockLineMidY ?? originY
|
// 3pt internal offset (matching ½·fontLineHeight for 17pt paragraph text), which
|
||||||
case .number, .bullet:
|
// by coincidence equals `firstBlockLineMidY`. Using firstBlockLineMidY directly
|
||||||
markerLineMidY = originY
|
// 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(
|
let markerFrame = markerFrameFor(
|
||||||
kind: markerInfo.kind,
|
kind: markerInfo.kind,
|
||||||
naturalWidth: markerInfo.naturalWidth,
|
naturalWidth: markerInfo.naturalWidth,
|
||||||
maxIndexWidth: maxIndexWidth,
|
maxIndexWidth: maxIndexWidth,
|
||||||
horizontalInset: horizontalInset,
|
horizontalInset: horizontalInset,
|
||||||
indexSpacing: indexSpacing,
|
|
||||||
ordered: ordered,
|
|
||||||
checklistMarkerSize: checklistMarkerSize,
|
checklistMarkerSize: checklistMarkerSize,
|
||||||
lineMidY: markerLineMidY,
|
lineMidY: markerLineMidY,
|
||||||
rtl: context.rtl,
|
rtl: context.rtl,
|
||||||
|
|
@ -2235,57 +2331,39 @@ private func layoutList(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the frame for a list marker, handling RTL and all three marker kinds.
|
/// 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(
|
private func markerFrameFor(
|
||||||
kind: InstantPageV2ListMarkerKind,
|
kind: InstantPageV2ListMarkerKind,
|
||||||
naturalWidth: CGFloat,
|
naturalWidth: CGFloat,
|
||||||
maxIndexWidth: CGFloat,
|
maxIndexWidth: CGFloat,
|
||||||
horizontalInset: CGFloat,
|
horizontalInset: CGFloat,
|
||||||
indexSpacing: CGFloat,
|
|
||||||
ordered: Bool,
|
|
||||||
checklistMarkerSize: CGSize,
|
checklistMarkerSize: CGSize,
|
||||||
lineMidY: CGFloat,
|
lineMidY: CGFloat,
|
||||||
rtl: Bool,
|
rtl: Bool,
|
||||||
boundingWidth: CGFloat
|
boundingWidth: CGFloat
|
||||||
) -> CGRect {
|
) -> CGRect {
|
||||||
|
let size: CGSize
|
||||||
switch kind {
|
switch kind {
|
||||||
case .bullet:
|
case .bullet:
|
||||||
// Bullet: 6×6 ellipse at vertically centred position.
|
size = CGSize(width: 6.0, height: 6.0)
|
||||||
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)
|
|
||||||
}
|
|
||||||
case .number:
|
case .number:
|
||||||
// Number: right-aligned within [horizontalInset, horizontalInset+maxIndexWidth].
|
size = CGSize(width: naturalWidth, height: 20.0)
|
||||||
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)
|
|
||||||
}
|
|
||||||
case .checklist:
|
case .checklist:
|
||||||
let size = checklistMarkerSize
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
// MARK: - Style helpers (ported from V1 InstantPageLayout.swift lines 32–88)
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ public final class LegacyJoinLinkPreviewController: ViewController {
|
||||||
if strongSelf.isRequest {
|
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))
|
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 {
|
} else {
|
||||||
if let peer = peer {
|
if let peer {
|
||||||
strongSelf.navigateToPeer(peer, nil)
|
strongSelf.navigateToPeer(peer, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,13 @@ private final class OpenInOptionsSheetContentComponent: Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
let optionInset: CGFloat = 2.0
|
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 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))
|
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)
|
transition.setFrame(view: self.scrollView, frame: scrollFrame)
|
||||||
|
|
@ -366,9 +373,9 @@ private final class OpenInOptionsSheetContentComponent: Component {
|
||||||
|
|
||||||
let optionOriginX: CGFloat
|
let optionOriginX: CGFloat
|
||||||
if component.options.count < 5 {
|
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 {
|
} else {
|
||||||
optionOriginX = optionInset + CGFloat(index) * optionSize.width
|
optionOriginX = optionInset + CGFloat(index) * (optionSize.width + optionSpacing)
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.setFrame(view: optionView, frame: CGRect(
|
transition.setFrame(view: optionView, frame: CGRect(
|
||||||
|
|
@ -388,7 +395,7 @@ private final class OpenInOptionsSheetContentComponent: Component {
|
||||||
if component.options.isEmpty {
|
if component.options.isEmpty {
|
||||||
optionsContentWidth = availableSize.width
|
optionsContentWidth = availableSize.width
|
||||||
} else {
|
} 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)
|
self.scrollView.contentSize = CGSize(width: max(availableSize.width, optionsContentWidth), height: optionSize.height)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||||
dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) }
|
dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) }
|
||||||
dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) }
|
dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) }
|
||||||
dict[-2137335386] = { return Api.BotInlineMessage.parse_botInlineMessageMediaWebPage($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[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) }
|
||||||
dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) }
|
dict[400266251] = { return Api.BotInlineResult.parse_botInlineMediaResult($0) }
|
||||||
dict[295067450] = { return Api.BotInlineResult.parse_botInlineResult($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[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) }
|
||||||
dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) }
|
dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) }
|
||||||
dict[1137015880] = { return Api.DocumentAttribute.parse_documentAttributeVideo($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[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) }
|
||||||
dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) }
|
dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) }
|
||||||
dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($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[-672693723] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaInvoice($0) }
|
||||||
dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) }
|
dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) }
|
||||||
dict[-1109605104] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaWebPage($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[1036876423] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageText($0) }
|
||||||
dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) }
|
dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) }
|
||||||
dict[-1227287081] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID64($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[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) }
|
||||||
dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) }
|
dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) }
|
||||||
dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($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[1528613672] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialFirebasePNV($0) }
|
||||||
dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) }
|
dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) }
|
||||||
dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($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[1001931436] = { return Api.OutboxReadDate.parse_outboxReadDate($0) }
|
||||||
dict[-1738178803] = { return Api.Page.parse_page($0) }
|
dict[-1738178803] = { return Api.Page.parse_page($0) }
|
||||||
dict[1464557951] = { return Api.PageBlock.parse_inputPageBlockMap($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[-837994576] = { return Api.PageBlock.parse_pageBlockAnchor($0) }
|
||||||
dict[-2143067670] = { return Api.PageBlock.parse_pageBlockAudio($0) }
|
dict[-2143067670] = { return Api.PageBlock.parse_pageBlockAudio($0) }
|
||||||
dict[-1162877472] = { return Api.PageBlock.parse_pageBlockAuthorDate($0) }
|
dict[-1162877472] = { return Api.PageBlock.parse_pageBlockAuthorDate($0) }
|
||||||
dict[641563686] = { return Api.PageBlock.parse_pageBlockBlockquote($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[-283684427] = { return Api.PageBlock.parse_pageBlockChannel($0) }
|
||||||
dict[1705048653] = { return Api.PageBlock.parse_pageBlockCollage($0) }
|
dict[1705048653] = { return Api.PageBlock.parse_pageBlockCollage($0) }
|
||||||
dict[972174080] = { return Api.PageBlock.parse_pageBlockCover($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[-454524911] = { return Api.PageBlock.parse_pageBlockList($0) }
|
||||||
dict[-1538310410] = { return Api.PageBlock.parse_pageBlockMap($0) }
|
dict[-1538310410] = { return Api.PageBlock.parse_pageBlockMap($0) }
|
||||||
dict[1493699616] = { return Api.PageBlock.parse_pageBlockMath($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[1182402406] = { return Api.PageBlock.parse_pageBlockParagraph($0) }
|
||||||
dict[391759200] = { return Api.PageBlock.parse_pageBlockPhoto($0) }
|
dict[391759200] = { return Api.PageBlock.parse_pageBlockPhoto($0) }
|
||||||
dict[-1066346178] = { return Api.PageBlock.parse_pageBlockPreformatted($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[1869903447] = { return Api.PageCaption.parse_pageCaption($0) }
|
||||||
dict[1674209194] = { return Api.PageListItem.parse_pageListItemBlocks($0) }
|
dict[1674209194] = { return Api.PageListItem.parse_pageListItemBlocks($0) }
|
||||||
dict[794323004] = { return Api.PageListItem.parse_pageListItemText($0) }
|
dict[794323004] = { return Api.PageListItem.parse_pageListItemText($0) }
|
||||||
dict[1109995988] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) }
|
dict[-1879910928] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) }
|
||||||
dict[-851533770] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) }
|
dict[352522633] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) }
|
||||||
dict[-1282352120] = { return Api.PageRelatedArticle.parse_pageRelatedArticle($0) }
|
dict[-1282352120] = { return Api.PageRelatedArticle.parse_pageRelatedArticle($0) }
|
||||||
dict[878078826] = { return Api.PageTableCell.parse_pageTableCell($0) }
|
dict[878078826] = { return Api.PageTableCell.parse_pageTableCell($0) }
|
||||||
dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) }
|
dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) }
|
||||||
|
|
@ -2042,8 +2042,6 @@ public extension Api {
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.InputNotifyPeer:
|
case let _1 as Api.InputNotifyPeer:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.InputPageListOrderedItem:
|
|
||||||
_1.serialize(buffer, boxed)
|
|
||||||
case let _1 as Api.InputPasskeyCredential:
|
case let _1 as Api.InputPasskeyCredential:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.InputPasskeyResponse:
|
case let _1 as Api.InputPasskeyResponse:
|
||||||
|
|
|
||||||
|
|
@ -1451,131 +1451,85 @@ public extension Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
indirect enum InputPageListOrderedItem: TypeConstructorDescription {
|
enum InputPasskeyCredential: TypeConstructorDescription {
|
||||||
public class Cons_inputPageListOrderedItemBlocks: TypeConstructorDescription {
|
public class Cons_inputPasskeyCredentialFirebasePNV: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var pnvToken: String
|
||||||
public var blocks: [Api.PageBlock]
|
public init(pnvToken: String) {
|
||||||
public var value: Int32?
|
self.pnvToken = pnvToken
|
||||||
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
|
|
||||||
}
|
}
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
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 class Cons_inputPasskeyCredentialPublicKey: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var id: String
|
||||||
public var text: Api.RichText
|
public var rawId: String
|
||||||
public var value: Int32?
|
public var response: Api.InputPasskeyResponse
|
||||||
public var type: String?
|
public init(id: String, rawId: String, response: Api.InputPasskeyResponse) {
|
||||||
public init(flags: Int32, text: Api.RichText, value: Int32?, type: String?) {
|
self.id = id
|
||||||
self.flags = flags
|
self.rawId = rawId
|
||||||
self.text = text
|
self.response = response
|
||||||
self.value = value
|
|
||||||
self.type = type
|
|
||||||
}
|
}
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
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 inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV)
|
||||||
case inputPageListOrderedItemText(Cons_inputPageListOrderedItemText)
|
case inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
case .inputPageListOrderedItemBlocks(let _data):
|
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(2105227266)
|
buffer.appendInt32(1528613672)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
serializeString(_data.pnvToken, buffer: buffer, boxed: false)
|
||||||
break
|
break
|
||||||
case .inputPageListOrderedItemText(let _data):
|
case .inputPasskeyCredentialPublicKey(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1682665696)
|
buffer.appendInt32(1009235855)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
serializeString(_data.id, buffer: buffer, boxed: false)
|
||||||
|
serializeString(_data.rawId, buffer: buffer, boxed: false)
|
||||||
|
_data.response.serialize(buffer, true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .inputPageListOrderedItemBlocks(let _data):
|
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||||
return ("inputPageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(_data.flags)), ("blocks", ConstructorParameterDescription(_data.blocks)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))])
|
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(_data.pnvToken))])
|
||||||
case .inputPageListOrderedItemText(let _data):
|
case .inputPasskeyCredentialPublicKey(let _data):
|
||||||
return ("inputPageListOrderedItemText", [("flags", ConstructorParameterDescription(_data.flags)), ("text", ConstructorParameterDescription(_data.text)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))])
|
return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(_data.id)), ("rawId", ConstructorParameterDescription(_data.rawId)), ("response", ConstructorParameterDescription(_data.response))])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse_inputPageListOrderedItemBlocks(_ reader: BufferReader) -> InputPageListOrderedItem? {
|
public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||||
var _1: Int32?
|
var _1: String?
|
||||||
_1 = reader.readInt32()
|
_1 = parseString(reader)
|
||||||
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)
|
|
||||||
}
|
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
if _c1 {
|
||||||
let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil
|
return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV(pnvToken: _1!))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func parse_inputPageListOrderedItemText(_ reader: BufferReader) -> InputPageListOrderedItem? {
|
public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||||
var _1: Int32?
|
var _1: String?
|
||||||
_1 = reader.readInt32()
|
_1 = parseString(reader)
|
||||||
var _2: Api.RichText?
|
var _2: String?
|
||||||
|
_2 = parseString(reader)
|
||||||
|
var _3: Api.InputPasskeyResponse?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
_2 = Api.parse(reader, signature: signature) as? Api.RichText
|
_3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil
|
let _c3 = _3 != nil
|
||||||
let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil
|
if _c1 && _c2 && _c3 {
|
||||||
if _c1 && _c2 && _c3 && _c4 {
|
return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!))
|
||||||
return Api.InputPageListOrderedItem.inputPageListOrderedItemText(Cons_inputPageListOrderedItemText(flags: _1!, text: _2!, value: _3, type: _4))
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
public extension Api {
|
||||||
enum InputPasskeyResponse: TypeConstructorDescription {
|
enum InputPasskeyResponse: TypeConstructorDescription {
|
||||||
public class Cons_inputPasskeyResponseLogin: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
public extension Api {
|
||||||
enum InputQuickReplyShortcut: TypeConstructorDescription {
|
enum InputQuickReplyShortcut: TypeConstructorDescription {
|
||||||
public class Cons_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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
public extension Api {
|
||||||
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
|
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
|
||||||
public class Cons_inputStorePaymentAuthCode: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
public extension Api {
|
||||||
indirect enum KeyboardButton: TypeConstructorDescription {
|
indirect enum KeyboardButton: TypeConstructorDescription {
|
||||||
public class Cons_inputKeyboardButtonRequestPeer: TypeConstructorDescription {
|
public class Cons_inputKeyboardButtonRequestPeer: TypeConstructorDescription {
|
||||||
|
|
|
||||||
|
|
@ -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))])
|
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 class Cons_pageBlockAnchor: TypeConstructorDescription {
|
||||||
public var name: String
|
public var name: String
|
||||||
public init(name: String) {
|
public init(name: String) {
|
||||||
|
|
@ -487,6 +472,17 @@ public extension Api {
|
||||||
return ("pageBlockBlockquote", [("text", ConstructorParameterDescription(self.text)), ("caption", ConstructorParameterDescription(self.caption))])
|
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 class Cons_pageBlockChannel: TypeConstructorDescription {
|
||||||
public var channel: Api.Chat
|
public var channel: Api.Chat
|
||||||
public init(channel: Api.Chat) {
|
public init(channel: Api.Chat) {
|
||||||
|
|
@ -688,12 +684,18 @@ public extension Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class Cons_pageBlockOrderedList: TypeConstructorDescription {
|
public class Cons_pageBlockOrderedList: TypeConstructorDescription {
|
||||||
|
public var flags: Int32
|
||||||
public var items: [Api.PageListOrderedItem]
|
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.items = items
|
||||||
|
self.start = start
|
||||||
|
self.type = type
|
||||||
}
|
}
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
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 {
|
public class Cons_pageBlockParagraph: TypeConstructorDescription {
|
||||||
|
|
@ -829,11 +831,11 @@ public extension Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case inputPageBlockMap(Cons_inputPageBlockMap)
|
case inputPageBlockMap(Cons_inputPageBlockMap)
|
||||||
case inputPageBlockOrderedList(Cons_inputPageBlockOrderedList)
|
|
||||||
case pageBlockAnchor(Cons_pageBlockAnchor)
|
case pageBlockAnchor(Cons_pageBlockAnchor)
|
||||||
case pageBlockAudio(Cons_pageBlockAudio)
|
case pageBlockAudio(Cons_pageBlockAudio)
|
||||||
case pageBlockAuthorDate(Cons_pageBlockAuthorDate)
|
case pageBlockAuthorDate(Cons_pageBlockAuthorDate)
|
||||||
case pageBlockBlockquote(Cons_pageBlockBlockquote)
|
case pageBlockBlockquote(Cons_pageBlockBlockquote)
|
||||||
|
case pageBlockBlockquoteBlocks(Cons_pageBlockBlockquoteBlocks)
|
||||||
case pageBlockChannel(Cons_pageBlockChannel)
|
case pageBlockChannel(Cons_pageBlockChannel)
|
||||||
case pageBlockCollage(Cons_pageBlockCollage)
|
case pageBlockCollage(Cons_pageBlockCollage)
|
||||||
case pageBlockCover(Cons_pageBlockCover)
|
case pageBlockCover(Cons_pageBlockCover)
|
||||||
|
|
@ -880,23 +882,6 @@ public extension Api {
|
||||||
serializeInt32(_data.h, buffer: buffer, boxed: false)
|
serializeInt32(_data.h, buffer: buffer, boxed: false)
|
||||||
_data.caption.serialize(buffer, true)
|
_data.caption.serialize(buffer, true)
|
||||||
break
|
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):
|
case .pageBlockAnchor(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-837994576)
|
buffer.appendInt32(-837994576)
|
||||||
|
|
@ -924,6 +909,17 @@ public extension Api {
|
||||||
_data.text.serialize(buffer, true)
|
_data.text.serialize(buffer, true)
|
||||||
_data.caption.serialize(buffer, true)
|
_data.caption.serialize(buffer, true)
|
||||||
break
|
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):
|
case .pageBlockChannel(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-283684427)
|
buffer.appendInt32(-283684427)
|
||||||
|
|
@ -1084,13 +1080,20 @@ public extension Api {
|
||||||
break
|
break
|
||||||
case .pageBlockOrderedList(let _data):
|
case .pageBlockOrderedList(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1702174239)
|
buffer.appendInt32(534181569)
|
||||||
}
|
}
|
||||||
|
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
||||||
buffer.appendInt32(481674261)
|
buffer.appendInt32(481674261)
|
||||||
buffer.appendInt32(Int32(_data.items.count))
|
buffer.appendInt32(Int32(_data.items.count))
|
||||||
for item in _data.items {
|
for item in _data.items {
|
||||||
item.serialize(buffer, true)
|
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
|
break
|
||||||
case .pageBlockParagraph(let _data):
|
case .pageBlockParagraph(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
|
|
@ -1204,8 +1207,6 @@ public extension Api {
|
||||||
switch self {
|
switch self {
|
||||||
case .inputPageBlockMap(let _data):
|
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))])
|
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):
|
case .pageBlockAnchor(let _data):
|
||||||
return ("pageBlockAnchor", [("name", ConstructorParameterDescription(_data.name))])
|
return ("pageBlockAnchor", [("name", ConstructorParameterDescription(_data.name))])
|
||||||
case .pageBlockAudio(let _data):
|
case .pageBlockAudio(let _data):
|
||||||
|
|
@ -1214,6 +1215,8 @@ public extension Api {
|
||||||
return ("pageBlockAuthorDate", [("author", ConstructorParameterDescription(_data.author)), ("publishedDate", ConstructorParameterDescription(_data.publishedDate))])
|
return ("pageBlockAuthorDate", [("author", ConstructorParameterDescription(_data.author)), ("publishedDate", ConstructorParameterDescription(_data.publishedDate))])
|
||||||
case .pageBlockBlockquote(let _data):
|
case .pageBlockBlockquote(let _data):
|
||||||
return ("pageBlockBlockquote", [("text", ConstructorParameterDescription(_data.text)), ("caption", ConstructorParameterDescription(_data.caption))])
|
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):
|
case .pageBlockChannel(let _data):
|
||||||
return ("pageBlockChannel", [("channel", ConstructorParameterDescription(_data.channel))])
|
return ("pageBlockChannel", [("channel", ConstructorParameterDescription(_data.channel))])
|
||||||
case .pageBlockCollage(let _data):
|
case .pageBlockCollage(let _data):
|
||||||
|
|
@ -1253,7 +1256,7 @@ public extension Api {
|
||||||
case .pageBlockMath(let _data):
|
case .pageBlockMath(let _data):
|
||||||
return ("pageBlockMath", [("source", ConstructorParameterDescription(_data.source))])
|
return ("pageBlockMath", [("source", ConstructorParameterDescription(_data.source))])
|
||||||
case .pageBlockOrderedList(let _data):
|
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):
|
case .pageBlockParagraph(let _data):
|
||||||
return ("pageBlockParagraph", [("text", ConstructorParameterDescription(_data.text))])
|
return ("pageBlockParagraph", [("text", ConstructorParameterDescription(_data.text))])
|
||||||
case .pageBlockPhoto(let _data):
|
case .pageBlockPhoto(let _data):
|
||||||
|
|
@ -1310,32 +1313,6 @@ public extension Api {
|
||||||
return nil
|
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? {
|
public static func parse_pageBlockAnchor(_ reader: BufferReader) -> PageBlock? {
|
||||||
var _1: String?
|
var _1: String?
|
||||||
_1 = parseString(reader)
|
_1 = parseString(reader)
|
||||||
|
|
@ -1397,6 +1374,24 @@ public extension Api {
|
||||||
return nil
|
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? {
|
public static func parse_pageBlockChannel(_ reader: BufferReader) -> PageBlock? {
|
||||||
var _1: Api.Chat?
|
var _1: Api.Chat?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
|
|
@ -1708,13 +1703,26 @@ public extension Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func parse_pageBlockOrderedList(_ reader: BufferReader) -> PageBlock? {
|
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() {
|
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
|
let _c1 = _1 != nil
|
||||||
if _c1 {
|
let _c2 = _2 != nil
|
||||||
return Api.PageBlock.pageBlockOrderedList(Cons_pageBlockOrderedList(items: _1!))
|
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 {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -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))])
|
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 class Cons_botInlineMessageText: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var flags: Int32
|
||||||
public var message: String
|
public var message: String
|
||||||
|
|
@ -780,6 +793,7 @@ public extension Api {
|
||||||
case botInlineMessageMediaInvoice(Cons_botInlineMessageMediaInvoice)
|
case botInlineMessageMediaInvoice(Cons_botInlineMessageMediaInvoice)
|
||||||
case botInlineMessageMediaVenue(Cons_botInlineMessageMediaVenue)
|
case botInlineMessageMediaVenue(Cons_botInlineMessageMediaVenue)
|
||||||
case botInlineMessageMediaWebPage(Cons_botInlineMessageMediaWebPage)
|
case botInlineMessageMediaWebPage(Cons_botInlineMessageMediaWebPage)
|
||||||
|
case botInlineMessageRichMessage(Cons_botInlineMessageRichMessage)
|
||||||
case botInlineMessageText(Cons_botInlineMessageText)
|
case botInlineMessageText(Cons_botInlineMessageText)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
|
@ -882,6 +896,16 @@ public extension Api {
|
||||||
_data.replyMarkup!.serialize(buffer, true)
|
_data.replyMarkup!.serialize(buffer, true)
|
||||||
}
|
}
|
||||||
break
|
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):
|
case .botInlineMessageText(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1937807902)
|
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))])
|
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):
|
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))])
|
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):
|
case .botInlineMessageText(let _data):
|
||||||
return ("botInlineMessageText", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))])
|
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
|
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? {
|
public static func parse_botInlineMessageText(_ reader: BufferReader) -> BotInlineMessage? {
|
||||||
var _1: Int32?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_1 = reader.readInt32()
|
||||||
|
|
|
||||||
|
|
@ -149,28 +149,36 @@ public extension Api {
|
||||||
indirect enum PageListOrderedItem: TypeConstructorDescription {
|
indirect enum PageListOrderedItem: TypeConstructorDescription {
|
||||||
public class Cons_pageListOrderedItemBlocks: TypeConstructorDescription {
|
public class Cons_pageListOrderedItemBlocks: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var flags: Int32
|
||||||
public var num: String
|
public var num: String?
|
||||||
public var blocks: [Api.PageBlock]
|
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.flags = flags
|
||||||
self.num = num
|
self.num = num
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
|
self.value = value
|
||||||
|
self.type = type
|
||||||
}
|
}
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
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 class Cons_pageListOrderedItemText: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var flags: Int32
|
||||||
public var num: String
|
public var num: String?
|
||||||
public var text: Api.RichText
|
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.flags = flags
|
||||||
self.num = num
|
self.num = num
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.value = value
|
||||||
|
self.type = type
|
||||||
}
|
}
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
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)
|
case pageListOrderedItemBlocks(Cons_pageListOrderedItemBlocks)
|
||||||
|
|
@ -180,23 +188,39 @@ public extension Api {
|
||||||
switch self {
|
switch self {
|
||||||
case .pageListOrderedItemBlocks(let _data):
|
case .pageListOrderedItemBlocks(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(1109995988)
|
buffer.appendInt32(-1879910928)
|
||||||
}
|
}
|
||||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
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(481674261)
|
||||||
buffer.appendInt32(Int32(_data.blocks.count))
|
buffer.appendInt32(Int32(_data.blocks.count))
|
||||||
for item in _data.blocks {
|
for item in _data.blocks {
|
||||||
item.serialize(buffer, true)
|
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
|
break
|
||||||
case .pageListOrderedItemText(let _data):
|
case .pageListOrderedItemText(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-851533770)
|
buffer.appendInt32(352522633)
|
||||||
}
|
}
|
||||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
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)
|
_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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,9 +228,9 @@ public extension Api {
|
||||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .pageListOrderedItemBlocks(let _data):
|
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):
|
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?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_1 = reader.readInt32()
|
||||||
var _2: String?
|
var _2: String?
|
||||||
_2 = parseString(reader)
|
if Int(_1 ?? 0) & Int(1 << 2) != 0 {
|
||||||
|
_2 = parseString(reader)
|
||||||
|
}
|
||||||
var _3: [Api.PageBlock]?
|
var _3: [Api.PageBlock]?
|
||||||
if let _ = reader.readInt32() {
|
if let _ = reader.readInt32() {
|
||||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageBlock.self)
|
_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 _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
if _c1 && _c2 && _c3 {
|
let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil
|
||||||
return Api.PageListOrderedItem.pageListOrderedItemBlocks(Cons_pageListOrderedItemBlocks(flags: _1!, num: _2!, blocks: _3!))
|
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 {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -233,16 +269,28 @@ public extension Api {
|
||||||
var _1: Int32?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_1 = reader.readInt32()
|
||||||
var _2: String?
|
var _2: String?
|
||||||
_2 = parseString(reader)
|
if Int(_1 ?? 0) & Int(1 << 2) != 0 {
|
||||||
|
_2 = parseString(reader)
|
||||||
|
}
|
||||||
var _3: Api.RichText?
|
var _3: Api.RichText?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
_3 = Api.parse(reader, signature: signature) as? Api.RichText
|
_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 _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
if _c1 && _c2 && _c3 {
|
let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil
|
||||||
return Api.PageListOrderedItem.pageListOrderedItemText(Cons_pageListOrderedItemText(flags: _1!, num: _2!, text: _3!))
|
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 {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -553,8 +553,8 @@ public extension Api {
|
||||||
public var date: Int32
|
public var date: Int32
|
||||||
public var effect: Int64?
|
public var effect: Int64?
|
||||||
public var suggestedPost: Api.SuggestedPost?
|
public var suggestedPost: Api.SuggestedPost?
|
||||||
public var 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.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.RichMessage?) {
|
||||||
self.flags = flags
|
self.flags = flags
|
||||||
self.replyTo = replyTo
|
self.replyTo = replyTo
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
@ -587,7 +587,7 @@ public extension Api {
|
||||||
switch self {
|
switch self {
|
||||||
case .draftMessage(let _data):
|
case .draftMessage(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1743452271)
|
buffer.appendInt32(1627271828)
|
||||||
}
|
}
|
||||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
||||||
if Int(_data.flags) & Int(1 << 4) != 0 {
|
if Int(_data.flags) & Int(1 << 4) != 0 {
|
||||||
|
|
@ -671,10 +671,10 @@ public extension Api {
|
||||||
_8 = Api.parse(reader, signature: signature) as? Api.SuggestedPost
|
_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 Int(_1 ?? 0) & Int(1 << 9) != 0 {
|
||||||
if let signature = reader.readInt32() {
|
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
|
let _c1 = _1 != nil
|
||||||
|
|
|
||||||
|
|
@ -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))])
|
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 class Cons_inputBotInlineMessageText: TypeConstructorDescription {
|
||||||
public var flags: Int32
|
public var flags: Int32
|
||||||
public var message: String
|
public var message: String
|
||||||
|
|
@ -1019,6 +1032,7 @@ public extension Api {
|
||||||
case inputBotInlineMessageMediaInvoice(Cons_inputBotInlineMessageMediaInvoice)
|
case inputBotInlineMessageMediaInvoice(Cons_inputBotInlineMessageMediaInvoice)
|
||||||
case inputBotInlineMessageMediaVenue(Cons_inputBotInlineMessageMediaVenue)
|
case inputBotInlineMessageMediaVenue(Cons_inputBotInlineMessageMediaVenue)
|
||||||
case inputBotInlineMessageMediaWebPage(Cons_inputBotInlineMessageMediaWebPage)
|
case inputBotInlineMessageMediaWebPage(Cons_inputBotInlineMessageMediaWebPage)
|
||||||
|
case inputBotInlineMessageRichMessage(Cons_inputBotInlineMessageRichMessage)
|
||||||
case inputBotInlineMessageText(Cons_inputBotInlineMessageText)
|
case inputBotInlineMessageText(Cons_inputBotInlineMessageText)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
|
@ -1132,6 +1146,16 @@ public extension Api {
|
||||||
_data.replyMarkup!.serialize(buffer, true)
|
_data.replyMarkup!.serialize(buffer, true)
|
||||||
}
|
}
|
||||||
break
|
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):
|
case .inputBotInlineMessageText(let _data):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(1036876423)
|
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))])
|
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):
|
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))])
|
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):
|
case .inputBotInlineMessageText(let _data):
|
||||||
return ("inputBotInlineMessageText", [("flags", ConstructorParameterDescription(_data.flags)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup))])
|
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
|
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? {
|
public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? {
|
||||||
var _1: Int32?
|
var _1: Int32?
|
||||||
_1 = reader.readInt32()
|
_1 = reader.readInt32()
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,9 @@ table InstantPageBlock_List {
|
||||||
}
|
}
|
||||||
|
|
||||||
table InstantPageBlock_BlockQuote {
|
table InstantPageBlock_BlockQuote {
|
||||||
text:RichText (id: 0, required);
|
text:RichText (id: 0);
|
||||||
caption:RichText (id: 1, required);
|
caption:RichText (id: 1, required);
|
||||||
|
blocks:[InstantPageBlock] (id: 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
table InstantPageBlock_PullQuote {
|
table InstantPageBlock_PullQuote {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
case auto(caption: String, entities: TextEntitiesMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?)
|
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 mapLocation(media: TelegramMediaMap, replyMarkup: ReplyMarkupMessageAttribute?)
|
||||||
case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?)
|
case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?)
|
||||||
case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?)
|
case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?)
|
||||||
|
|
@ -22,7 +22,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
|
||||||
case 0:
|
case 0:
|
||||||
self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
|
self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
|
||||||
case 1:
|
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:
|
case 2:
|
||||||
self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
|
self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
|
||||||
case 3:
|
case 3:
|
||||||
|
|
@ -51,7 +51,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
|
||||||
} else {
|
} else {
|
||||||
encoder.encodeNil(forKey: "m")
|
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.encodeInt32(1, forKey: "_v")
|
||||||
encoder.encodeString(text, forKey: "t")
|
encoder.encodeString(text, forKey: "t")
|
||||||
if let entities = entities {
|
if let entities = entities {
|
||||||
|
|
@ -59,6 +59,11 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
|
||||||
} else {
|
} else {
|
||||||
encoder.encodeNil(forKey: "e")
|
encoder.encodeNil(forKey: "e")
|
||||||
}
|
}
|
||||||
|
if let richText {
|
||||||
|
encoder.encodeObject(richText, forKey: "rt")
|
||||||
|
} else {
|
||||||
|
encoder.encodeNil(forKey: "rt")
|
||||||
|
}
|
||||||
encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du")
|
encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du")
|
||||||
if let previewParameters = previewParameters {
|
if let previewParameters = previewParameters {
|
||||||
encoder.encodeObject(previewParameters, forKey: "prp")
|
encoder.encodeObject(previewParameters, forKey: "prp")
|
||||||
|
|
@ -148,14 +153,17 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .text(lhsText, lhsEntities, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup):
|
case let .text(lhsText, lhsEntities, lhsRichText, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup):
|
||||||
if case let .text(rhsText, rhsEntities, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs {
|
if case let .text(rhsText, rhsEntities, rhsRichText, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs {
|
||||||
if lhsText != rhsText {
|
if lhsText != rhsText {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhsEntities != rhsEntities {
|
if lhsEntities != rhsEntities {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhsRichText != rhsRichText {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhsDisableUrlPreview != rhsDisableUrlPreview {
|
if lhsDisableUrlPreview != rhsDisableUrlPreview {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -508,12 +516,18 @@ extension ChatContextResultMessage {
|
||||||
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
|
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
|
||||||
}
|
}
|
||||||
let leadingPreview = (flags & (1 << 3)) != 0
|
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,
|
leadingPreview: leadingPreview,
|
||||||
forceLargeMedia: nil,
|
forceLargeMedia: nil,
|
||||||
isManuallyAdded: false,
|
isManuallyAdded: false,
|
||||||
isSafe: false
|
isSafe: false
|
||||||
), replyMarkup: parsedReplyMarkup)
|
), 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):
|
case let .botInlineMessageMediaGeo(botInlineMessageMediaGeoData):
|
||||||
let (_, geo, heading, period, proximityNotificationRadius, replyMarkup) = (botInlineMessageMediaGeoData.flags, botInlineMessageMediaGeoData.geo, botInlineMessageMediaGeoData.heading, botInlineMessageMediaGeoData.period, botInlineMessageMediaGeoData.proximityNotificationRadius, botInlineMessageMediaGeoData.replyMarkup)
|
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)
|
let media = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading)
|
||||||
|
|
|
||||||
|
|
@ -89,30 +89,25 @@ extension InstantPageListItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiInputPageOrderedListItem() -> Api.InputPageListOrderedItem {
|
func apiInputPageOrderedListItem() -> Api.PageListOrderedItem {
|
||||||
switch self {
|
switch self {
|
||||||
case let .text(value, num, checked):
|
case let .text(value, num, checked):
|
||||||
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
||||||
|
|
||||||
var inputNum: Int32?
|
if num != nil {
|
||||||
if let num, let numValue = Int32(num) {
|
|
||||||
inputNum = numValue
|
|
||||||
flags |= (1 << 2)
|
flags |= (1 << 2)
|
||||||
}
|
}
|
||||||
|
return .pageListOrderedItemText(Api.PageListOrderedItem.Cons_pageListOrderedItemText(flags: flags, num: num, text: value.apiRichText(), value: nil, type: nil))
|
||||||
return .inputPageListOrderedItemText(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemText(flags: flags, text: value.apiRichText(), value: inputNum, type: nil))
|
|
||||||
case let .blocks(blocks, num, checked):
|
case let .blocks(blocks, num, checked):
|
||||||
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
||||||
|
|
||||||
var inputNum: Int32?
|
if num != nil {
|
||||||
if let num, let numValue = Int32(num) {
|
|
||||||
inputNum = numValue
|
|
||||||
flags |= (1 << 2)
|
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:
|
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)
|
self = .anchor(name)
|
||||||
case let .pageBlockBlockquote(pageBlockBlockquoteData):
|
case let .pageBlockBlockquote(pageBlockBlockquoteData):
|
||||||
let (text, caption) = (pageBlockBlockquoteData.text, pageBlockBlockquoteData.caption)
|
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):
|
case let .pageBlockPullquote(pageBlockPullquoteData):
|
||||||
let (text, caption) = (pageBlockPullquoteData.text, pageBlockPullquoteData.caption)
|
let (text, caption) = (pageBlockPullquoteData.text, pageBlockPullquoteData.caption)
|
||||||
self = .pullQuote(text: RichText(apiText: text), caption: RichText(apiText: 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)
|
self = .heading(text: RichText(apiText: pageBlockHeading6.text), level: 6)
|
||||||
case let .pageBlockMath(pageBlockMath):
|
case let .pageBlockMath(pageBlockMath):
|
||||||
self = .formula(latex: pageBlockMath.source)
|
self = .formula(latex: pageBlockMath.source)
|
||||||
case .inputPageBlockMap, .inputPageBlockOrderedList, .pageBlockThinking:
|
case .inputPageBlockMap, .pageBlockThinking:
|
||||||
self = .unsupported
|
self = .unsupported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,12 +368,18 @@ extension InstantPageBlock {
|
||||||
return .pageBlockAnchor(Api.PageBlock.Cons_pageBlockAnchor(name: value))
|
return .pageBlockAnchor(Api.PageBlock.Cons_pageBlockAnchor(name: value))
|
||||||
case let .list(items, ordered):
|
case let .list(items, ordered):
|
||||||
if 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 {
|
} else {
|
||||||
return .pageBlockList(Api.PageBlock.Cons_pageBlockList(items: items.map { $0.apiInputPageListItem() }))
|
return .pageBlockList(Api.PageBlock.Cons_pageBlockList(items: items.map { $0.apiInputPageListItem() }))
|
||||||
}
|
}
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
return .pageBlockBlockquote(Api.PageBlock.Cons_pageBlockBlockquote(text: text.apiRichText(), caption: caption.apiRichText()))
|
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):
|
case let .pullQuote(text, caption):
|
||||||
return .pageBlockPullquote(Api.PageBlock.Cons_pageBlockPullquote(text: text.apiRichText(), caption: caption.apiRichText()))
|
return .pageBlockPullquote(Api.PageBlock.Cons_pageBlockPullquote(text: text.apiRichText(), caption: caption.apiRichText()))
|
||||||
case let .image(id, caption, url, webpageId):
|
case let .image(id, caption, url, webpageId):
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
case divider
|
case divider
|
||||||
case anchor(String)
|
case anchor(String)
|
||||||
case list(items: [InstantPageListItem], ordered: Bool)
|
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 pullQuote(text: RichText, caption: RichText)
|
||||||
case image(id: MediaId, caption: InstantPageCaption, url: String?, webpageId: MediaId?)
|
case image(id: MediaId, caption: InstantPageCaption, url: String?, webpageId: MediaId?)
|
||||||
case video(id: MediaId, caption: InstantPageCaption, autoplay: Bool, loop: Bool)
|
case video(id: MediaId, caption: InstantPageCaption, autoplay: Bool, loop: Bool)
|
||||||
|
|
@ -121,7 +121,13 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
case InstantPageBlockType.list.rawValue:
|
case InstantPageBlockType.list.rawValue:
|
||||||
self = .list(items: decodeListItems(decoder), ordered: decoder.decodeOptionalInt32ForKey("o") != 0)
|
self = .list(items: decodeListItems(decoder), ordered: decoder.decodeOptionalInt32ForKey("o") != 0)
|
||||||
case InstantPageBlockType.blockQuote.rawValue:
|
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:
|
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)
|
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:
|
case InstantPageBlockType.image.rawValue:
|
||||||
|
|
@ -225,9 +231,9 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
encoder.encodeInt32(InstantPageBlockType.list.rawValue, forKey: "r")
|
encoder.encodeInt32(InstantPageBlockType.list.rawValue, forKey: "r")
|
||||||
encoder.encodeObjectArray(items, forKey: "ml")
|
encoder.encodeObjectArray(items, forKey: "ml")
|
||||||
encoder.encodeInt32(ordered ? 1 : 0, forKey: "o")
|
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.encodeInt32(InstantPageBlockType.blockQuote.rawValue, forKey: "r")
|
||||||
encoder.encodeObject(text, forKey: "t")
|
encoder.encodeObjectArray(blocks, forKey: "b")
|
||||||
encoder.encodeObject(caption, forKey: "c")
|
encoder.encodeObject(caption, forKey: "c")
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
encoder.encodeInt32(InstantPageBlockType.pullQuote.rawValue, forKey: "r")
|
encoder.encodeInt32(InstantPageBlockType.pullQuote.rawValue, forKey: "r")
|
||||||
|
|
@ -445,8 +451,8 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(lhsBlocks, lhsCaption):
|
||||||
if case .blockQuote(text, caption) = rhs {
|
if case let .blockQuote(rhsBlocks, rhsCaption) = rhs, lhsBlocks == rhsBlocks, lhsCaption == rhsCaption {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
|
|
@ -621,7 +627,15 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_BlockQuote.self) else {
|
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_BlockQuote.self) else {
|
||||||
throw FlatBuffersError.missingRequiredField()
|
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:
|
case .instantpageblockPullquote:
|
||||||
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_PullQuote.self) else {
|
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_PullQuote.self) else {
|
||||||
throw FlatBuffersError.missingRequiredField()
|
throw FlatBuffersError.missingRequiredField()
|
||||||
|
|
@ -796,12 +810,13 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
|
||||||
TelegramCore_InstantPageBlock_List.addVectorOf(items: itemsOffset, &builder)
|
TelegramCore_InstantPageBlock_List.addVectorOf(items: itemsOffset, &builder)
|
||||||
TelegramCore_InstantPageBlock_List.add(ordered: ordered, &builder)
|
TelegramCore_InstantPageBlock_List.add(ordered: ordered, &builder)
|
||||||
offset = TelegramCore_InstantPageBlock_List.endInstantPageBlock_List(&builder, start: start)
|
offset = TelegramCore_InstantPageBlock_List.endInstantPageBlock_List(&builder, start: start)
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
valueType = .instantpageblockBlockquote
|
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 captionOffset = caption.encodeToFlatBuffers(builder: &builder)
|
||||||
let start = TelegramCore_InstantPageBlock_BlockQuote.startInstantPageBlock_BlockQuote(&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)
|
TelegramCore_InstantPageBlock_BlockQuote.add(caption: captionOffset, &builder)
|
||||||
offset = TelegramCore_InstantPageBlock_BlockQuote.endInstantPageBlock_BlockQuote(&builder, start: start)
|
offset = TelegramCore_InstantPageBlock_BlockQuote.endInstantPageBlock_BlockQuote(&builder, start: start)
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
|
|
|
||||||
|
|
@ -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: [])
|
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):
|
case let .text(text, entities, richText, disableUrlPreview, previewParameters, replyMarkup):
|
||||||
if let entities = entities {
|
if let richText {
|
||||||
attributes.append(entities)
|
attributes.append(richText)
|
||||||
|
} else {
|
||||||
|
if let entities = entities {
|
||||||
|
attributes.append(entities)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let replyMarkup = replyMarkup {
|
if let replyMarkup = replyMarkup {
|
||||||
attributes.append(replyMarkup)
|
attributes.append(replyMarkup)
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,9 @@ extension InstantPageBlock {
|
||||||
result.append(item.previewText())
|
result.append(item.previewText())
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
case let .blockQuote(text, caption):
|
case let .blockQuote(blocks, caption):
|
||||||
return text.previewText() + caption.previewText()
|
let body = blocks.map { $0.previewText() }.joined(separator: " ")
|
||||||
|
return body + caption.previewText()
|
||||||
case let .pullQuote(text, caption):
|
case let .pullQuote(text, caption):
|
||||||
return text.previewText() + caption.previewText()
|
return text.previewText() + caption.previewText()
|
||||||
case .image(_, _, _, _):
|
case .image(_, _, _, _):
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,9 @@ public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedStr
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
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)
|
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
|
||||||
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
||||||
|
|
|
||||||
|
|
@ -576,7 +576,15 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
||||||
isReplyThread = true
|
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))
|
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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -451,11 +451,18 @@ public final class PeerChannelMemberCategoriesContextsManager {
|
||||||
return engine.peers.joinChannel(peerId: peerId, hash: hash)
|
return engine.peers.joinChannel(peerId: peerId, hash: hash)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> beforeNext { [weak self] result in
|
|> beforeNext { [weak self] result in
|
||||||
if let strongSelf = self, case let .joined(updated) = result, let updated {
|
if let self {
|
||||||
strongSelf.impl.with { impl in
|
self.impl.with { impl in
|
||||||
for (contextPeerId, context) in impl.contexts {
|
for (contextPeerId, context) in impl.contexts {
|
||||||
if peerId == contextPeerId {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemCo
|
||||||
if lhs.text != rhs.text {
|
if lhs.text != rhs.text {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.entities != rhs.entities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.richText != rhs.richText {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.media.count != rhs.media.count {
|
if lhs.media.count != rhs.media.count {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +41,7 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemCo
|
||||||
|
|
||||||
let text: String
|
let text: String
|
||||||
let entities: TextEntitiesMessageAttribute?
|
let entities: TextEntitiesMessageAttribute?
|
||||||
|
let richText: RichTextMessageAttribute?
|
||||||
let media: [EngineRawMedia]
|
let media: [EngineRawMedia]
|
||||||
let replyMarkup: ReplyMarkupMessageAttribute?
|
let replyMarkup: ReplyMarkupMessageAttribute?
|
||||||
let botAddress: String
|
let botAddress: String
|
||||||
|
|
@ -207,6 +214,9 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
|
||||||
if let entities = messageItem.entities {
|
if let entities = messageItem.entities {
|
||||||
attributes.append(entities)
|
attributes.append(entities)
|
||||||
}
|
}
|
||||||
|
if let richText = messageItem.richText {
|
||||||
|
attributes.append(richText)
|
||||||
|
}
|
||||||
if let replyMarkup = messageItem.replyMarkup {
|
if let replyMarkup = messageItem.replyMarkup {
|
||||||
attributes.append(replyMarkup)
|
attributes.append(replyMarkup)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ private final class SheetContent: CombinedComponent {
|
||||||
|
|
||||||
var text: String = ""
|
var text: String = ""
|
||||||
var entities: TextEntitiesMessageAttribute?
|
var entities: TextEntitiesMessageAttribute?
|
||||||
|
var richText: RichTextMessageAttribute?
|
||||||
var media: [EngineRawMedia] = []
|
var media: [EngineRawMedia] = []
|
||||||
var replyMarkup: ReplyMarkupMessageAttribute?
|
var replyMarkup: ReplyMarkupMessageAttribute?
|
||||||
|
|
||||||
|
|
@ -152,8 +153,9 @@ private final class SheetContent: CombinedComponent {
|
||||||
media = [image]
|
media = [image]
|
||||||
}
|
}
|
||||||
replyMarkup = replyMarkupValue
|
replyMarkup = replyMarkupValue
|
||||||
case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue):
|
case let .text(textValue, entitiesValue, richTextValue, disableUrlPreview, previewParameters, replyMarkupValue):
|
||||||
text = textValue
|
text = textValue
|
||||||
|
richText = richTextValue
|
||||||
entities = entitiesValue
|
entities = entitiesValue
|
||||||
let _ = disableUrlPreview
|
let _ = disableUrlPreview
|
||||||
let _ = previewParameters
|
let _ = previewParameters
|
||||||
|
|
@ -181,9 +183,10 @@ private final class SheetContent: CombinedComponent {
|
||||||
media = [content]
|
media = [content]
|
||||||
}
|
}
|
||||||
replyMarkup = replyMarkupValue
|
replyMarkup = replyMarkupValue
|
||||||
case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue):
|
case let .text(textValue, entitiesValue, richTextValue, disableUrlPreview, previewParameters, replyMarkupValue):
|
||||||
text = textValue
|
text = textValue
|
||||||
entities = entitiesValue
|
entities = entitiesValue
|
||||||
|
richText = richTextValue
|
||||||
let _ = disableUrlPreview
|
let _ = disableUrlPreview
|
||||||
let _ = previewParameters
|
let _ = previewParameters
|
||||||
replyMarkup = replyMarkupValue
|
replyMarkup = replyMarkupValue
|
||||||
|
|
@ -206,6 +209,7 @@ private final class SheetContent: CombinedComponent {
|
||||||
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
|
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
|
||||||
text: text,
|
text: text,
|
||||||
entities: entities,
|
entities: entities,
|
||||||
|
richText: richText,
|
||||||
media: media,
|
media: media,
|
||||||
replyMarkup: replyMarkup,
|
replyMarkup: replyMarkup,
|
||||||
botAddress: component.botAddress
|
botAddress: component.botAddress
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue