mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Rich bubble: scrollToAnchor + getAnchorRect
Adds a base getAnchorRect on ChatMessageBubbleContentNode, the rich-bubble override (including titleHeight in details recursion), bubble-item forwarding to content nodes, ChatControllerInteraction.scrollToMessageIdWithAnchor, and a scrollToAnchor that lands the anchor at the top of the content area / its line. Threads anchor/scroll params through ChatController and related call sites.
This commit is contained in:
parent
f29af03cd7
commit
fdb2f369ec
26 changed files with 1148 additions and 100 deletions
|
|
@ -0,0 +1,504 @@
|
|||
# Rich-Data Bubble scrollToAnchor Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `ChatMessageRichDataBubbleContentNode.scrollToAnchor` actually scroll the chat history so that an in-page anchor's line lands at the top of the visible content area.
|
||||
|
||||
**Architecture:** Mirror the existing `getQuoteRect` mechanism. The rich-data bubble exposes `getAnchorRect(anchor:)`, the bubble item node forwards to it. A new `ChatControllerInteraction.scrollToMessageIdWithAnchor` closure walks visible items via `forEachVisibleItemNode` (the bubble is necessarily at least partially visible because the user tapped a link in it), reads the anchor's item-local y, and routes through `ChatHistoryListNode.scrollToMessage(... scrollPosition: .bottom(anchorY))`. `.bottom` places the item so the anchor lands at the visual top of the content area; it works uniformly for short and tall items, where `.center(.custom)` is bypassed for items that fit in the content area.
|
||||
|
||||
**Tech Stack:** Swift, Bazel build, AsyncDisplayKit. No unit tests in this project — verification is a full Bazel build.
|
||||
|
||||
**Spec:** [docs/superpowers/specs/2026-05-01-rich-data-bubble-scroll-to-anchor-design.md](../specs/2026-05-01-rich-data-bubble-scroll-to-anchor-design.md)
|
||||
|
||||
**Build command (run after each task):**
|
||||
|
||||
```sh
|
||||
source ~/.zshrc 2>/dev/null; \
|
||||
python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build \
|
||||
--configurationPath build-system/appstore-configuration.json \
|
||||
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
|
||||
--gitCodesigningType development --gitCodesigningUseCurrent \
|
||||
--buildNumber=1 --configuration=debug_sim_arm64
|
||||
```
|
||||
|
||||
After Tasks 1–3 the build must be green; the feature is not yet live (no callers use the new methods). After Task 4 the new closure exists and is implemented but the bubble still routes through the old stub. After Task 5 the feature is live.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add base `getAnchorRect` to `ChatMessageBubbleContentNode`
|
||||
|
||||
This makes `getAnchorRect(anchor:)` callable on every content node (returns `nil` by default) so the iteration in `ChatMessageBubbleItemNode` doesn't need a type-test.
|
||||
|
||||
**Files:**
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift`
|
||||
|
||||
- [ ] **Step 1: Add the base method**
|
||||
|
||||
Open the file. Find the existing `open func transitionNode(messageId:media:adjustRect:) -> (...)` definition (around line 261). Add a new method directly after its closing brace:
|
||||
|
||||
```swift
|
||||
open func getAnchorRect(anchor: String) -> CGRect? {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
The result is that the file should contain, contiguously:
|
||||
|
||||
```swift
|
||||
open func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func getAnchorRect(anchor: String) -> CGRect? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func updateHiddenMedia(_ media: [Media]?) -> Bool {
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
Run the build command at the top of this plan.
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```sh
|
||||
git add submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
ChatMessageBubbleContentNode: add base getAnchorRect
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Override `getAnchorRect` in `ChatMessageRichDataBubbleContentNode`
|
||||
|
||||
Walks the cached instant-page layout and returns the rect of an anchor (in the bubble content node's coordinate space) without triggering any side-effects.
|
||||
|
||||
**Files:**
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift`
|
||||
|
||||
- [ ] **Step 1: Add the override and recursive helper**
|
||||
|
||||
Open the file. Find the existing `private func splitAnchor(_ url: String)` (around line 774). Insert two new methods directly above it (so the new public override comes before the private string helpers but after `findInstantPageMedia`):
|
||||
|
||||
```swift
|
||||
override public func getAnchorRect(anchor: String) -> CGRect? {
|
||||
guard let layout = self.currentPageLayout?.layout else {
|
||||
return nil
|
||||
}
|
||||
if let rect = self.anchorRect(in: layout.items, anchor: anchor, baseY: 0.0) {
|
||||
// Translate from layout/containerNode coords to bubble-content-node coords.
|
||||
// containerNode is offset by (1, 1) from the bubble content node.
|
||||
return rect.offsetBy(dx: 1.0, dy: 1.0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func anchorRect(in items: [InstantPageItem], anchor: String, baseY: CGFloat) -> CGRect? {
|
||||
for item in items {
|
||||
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
|
||||
return CGRect(x: item.frame.minX, y: baseY + item.frame.minY, width: 1.0, height: 1.0)
|
||||
} else if let item = item as? InstantPageTextItem {
|
||||
if let (lineIndex, _) = item.anchors[anchor] {
|
||||
let lineFrame = item.lines[lineIndex].frame
|
||||
return CGRect(x: item.frame.minX + lineFrame.minX, y: baseY + item.frame.minY + lineFrame.minY, width: lineFrame.width, height: lineFrame.height)
|
||||
}
|
||||
} else if let item = item as? InstantPageTableItem {
|
||||
if let (offset, _) = item.anchors[anchor] {
|
||||
return CGRect(x: item.frame.minX, y: baseY + item.frame.minY + offset, width: item.frame.width, height: 1.0)
|
||||
}
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
// Inner items are laid out below the title bar, so the recursive base
|
||||
// must include titleHeight (mirrors InstantPageDetailsNode.linkSelectionRects).
|
||||
if let rect = self.anchorRect(in: item.items, anchor: anchor, baseY: baseY + item.frame.minY + item.titleHeight) {
|
||||
return rect
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Note: the existing file already imports `InstantPageUI`, so `InstantPageItem`, `InstantPageAnchorItem`, `InstantPageTextItem`, `InstantPageTableItem`, and `InstantPageDetailsItem` resolve. `InstantPageTextItem.anchors` is typed `[String: (Int, Bool)]`, `InstantPageTableItem.anchors` is `[String: (CGFloat, Bool)]` — destructure accordingly.
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
Run the build command.
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```sh
|
||||
git add submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
Rich bubble: add getAnchorRect override
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Forward `getAnchorRect` from `ChatMessageBubbleItemNode`
|
||||
|
||||
Iterates content nodes and converts the rect to the bubble item node's coordinate space. Mirrors the existing `getQuoteRect` shape exactly.
|
||||
|
||||
**Files:**
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift`
|
||||
|
||||
- [ ] **Step 1: Add the public forwarder**
|
||||
|
||||
Open the file. Find the existing `public func getQuoteRect(quote: String, offset: Int?) -> CGRect?` (around line 7237). Insert a new method directly after its closing brace, before `public func getInnerReplySubjectRect(...)`:
|
||||
|
||||
```swift
|
||||
public func getAnchorRect(anchor: String) -> CGRect? {
|
||||
for contentNode in self.contentNodes {
|
||||
if let result = contentNode.getAnchorRect(anchor: anchor) {
|
||||
return contentNode.view.convert(result, to: self.view)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
Run the build command.
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```sh
|
||||
git add submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
Bubble item: forward getAnchorRect to content nodes
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add `scrollToMessageIdWithAnchor` closure (declaration + 7 sites)
|
||||
|
||||
Adds the new `(MessageIndex, String) -> Void` closure to `ChatControllerInteraction`, the real implementation in `ChatController.swift`, and no-op stubs at the six other call sites. After this task the build is green and the closure works end-to-end on the chat-controller side; the rich-data bubble still routes through the old stub so the feature is not yet live.
|
||||
|
||||
**Files (8 edits):**
|
||||
- Modify: `submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift` (3 edits: field, init param, assignment)
|
||||
- Modify: `submodules/TelegramUI/Sources/ChatController.swift` (real implementation)
|
||||
- Modify: `submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift` (no-op stub)
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` (no-op stub)
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift` (no-op stub)
|
||||
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` (no-op stub)
|
||||
- Modify: `submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift` (no-op stub)
|
||||
- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift` (no-op stub)
|
||||
|
||||
- [ ] **Step 1: Add the field on `ChatControllerInteraction`**
|
||||
|
||||
In `submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift`, find:
|
||||
|
||||
```swift
|
||||
public let scrollToMessageId: (MessageIndex, CGFloat) -> Void
|
||||
```
|
||||
|
||||
(around line 311). Insert directly after it:
|
||||
|
||||
```swift
|
||||
public let scrollToMessageIdWithAnchor: (MessageIndex, String) -> Void
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the init parameter**
|
||||
|
||||
In the same file, find the init parameter list (around line 490):
|
||||
|
||||
```swift
|
||||
scrollToMessageId: @escaping (MessageIndex, CGFloat) -> Void,
|
||||
```
|
||||
|
||||
Insert directly after it:
|
||||
|
||||
```swift
|
||||
scrollToMessageIdWithAnchor: @escaping (MessageIndex, String) -> Void,
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the init assignment**
|
||||
|
||||
In the same file, find (around line 622):
|
||||
|
||||
```swift
|
||||
self.scrollToMessageId = scrollToMessageId
|
||||
```
|
||||
|
||||
Insert directly after it:
|
||||
|
||||
```swift
|
||||
self.scrollToMessageIdWithAnchor = scrollToMessageIdWithAnchor
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add real implementation in `ChatController.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Sources/ChatController.swift`, find (around line 5397):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { [weak self] index, offset in
|
||||
self?.chatDisplayNode.historyNode.scrollToMessage(index: index, offset: offset)
|
||||
}, navigateToStory: { [weak self] message, storyId in
|
||||
```
|
||||
|
||||
Insert a new closure between `scrollToMessageId` and `navigateToStory`:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { [weak self] index, offset in
|
||||
self?.chatDisplayNode.historyNode.scrollToMessage(index: index, offset: offset)
|
||||
}, scrollToMessageIdWithAnchor: { [weak self] index, anchor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var anchorY: CGFloat?
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
guard anchorY == nil else {
|
||||
return
|
||||
}
|
||||
if let itemNode = itemNode as? ChatMessageBubbleItemNode,
|
||||
itemNode.item?.message.id == index.id,
|
||||
let rect = itemNode.getAnchorRect(anchor: anchor) {
|
||||
anchorY = rect.minY
|
||||
}
|
||||
}
|
||||
if let anchorY {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(
|
||||
from: index, to: index,
|
||||
animated: true, highlight: false,
|
||||
scrollPosition: .bottom(anchorY)
|
||||
)
|
||||
} else {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(index: index)
|
||||
}
|
||||
}, navigateToStory: { [weak self] message, storyId in
|
||||
```
|
||||
|
||||
`scrollToMessage(from:to:animated:highlight:scrollPosition:)` is the existing public method on `ChatHistoryListNode` (declared at line 3585 in `ChatHistoryListNode.swift`); `quote`, `subject`, and `setupReply` use their default values. `ChatMessageBubbleItemNode` is already imported at the top of `ChatController.swift`. The `forEachVisibleItemNode` walk is sound because tapping the in-page anchor link requires the bubble to be at least partially visible.
|
||||
|
||||
- [ ] **Step 5: Add no-op stub in `BrowserBookmarksScreen.swift`**
|
||||
|
||||
In `submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift`, find (around line 183):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add no-op stub in `ChatRecentActionsControllerNode.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift`, find (around line 660):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Add no-op stub in `ChatSendAudioMessageContextPreview.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift`, find (around line 507):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Add no-op stub in `PeerInfoScreen.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift`, find (around line 1278):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Add no-op stub in `OverlayAudioPlayerControllerNode.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift`, find (around line 252):
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Add no-op stub in `SharedAccountContext.swift`**
|
||||
|
||||
In `submodules/TelegramUI/Sources/SharedAccountContext.swift`, find (around line 2565):
|
||||
|
||||
```swift
|
||||
scrollToMessageId: { _, _ in
|
||||
```
|
||||
|
||||
(Note: this site has no leading `}, ` because it is the first argument on its line — verify with `grep -n "scrollToMessageId:" submodules/TelegramUI/Sources/SharedAccountContext.swift`.)
|
||||
|
||||
Insert a new closure directly after the closing `}` of the `scrollToMessageId` stub. If the original looks like:
|
||||
|
||||
```swift
|
||||
scrollToMessageId: { _, _ in
|
||||
},
|
||||
navigateToStory: { _, _ in
|
||||
```
|
||||
|
||||
it should become:
|
||||
|
||||
```swift
|
||||
scrollToMessageId: { _, _ in
|
||||
},
|
||||
scrollToMessageIdWithAnchor: { _, _ in
|
||||
},
|
||||
navigateToStory: { _, _ in
|
||||
```
|
||||
|
||||
Match the surrounding indentation and trailing-comma style of the file.
|
||||
|
||||
- [ ] **Step 11: Build**
|
||||
|
||||
Run the build command.
|
||||
Expected: build succeeds. Any compile error in this task means a stub site was missed or the closure type was mismatched — search for `scrollToMessageId:` again and confirm every site has a corresponding `scrollToMessageIdWithAnchor:`.
|
||||
|
||||
- [ ] **Step 12: Commit**
|
||||
|
||||
```sh
|
||||
git add \
|
||||
submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift \
|
||||
submodules/TelegramUI/Sources/ChatController.swift \
|
||||
submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift \
|
||||
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift \
|
||||
submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift \
|
||||
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift \
|
||||
submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift \
|
||||
submodules/TelegramUI/Sources/SharedAccountContext.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
ChatControllerInteraction: add scrollToMessageIdWithAnchor closure
|
||||
|
||||
Routes through ChatHistoryListNode.scrollToMessage with a custom
|
||||
.center(.custom) callback that asks the bubble item for the anchor
|
||||
rect's midY. Six existing no-op interaction sites get matching
|
||||
no-op stubs.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire up `scrollToAnchor` in the rich-data bubble
|
||||
|
||||
Replace the stub body so that taps on in-page anchor links actually scroll. After this task the feature is live end-to-end.
|
||||
|
||||
**Files:**
|
||||
- Modify: `submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift`
|
||||
|
||||
- [ ] **Step 1: Replace `scrollToAnchor` body**
|
||||
|
||||
In `ChatMessageRichDataBubbleContentNode.swift`, find (around line 796):
|
||||
|
||||
```swift
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
// 0.0 is offset
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index, 0.0)
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
if anchor.isEmpty {
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index, 0.0)
|
||||
} else {
|
||||
item.controllerInteraction.scrollToMessageIdWithAnchor(item.message.index, anchor)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The empty-anchor branch keeps the existing "scroll to message top" behavior for `#` URLs with no fragment.
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
Run the build command.
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Manual smoke test**
|
||||
|
||||
This project has no unit tests. Smoke-test in the simulator:
|
||||
|
||||
1. Launch the app on the iOS simulator.
|
||||
2. Open a chat that contains a webpage message rendered as a rich-data bubble. Good source: a Wikipedia article URL whose Telegram instant-page render contains in-page section/footnote links (e.g., the "Contents" section or the `[1]`-style citation links).
|
||||
3. Tap a section/footnote link inside the bubble.
|
||||
4. Expected: the chat scrolls so that the target line of the bubble is centered in the visible area. If the bubble is partially off-screen, the chat scrolls to bring the line into view.
|
||||
5. Tap a `#`-only link (no fragment) if you can find one. Expected: chat scrolls to the message top (existing pre-task behavior preserved).
|
||||
|
||||
If the scroll doesn't land where expected, double-check the coord conversions in Task 2 (`+1, +1` for `containerNode` inset) and Task 3 (`contentNode.view.convert(rect, to: self.view)`).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```sh
|
||||
git add submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
Rich bubble: scrollToAnchor scrolls to anchor's line
|
||||
|
||||
Empty anchor keeps the previous scroll-to-message-top behavior.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Done
|
||||
|
||||
All five tasks complete leaves:
|
||||
- Each commit independently builds.
|
||||
- The feature is live: tapping an in-page anchor link inside a rich-data bubble scrolls the chat to center the target line.
|
||||
- Reference popups and details expansion are deferred (per spec).
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
# ChatMessageRichDataBubbleContentNode.scrollToAnchor
|
||||
|
||||
## Background
|
||||
|
||||
`ChatMessageRichDataBubbleContentNode` renders a webpage's `instantPage` inline inside a chat message bubble (the same layout/tile machinery as `InstantPageControllerNode`, but embedded as a content node of `ChatMessageBubbleItemNode`).
|
||||
|
||||
The bubble already detects in-page anchor links (URL with a `#fragment`) when its base URL matches the current loaded webpage and routes them to a private `scrollToAnchor(_ anchor: String)`. That method is a stub today:
|
||||
|
||||
```swift
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
guard let item = self.item else { return }
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index, 0.0)
|
||||
}
|
||||
```
|
||||
|
||||
`ChatHistoryListNode.scrollToMessage(index:offset:)` ignores the offset, so the anchor name is dropped and the bubble simply scrolls to the message top. Tapping a footnote / section link inside a long instant-page bubble does nothing useful when the target is below the fold.
|
||||
|
||||
## Goal
|
||||
|
||||
When an in-page anchor inside a rich-data bubble is tapped, scroll the chat history so the anchor's line lands at the top of the visible content area.
|
||||
|
||||
## Non-goals (explicitly deferred)
|
||||
|
||||
- **Reference popup**: `InstantPageControllerNode.scrollToAnchor` shows `InstantPageReferenceController` as an overlay when the anchor is a footnote-style "reference" (text item, non-empty anchor text). We will simply scroll to the line containing the reference instead. No popup.
|
||||
- **Collapsed details expansion**: The bubble already no-ops `updateDetailsExpanded`, so the runtime never toggles `InstantPageDetailsItem` state. We compute the rect for anchors inside details items as if they were expanded; no expansion side-effect is performed. Worst case for a layout-collapsed details anchor is a slightly-off scroll target — acceptable for v1.
|
||||
|
||||
## Approach
|
||||
|
||||
Add a `getAnchorRect(anchor:)` resolver on the bubble (mirrors `getQuoteRect`'s shape: base no-op, rich-data override walks the layout, bubble item forwards to content nodes). The chat controller then uses `forEachVisibleItemNode` to find the bubble being scrolled to (it is by definition partially visible — the user tapped a link in it), reads the anchor's item-local y, and dispatches `historyNode.scrollToMessage(... scrollPosition: .bottom(anchorY))`. `.bottom(additionalOffset)` places the item so its frame.maxY lands at `(visibleSize.height - insets.bottom) + additionalOffset`; with `additionalOffset = anchorY` (item-local-y of the anchor's top edge), the anchor renders at the visual top of the chat's content area regardless of whether the item is short or tall. (`.center(.custom)` was the original pick but is bypassed for items that fit in the content area, and the rotation maps "list-coord low" to "visual bottom" in chat lists, so `.bottom` is the more uniform primitive here.)
|
||||
|
||||
### Components
|
||||
|
||||
#### 1. `ChatMessageBubbleContentNode.getAnchorRect(anchor:)` — base, default `nil`
|
||||
|
||||
Add an `open func getAnchorRect(anchor: String) -> CGRect? { return nil }` to the base class so callers don't need to type-test every content node.
|
||||
|
||||
#### 2. `ChatMessageRichDataBubbleContentNode.getAnchorRect(anchor:)` — override
|
||||
|
||||
Walk `self.currentPageLayout?.layout.items`, mirroring the cases in `InstantPageControllerNode.findAnchorItem`:
|
||||
- `InstantPageAnchorItem` with matching `anchor` → return a 1pt rect at the item's `frame.origin`.
|
||||
- `InstantPageTextItem`, `item.anchors[anchor] == (lineIndex, _)` → return the rect of `item.lines[lineIndex].frame`, offset by `item.frame.origin`.
|
||||
- `InstantPageTableItem`, `item.anchors[anchor] == (offset, _)` → return a 1pt-tall row-width rect at `item.frame.origin + (0, offset)`.
|
||||
- `InstantPageDetailsItem` → recurse into `item.items` with `baseY` increased by `item.frame.minY + item.titleHeight` (inner items live below the title bar; mirrors `InstantPageDetailsNode.linkSelectionRects`). Per non-goal #2, no expand side-effect.
|
||||
|
||||
The walk returns coordinates in *layout space* (= `containerNode`-local). The bubble's `containerNode` is offset `(1, 1)` from the bubble content node, so add `(1, 1)` before returning. The returned rect is in `ChatMessageRichDataBubbleContentNode`'s own coordinate space (its `view`).
|
||||
|
||||
If no anchor matches anywhere in the tree, return `nil`.
|
||||
|
||||
#### 3. `ChatMessageBubbleItemNode.getAnchorRect(anchor:)` — public
|
||||
|
||||
Add next to the existing `getQuoteRect(quote:offset:)`. Iterate `self.contentNodes`; for each, call `contentNode.getAnchorRect(anchor:)` and, if non-nil, return `contentNode.view.convert(rect, to: self.view)`. Return `nil` if no content node knows the anchor.
|
||||
|
||||
#### 4. `ChatControllerInteraction.scrollToMessageIdWithAnchor` — new closure
|
||||
|
||||
Add a new public closure on `ChatControllerInteraction`:
|
||||
|
||||
```swift
|
||||
public let scrollToMessageIdWithAnchor: (MessageIndex, String) -> Void
|
||||
```
|
||||
|
||||
Wire through the initializer (parameter, assignment) alongside the existing `scrollToMessageId`. The existing `scrollToMessageId(MessageIndex, CGFloat)` closure stays untouched — its 7 callers (incl. 6 no-op stubs) need no signature change.
|
||||
|
||||
Add no-op stubs `scrollToMessageIdWithAnchor: { _, _ in }` at the six existing no-op sites:
|
||||
- `BrowserUI/Sources/BrowserBookmarksScreen.swift`
|
||||
- `Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift`
|
||||
- `Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift`
|
||||
- `Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift`
|
||||
- `TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift`
|
||||
- `TelegramUI/Sources/SharedAccountContext.swift`
|
||||
|
||||
#### 5. Real implementation in `ChatController.swift`
|
||||
|
||||
Next to the existing `scrollToMessageId:` argument in the `ChatControllerInteraction(...)` construction, add:
|
||||
|
||||
```swift
|
||||
scrollToMessageIdWithAnchor: { [weak self] index, anchor in
|
||||
guard let self else { return }
|
||||
var anchorY: CGFloat?
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
guard anchorY == nil else { return }
|
||||
if let itemNode = itemNode as? ChatMessageBubbleItemNode,
|
||||
itemNode.item?.message.id == index.id,
|
||||
let rect = itemNode.getAnchorRect(anchor: anchor) {
|
||||
anchorY = rect.minY
|
||||
}
|
||||
}
|
||||
if let anchorY {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(
|
||||
from: index, to: index,
|
||||
animated: true, highlight: false,
|
||||
scrollPosition: .bottom(anchorY)
|
||||
)
|
||||
} else {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(index: index)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ChatHistoryListNode.scrollToMessage(from:to:animated:highlight:quote:subject:scrollPosition:setupReply:)` already accepts `scrollPosition` and routes it through `MessageHistoryScrollToSubject` → `ListViewScrollToItem.position`. The `.bottom(additionalOffset)` formula sets `frame.maxY' = (visibleSize.height - insets.bottom) + additionalOffset`; with `additionalOffset = anchorY` (the anchor's item-local y in pre-transform coords), the chat list — rotated 180° at the layer — renders the anchor at the visual top of the content area. The `forEachVisibleItemNode` walk is safe because tapping the in-page anchor link requires the bubble to be at least partially visible.
|
||||
|
||||
#### 6. Replace the `scrollToAnchor` stub
|
||||
|
||||
In `ChatMessageRichDataBubbleContentNode.swift`:
|
||||
|
||||
```swift
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
guard let item = self.item else { return }
|
||||
if anchor.isEmpty {
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index, 0.0)
|
||||
} else {
|
||||
item.controllerInteraction.scrollToMessageIdWithAnchor(item.message.index, anchor)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Empty anchor (the `#` with no fragment case) keeps the existing "scroll to message top" behavior.
|
||||
|
||||
## Files touched
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift` | Add `open func getAnchorRect(anchor:) -> CGRect?` returning `nil`. |
|
||||
| `submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift` | Override `getAnchorRect`; rewrite `scrollToAnchor` body. |
|
||||
| `submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift` | Add public `getAnchorRect(anchor:)`. |
|
||||
| `submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift` | New `scrollToMessageIdWithAnchor` field + init param + assignment. |
|
||||
| `submodules/TelegramUI/Sources/ChatController.swift` | Real implementation of the closure. |
|
||||
| 6 no-op stub sites | Add `scrollToMessageIdWithAnchor: { _, _ in }` next to existing stub. |
|
||||
|
||||
## What is *not* changed
|
||||
|
||||
- No new types in `Display/`, `AccountContext/`, or `TelegramCore/`.
|
||||
- No changes to `MessageHistoryScrollToSubject` or `ChatHistoryLocation`.
|
||||
- No changes to `InstantPageUI/` (the layout-walking logic is replicated in the rich-data bubble file rather than exported, since it's both small and specialized for the embedded layout).
|
||||
- No changes to the existing `scrollToMessageId(_, CGFloat)` closure or its 7 call sites' signatures.
|
||||
|
||||
## Verification
|
||||
|
||||
There are no unit tests in this project. Verification is a full Bazel build:
|
||||
|
||||
```sh
|
||||
source ~/.zshrc 2>/dev/null; \
|
||||
python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build \
|
||||
--configurationPath build-system/appstore-configuration.json \
|
||||
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
|
||||
--gitCodesigningType development --gitCodesigningUseCurrent \
|
||||
--buildNumber=1 --configuration=debug_sim_arm64
|
||||
```
|
||||
|
||||
Manual smoke test in the simulator: open a chat that contains a webpage message rendered as a rich-data bubble with an instant page that has internal anchors (e.g., a Wikipedia article with section links or footnote references). Tap a section link or footnote link; the chat should scroll so that the target line lands at the top of the visible content area.
|
||||
|
|
@ -83,6 +83,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
controller.openUrl(url.url)
|
||||
controller.dismiss()
|
||||
}
|
||||
}, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
|
|
@ -179,7 +180,8 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
}, requestMessageUpdate: { _, _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
self.settings = InstantPagePresentationSettings(
|
||||
themeType: self.presentationData.theme.overallDarkAppearance ? .dark : .light,
|
||||
fontSize: fontSize,
|
||||
lineSpacingFactor: 1.0,
|
||||
forceSerif: state.isSerif,
|
||||
autoNightMode: false,
|
||||
ignoreAutoNightModeUntil: 0
|
||||
|
|
@ -498,7 +499,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
return
|
||||
}
|
||||
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights, cachedMessageSyntaxHighlight: self.codeHighlight)
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, sideInset: 17.0, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights, cachedMessageSyntaxHighlight: self.codeHighlight)
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
|
||||
|
||||
|
|
|
|||
|
|
@ -2687,6 +2687,9 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
return false
|
||||
}
|
||||
let pinToEdgeTopInset = self.calculatePinToEdgeTopInset()
|
||||
if pinToEdgeTopInset <= 0.0 {
|
||||
return false
|
||||
}
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == targetIndex {
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
updateLayout = true
|
||||
animated = true
|
||||
}
|
||||
if previousSettings.fontSize != settings.fontSize || previousSettings.forceSerif != settings.forceSerif {
|
||||
if previousSettings.fontSize != settings.fontSize || previousSettings.lineSpacingFactor != settings.lineSpacingFactor || previousSettings.forceSerif != settings.forceSerif {
|
||||
animated = false
|
||||
updateLayout = true
|
||||
}
|
||||
|
|
@ -475,7 +475,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, sideInset: 17.0, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
|
||||
|
||||
|
|
|
|||
|
|
@ -165,8 +165,7 @@ private func instantPageFirstTextLineMidY(in items: [InstantPageItem]) -> CGFloa
|
|||
return nil
|
||||
}
|
||||
|
||||
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool) -> InstantPageLayout {
|
||||
|
||||
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool, isLast: Bool) -> InstantPageLayout {
|
||||
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
|
||||
var items: [InstantPageItem] = []
|
||||
var offset = contentSize.height
|
||||
|
|
@ -218,7 +217,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
|
||||
switch block {
|
||||
case let .cover(block):
|
||||
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
|
||||
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: true)
|
||||
case let .title(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
|
||||
|
|
@ -355,9 +354,13 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case .divider:
|
||||
let lineWidth = floor(boundingWidth / 2.0)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: theme.textCategories.caption.color)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem])
|
||||
if isLast {
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
} else {
|
||||
let lineWidth = floor(boundingWidth / 2.0)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: theme.textCategories.caption.color)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem])
|
||||
}
|
||||
case let .list(contentItems, ordered):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var maxIndexWidth: CGFloat = 0.0
|
||||
|
|
@ -462,8 +465,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var previousBlock: InstantPageBlock?
|
||||
var originY: CGFloat = contentSize.height
|
||||
var firstBlockLineMidY: CGFloat?
|
||||
for subBlock in blocks {
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
|
||||
let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing))
|
||||
|
|
@ -680,7 +684,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var i = 0
|
||||
for subItem in innerItems {
|
||||
let frame = mosaicLayout[i].0
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true)
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true, isLast: false)
|
||||
items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin))
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -753,8 +757,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
|
||||
var previousBlock: InstantPageBlock?
|
||||
for subBlock in blocks {
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing))
|
||||
|
|
@ -936,8 +941,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var subDetailsIndex = 0
|
||||
|
||||
var previousBlock: InstantPageBlock?
|
||||
for subBlock in blocks {
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
|
||||
|
|
@ -1007,10 +1013,12 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
contentSize.height += item.frame.height
|
||||
items.append(item)
|
||||
|
||||
let inset: CGFloat = i == articles.count - 1 ? 0.0 : 17.0
|
||||
let lineSize = CGSize(width: boundingWidth - inset, height: UIScreenPixel)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: rtl || item.rtl ? 0.0 : inset, y: contentSize.height - lineSize.height), size: lineSize), shapeFrame: CGRect(origin: CGPoint(), size: lineSize), shape: .rect, color: theme.controlColor)
|
||||
items.append(shapeItem)
|
||||
if !isLast {
|
||||
let inset: CGFloat = i == articles.count - 1 ? 0.0 : 17.0
|
||||
let lineSize = CGSize(width: boundingWidth - inset, height: UIScreenPixel)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: rtl || item.rtl ? 0.0 : inset, y: contentSize.height - lineSize.height), size: lineSize), shapeFrame: CGRect(origin: CGPoint(), size: lineSize), shape: .rect, color: theme.controlColor)
|
||||
items.append(shapeItem)
|
||||
}
|
||||
}
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .map(latitude, longitude, zoom, dimensions, caption):
|
||||
|
|
@ -1046,7 +1054,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
}
|
||||
|
||||
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true) -> InstantPageLayout {
|
||||
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, sideInset: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true) -> InstantPageLayout {
|
||||
var maybeLoadedContent: TelegramMediaWebpageLoadedContent?
|
||||
if case let .Loaded(content) = webPage.content {
|
||||
maybeLoadedContent = content
|
||||
|
|
@ -1074,8 +1082,9 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
|
|||
var detailsIndexCounter: Int = 0
|
||||
|
||||
var previousBlock: InstantPageBlock?
|
||||
for block in pageBlocks {
|
||||
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
|
||||
for i in 0 ..< pageBlocks.count {
|
||||
let block = pageBlocks[i]
|
||||
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: sideInset + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == pageBlocks.count - 1)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block)
|
||||
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
|
||||
items.append(contentsOf: blockItems)
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ public final class InstantPageTextItem: InstantPageItem {
|
|||
context.setFillColor(color.cgColor)
|
||||
}
|
||||
let itemFrame = item.frame.offsetBy(dx: lineFrame.minX, dy: 0.0)
|
||||
context.fill(CGRect(x: itemFrame.minX, y: itemFrame.minY + lineFrame.size.height + 1.0, width: itemFrame.size.width, height: 1.0))
|
||||
context.fill(CGRect(x: itemFrame.minX, y: itemFrame.minY + lineFrame.size.height + 2.0, width: itemFrame.size.width, height: 1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@ final class InstantPageTextStyleStack {
|
|||
|
||||
if let link = link, let linkColor = linkColor {
|
||||
attributes[NSAttributedString.Key.foregroundColor] = linkColor
|
||||
if linkColor == color {
|
||||
attributes[NSAttributedString.Key.underlineStyle] = NSUnderlineStyle.single.rawValue as NSNumber
|
||||
}
|
||||
if link, let linkMarkerColor = linkMarkerColor {
|
||||
attributes[NSAttributedString.Key(rawValue: InstantPageMarkerColorAttribute)] = linkMarkerColor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,23 +4,29 @@ import Display
|
|||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
|
||||
enum InstantPageFontStyle {
|
||||
public enum InstantPageFontStyle {
|
||||
case sans
|
||||
case serif
|
||||
}
|
||||
|
||||
struct InstantPageFont {
|
||||
public struct InstantPageFont {
|
||||
let style: InstantPageFontStyle
|
||||
let size: CGFloat
|
||||
let lineSpacingFactor: CGFloat
|
||||
|
||||
public init(style: InstantPageFontStyle, size: CGFloat, lineSpacingFactor: CGFloat) {
|
||||
self.style = style
|
||||
self.size = size
|
||||
self.lineSpacingFactor = lineSpacingFactor
|
||||
}
|
||||
}
|
||||
|
||||
struct InstantPageTextAttributes {
|
||||
public struct InstantPageTextAttributes {
|
||||
let font: InstantPageFont
|
||||
let color: UIColor
|
||||
let underline: Bool
|
||||
|
||||
init(font: InstantPageFont, color: UIColor, underline: Bool = false) {
|
||||
public init(font: InstantPageFont, color: UIColor, underline: Bool = false) {
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.underline = underline
|
||||
|
|
@ -30,8 +36,8 @@ struct InstantPageTextAttributes {
|
|||
return InstantPageTextAttributes(font: self.font, color: self.color, underline: underline)
|
||||
}
|
||||
|
||||
func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextAttributes {
|
||||
return InstantPageTextAttributes(font: InstantPageFont(style: forceSerif ? .serif : self.font.style, size: floor(self.font.size * sizeMultiplier), lineSpacingFactor: self.font.lineSpacingFactor), color: self.color, underline: self.underline)
|
||||
func withUpdatedFontStyles(sizeMultiplier: CGFloat, lineSpacingFactor: CGFloat, forceSerif: Bool) -> InstantPageTextAttributes {
|
||||
return InstantPageTextAttributes(font: InstantPageFont(style: forceSerif ? .serif : self.font.style, size: floor(self.font.size * sizeMultiplier), lineSpacingFactor: self.font.lineSpacingFactor * lineSpacingFactor), color: self.color, underline: self.underline)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +62,17 @@ public struct InstantPageTextCategories {
|
|||
let table: InstantPageTextAttributes
|
||||
let article: InstantPageTextAttributes
|
||||
|
||||
public init(kicker: InstantPageTextAttributes, header: InstantPageTextAttributes, subheader: InstantPageTextAttributes, paragraph: InstantPageTextAttributes, caption: InstantPageTextAttributes, credit: InstantPageTextAttributes, table: InstantPageTextAttributes, article: InstantPageTextAttributes) {
|
||||
self.kicker = kicker
|
||||
self.header = header
|
||||
self.subheader = subheader
|
||||
self.paragraph = paragraph
|
||||
self.caption = caption
|
||||
self.credit = credit
|
||||
self.table = table
|
||||
self.article = article
|
||||
}
|
||||
|
||||
func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes {
|
||||
switch type {
|
||||
case .kicker:
|
||||
|
|
@ -77,8 +94,17 @@ public struct InstantPageTextCategories {
|
|||
}
|
||||
}
|
||||
|
||||
func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories {
|
||||
return InstantPageTextCategories(kicker: self.kicker.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), credit: self.credit.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif))
|
||||
func withUpdatedFontStyles(sizeMultiplier: CGFloat, lineSpacingFactor: CGFloat, forceSerif: Bool) -> InstantPageTextCategories {
|
||||
return InstantPageTextCategories(
|
||||
kicker: self.kicker.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
credit: self.credit.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif),
|
||||
article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +158,8 @@ public final class InstantPageTheme {
|
|||
self.overlayPanelColor = overlayPanelColor
|
||||
}
|
||||
|
||||
public func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme {
|
||||
return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor)
|
||||
public func withUpdatedFontStyles(sizeMultiplier: CGFloat, lineSpacingFactor: CGFloat, forceSerif: Bool) -> InstantPageTheme {
|
||||
return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, lineSpacingFactor: lineSpacingFactor, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor)
|
||||
}
|
||||
|
||||
func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes {
|
||||
|
|
@ -339,14 +365,14 @@ func instantPageThemeTypeForSettingsAndTime(themeSettings: PresentationThemeSett
|
|||
|
||||
public func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPagePresentationSettings) -> InstantPageTheme {
|
||||
switch type {
|
||||
case .light:
|
||||
return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif)
|
||||
case .sepia:
|
||||
return sepiaTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif)
|
||||
case .gray:
|
||||
return grayTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif)
|
||||
case .dark:
|
||||
return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif)
|
||||
case .light:
|
||||
return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), lineSpacingFactor: settings.lineSpacingFactor, forceSerif: settings.forceSerif)
|
||||
case .sepia:
|
||||
return sepiaTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), lineSpacingFactor: settings.lineSpacingFactor, forceSerif: settings.forceSerif)
|
||||
case .gray:
|
||||
return grayTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), lineSpacingFactor: settings.lineSpacingFactor, forceSerif: settings.forceSerif)
|
||||
case .dark:
|
||||
return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), lineSpacingFactor: settings.lineSpacingFactor, forceSerif: settings.forceSerif)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
|
|||
break
|
||||
case .ignore:
|
||||
return .fail
|
||||
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom:
|
||||
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom, .externalInstantPage:
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ public struct ChatMessageBubbleContentTapAction {
|
|||
case date(Int32, String)
|
||||
case largeEmoji(String, String?, TelegramMediaFile)
|
||||
case customEmoji(TelegramMediaFile)
|
||||
case externalInstantPage(url: Url, webpageId: MediaId, anchor: String?)
|
||||
case custom(() -> Void)
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +261,11 @@ open class ChatMessageBubbleContentNode: ASDisplayNode {
|
|||
open func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
open func getAnchorRect(anchor: String) -> CGRect? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func updateHiddenMedia(_ media: [Media]?) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1347,7 +1347,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
break
|
||||
case .ignore:
|
||||
return .fail
|
||||
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom:
|
||||
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .date, .custom, .externalInstantPage:
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
|
|
@ -5791,6 +5791,27 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, message: item.content.firstMessage, allowInlineWebpageResolution: url.allowInlineWebpageResolution, progress: tapAction.activate?()))
|
||||
}, contextMenuOnLongPress: !tapAction.hasLongTapAction))
|
||||
}
|
||||
case let .externalInstantPage(url, webpageId, anchor):
|
||||
if case .longTap = gesture, !tapAction.hasLongTapAction, let item = self.item {
|
||||
let tapMessage = item.content.firstMessage
|
||||
var subFrame = self.backgroundNode.frame
|
||||
if case .group = item.content {
|
||||
for contentNode in self.contentNodes {
|
||||
if contentNode.item?.message.stableId == tapMessage.stableId {
|
||||
subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: tapMessage, selectAll: false, subFrame: subFrame, disableDefaultPressAnimation: true))
|
||||
} else {
|
||||
return .action(InternalBubbleTapAction.Action({ [weak self] in
|
||||
guard let self, let item = self.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.openExternalInstantPage(ChatControllerInteraction.OpenInstantPage(webpageId: webpageId, url: url.url, anchor: anchor, concealed: true, progress: tapAction.activate?()))
|
||||
}, contextMenuOnLongPress: !tapAction.hasLongTapAction))
|
||||
}
|
||||
case let .phone(number):
|
||||
return .action(InternalBubbleTapAction.Action({ [weak self] in
|
||||
guard let self, let item = self.item, let contentNode = self.contextContentNodeForLink(number, rects: rects) else {
|
||||
|
|
@ -5999,6 +6020,42 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
} else {
|
||||
disableDefaultPressAnimation = true
|
||||
}
|
||||
case let .externalInstantPage(url, webpageId, anchor):
|
||||
if tapAction.hasLongTapAction {
|
||||
return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in
|
||||
guard let self, let item = self.item else {
|
||||
return
|
||||
}
|
||||
let cleanUrl = url.url.replacingOccurrences(of: "mailto:", with: "")
|
||||
guard let contentNode = self.contextContentNodeForLink(cleanUrl, rects: rects) else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture))
|
||||
}, contextMenuOnLongPress: false))
|
||||
} else {
|
||||
disableDefaultPressAnimation = true
|
||||
}
|
||||
|
||||
if case .longTap = gesture, !tapAction.hasLongTapAction, let item = self.item {
|
||||
let tapMessage = item.content.firstMessage
|
||||
var subFrame = self.backgroundNode.frame
|
||||
if case .group = item.content {
|
||||
for contentNode in self.contentNodes {
|
||||
if contentNode.item?.message.stableId == tapMessage.stableId {
|
||||
subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: tapMessage, selectAll: false, subFrame: subFrame, disableDefaultPressAnimation: true))
|
||||
} else {
|
||||
return .action(InternalBubbleTapAction.Action({ [weak self] in
|
||||
guard let self, let item = self.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.openExternalInstantPage(ChatControllerInteraction.OpenInstantPage(webpageId: webpageId, url: url.url, anchor: anchor, concealed: true, progress: tapAction.activate?()))
|
||||
}, contextMenuOnLongPress: !tapAction.hasLongTapAction))
|
||||
}
|
||||
case let .phone(number):
|
||||
if tapAction.hasLongTapAction {
|
||||
return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in
|
||||
|
|
@ -7187,7 +7244,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
public func getAnchorRect(anchor: String) -> CGRect? {
|
||||
for contentNode in self.contentNodes {
|
||||
if let result = contentNode.getAnchorRect(anchor: anchor) {
|
||||
return contentNode.view.convert(result, to: self.view)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getInnerReplySubjectRect(innerSubject: EngineMessageReplyInnerSubject) -> CGRect? {
|
||||
switch innerSubject {
|
||||
case let .todoItem(todoItemId):
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
private var visibleTiles: [Int: InstantPageTileNode] = [:]
|
||||
private var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
||||
private var currentPageLayout: (boundingWidth: CGFloat, layout: InstantPageLayout)?
|
||||
private var pageTheme: InstantPageTheme?
|
||||
private var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
private var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
||||
private var currentExpandedDetails: [Int : Bool]?
|
||||
|
|
@ -56,6 +57,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
}
|
||||
|
||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let previousItem = self.item
|
||||
let currentPageLayout = self.currentPageLayout
|
||||
let previousCurrentLayoutTiles = self.currentLayoutTiles
|
||||
|
||||
|
|
@ -71,20 +73,117 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
var pageLayout: InstantPageLayout?
|
||||
var currentLayoutTiles: [InstantPageTile] = []
|
||||
|
||||
let isDark = item.presentationData.theme.theme.overallDarkAppearance
|
||||
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
let messageTheme = isIncoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
||||
|
||||
var underlineLinks = true
|
||||
if !messageTheme.primaryTextColor.isEqual(messageTheme.linkTextColor) {
|
||||
underlineLinks = false
|
||||
}
|
||||
let _ = underlineLinks
|
||||
|
||||
let author = item.message.author
|
||||
let mainColor: UIColor
|
||||
var secondaryColor: UIColor? = nil
|
||||
var tertiaryColor: UIColor? = nil
|
||||
|
||||
let nameColors: PeerNameColors.Colors?
|
||||
switch author?.nameColor {
|
||||
case let .preset(nameColor):
|
||||
nameColors = item.context.peerNameColors.get(nameColor, dark: item.presentationData.theme.theme.overallDarkAppearance)
|
||||
case let .collectible(collectibleColor):
|
||||
nameColors = collectibleColor.peerNameColors(dark: item.presentationData.theme.theme.overallDarkAppearance)
|
||||
default:
|
||||
nameColors = nil
|
||||
}
|
||||
|
||||
let codeBlockTitleColor: UIColor
|
||||
let codeBlockAccentColor: UIColor
|
||||
let codeBlockBackgroundColor: UIColor
|
||||
if !isIncoming {
|
||||
mainColor = messageTheme.accentTextColor
|
||||
if let _ = nameColors?.secondary {
|
||||
secondaryColor = .clear
|
||||
}
|
||||
if let _ = nameColors?.tertiary {
|
||||
tertiaryColor = .clear
|
||||
}
|
||||
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
codeBlockTitleColor = .white
|
||||
codeBlockAccentColor = UIColor(white: 1.0, alpha: 0.5)
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
} else {
|
||||
codeBlockTitleColor = mainColor
|
||||
codeBlockAccentColor = mainColor
|
||||
codeBlockBackgroundColor = mainColor.withMultipliedAlpha(0.1)
|
||||
}
|
||||
} else {
|
||||
let authorNameColor = nameColors?.main
|
||||
secondaryColor = nameColors?.secondary
|
||||
tertiaryColor = nameColors?.tertiary
|
||||
|
||||
if let authorNameColor {
|
||||
mainColor = authorNameColor
|
||||
} else {
|
||||
mainColor = messageTheme.accentTextColor
|
||||
}
|
||||
|
||||
codeBlockTitleColor = mainColor
|
||||
codeBlockAccentColor = mainColor
|
||||
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.65)
|
||||
} else {
|
||||
codeBlockBackgroundColor = UIColor(white: 0.0, alpha: 0.05)
|
||||
}
|
||||
}
|
||||
|
||||
let _ = secondaryColor
|
||||
let _ = tertiaryColor
|
||||
|
||||
let _ = codeBlockTitleColor
|
||||
let _ = codeBlockAccentColor
|
||||
|
||||
let textCategories = InstantPageTextCategories(
|
||||
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||
header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: messageTheme.primaryTextColor),
|
||||
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: messageTheme.secondaryTextColor),
|
||||
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor),
|
||||
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: messageTheme.primaryTextColor)
|
||||
)
|
||||
let pageTheme = InstantPageTheme(
|
||||
type: isDark ? .dark : .light,
|
||||
pageBackgroundColor: .clear,
|
||||
textCategories: textCategories,
|
||||
serif: false,
|
||||
codeBlockBackgroundColor: codeBlockBackgroundColor,
|
||||
linkColor: messageTheme.linkTextColor,
|
||||
textHighlightColor: messageTheme.accentTextColor.withMultipliedAlpha(0.1),
|
||||
linkHighlightColor: messageTheme.linkTextColor.withMultipliedAlpha(0.1),
|
||||
markerColor: UIColor(rgb: 0xfef3bc),
|
||||
panelBackgroundColor: messageTheme.accentControlColor.withMultipliedAlpha(0.1),
|
||||
panelHighlightedBackgroundColor: messageTheme.accentControlColor.withMultipliedAlpha(0.25),
|
||||
panelPrimaryColor: messageTheme.primaryTextColor,
|
||||
panelSecondaryColor: messageTheme.secondaryTextColor,
|
||||
panelAccentColor: messageTheme.accentTextColor,
|
||||
tableBorderColor: isDark || !isIncoming ? messageTheme.accentControlColor.withMultipliedAlpha(0.25) : UIColor(white: 0.0, alpha: 0.1),
|
||||
tableHeaderColor: isDark || !isIncoming ? messageTheme.accentControlColor.withMultipliedAlpha(0.1) : UIColor(white: 0.0, alpha: 0.05),
|
||||
controlColor: messageTheme.accentControlColor,
|
||||
imageTintColor: nil,
|
||||
overlayPanelColor: isDark ? UIColor(white: 0.0, alpha: 0.13) : UIColor(white: 1.0, alpha: 0.13)
|
||||
)
|
||||
|
||||
if let webpage = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let instantPage = content.instantPage {
|
||||
if let current = currentPageLayout, current.boundingWidth == boundingSize.width {
|
||||
if let current = currentPageLayout, current.boundingWidth == boundingSize.width, previousItem?.presentationData.theme.theme === item.presentationData.theme.theme {
|
||||
pageLayout = current.layout
|
||||
currentLayoutTiles = previousCurrentLayoutTiles
|
||||
} else {
|
||||
let pageTheme = instantPageThemeForType(item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings(
|
||||
themeType: item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light,
|
||||
fontSize: .standard,
|
||||
lineSpacingFactor: 0.9,
|
||||
forceSerif: false,
|
||||
autoNightMode: false,
|
||||
ignoreAutoNightModeUntil: 0
|
||||
))
|
||||
pageLayout = instantPageLayoutForWebPage(webpage, instantPage: instantPage._parse(), userLocation: .other, boundingWidth: boundingWidth - 2.0, safeInset: 0.0, strings: item.presentationData.strings, theme: pageTheme, dateTimeFormat: item.presentationData.dateTimeFormat, webEmbedHeights: [:], addFeedback: false)
|
||||
pageLayout = instantPageLayoutForWebPage(webpage, instantPage: instantPage._parse(), userLocation: .other, boundingWidth: boundingWidth - 2.0, sideInset: 10.0, safeInset: 0.0, strings: item.presentationData.strings, theme: pageTheme, dateTimeFormat: item.presentationData.dateTimeFormat, webEmbedHeights: [:], addFeedback: false)
|
||||
if let pageLayout {
|
||||
currentLayoutTiles = instantPageTilesFromLayout(pageLayout, boundingWidth: boundingWidth)
|
||||
}
|
||||
|
|
@ -100,6 +199,7 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
return
|
||||
}
|
||||
self.item = item
|
||||
self.pageTheme = pageTheme
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: boundingSize.width - 2.0, height: boundingSize.height - 2.0))
|
||||
|
||||
|
|
@ -158,17 +258,9 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
}
|
||||
|
||||
private func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
|
||||
guard let messageItem = self.item else {
|
||||
guard let messageItem = self.item, let pageTheme = self.pageTheme else {
|
||||
return
|
||||
}
|
||||
let pageTheme = instantPageThemeForType(messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings(
|
||||
themeType: messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light,
|
||||
fontSize: .standard,
|
||||
lineSpacingFactor: 0.9,
|
||||
forceSerif: false,
|
||||
autoNightMode: false,
|
||||
ignoreAutoNightModeUntil: 0
|
||||
))
|
||||
let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate)
|
||||
|
||||
var visibleTileIndices = Set<Int>()
|
||||
|
|
@ -427,13 +519,23 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
// The chat URL handler will show a confirmation when concealed is true
|
||||
// and the visible text differs from the destination — safer default.
|
||||
let concealed = true
|
||||
let url = ChatMessageBubbleContentTapAction.Url(url: urlHit.urlItem.url, concealed: concealed)
|
||||
let url = ChatMessageBubbleContentTapAction.Url(url: urlHit.urlItem.url, concealed: concealed, allowInlineWebpageResolution: urlHit.urlItem.webpageId != nil)
|
||||
let rects = self.computeHighlightRects(item: urlHit.item, parentOffset: urlHit.parentOffset, localPoint: urlHit.localPoint)
|
||||
return ChatMessageBubbleContentTapAction(
|
||||
content: .url(url),
|
||||
rects: rects,
|
||||
activate: self.makeActivate(item: urlHit.item, parentOffset: urlHit.parentOffset, localPoint: urlHit.localPoint)
|
||||
)
|
||||
|
||||
if let webpageId = urlHit.urlItem.webpageId {
|
||||
let split = self.splitAnchor(url.url)
|
||||
return ChatMessageBubbleContentTapAction(
|
||||
content: .externalInstantPage(url: url, webpageId: webpageId, anchor: split.anchor),
|
||||
rects: rects,
|
||||
activate: self.makeActivate(item: urlHit.item, parentOffset: urlHit.parentOffset, localPoint: urlHit.localPoint)
|
||||
)
|
||||
} else {
|
||||
return ChatMessageBubbleContentTapAction(
|
||||
content: .url(url),
|
||||
rects: rects,
|
||||
activate: self.makeActivate(item: urlHit.item, parentOffset: urlHit.parentOffset, localPoint: urlHit.localPoint)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func textItemAtLocation(_ location: CGPoint) -> (item: InstantPageTextItem, parentOffset: CGPoint)? {
|
||||
|
|
@ -650,6 +752,42 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
return nil
|
||||
}
|
||||
|
||||
override public func getAnchorRect(anchor: String) -> CGRect? {
|
||||
guard let layout = self.currentPageLayout?.layout else {
|
||||
return nil
|
||||
}
|
||||
if let rect = self.anchorRect(in: layout.items, anchor: anchor, baseY: 0.0) {
|
||||
// Translate from layout/containerNode coords to bubble-content-node coords.
|
||||
// containerNode is offset by (1, 1) from the bubble content node.
|
||||
return rect.offsetBy(dx: 1.0, dy: 1.0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func anchorRect(in items: [InstantPageItem], anchor: String, baseY: CGFloat) -> CGRect? {
|
||||
for item in items {
|
||||
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
|
||||
return CGRect(x: item.frame.minX, y: baseY + item.frame.minY, width: 1.0, height: 1.0)
|
||||
} else if let item = item as? InstantPageTextItem {
|
||||
if let (lineIndex, _) = item.anchors[anchor] {
|
||||
let lineFrame = item.lines[lineIndex].frame
|
||||
return CGRect(x: item.frame.minX + lineFrame.minX, y: baseY + item.frame.minY + lineFrame.minY, width: lineFrame.width, height: lineFrame.height)
|
||||
}
|
||||
} else if let item = item as? InstantPageTableItem {
|
||||
if let (offset, _) = item.anchors[anchor] {
|
||||
return CGRect(x: item.frame.minX, y: baseY + item.frame.minY + offset, width: item.frame.width, height: 1.0)
|
||||
}
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
// Inner items are laid out below the title bar, so the recursive base
|
||||
// must include titleHeight (mirrors InstantPageDetailsNode.linkSelectionRects).
|
||||
if let rect = self.anchorRect(in: item.items, anchor: anchor, baseY: baseY + item.frame.minY + item.titleHeight) {
|
||||
return rect
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
||||
/*if let statusNode = self.statusNode, !statusNode.isHidden {
|
||||
return statusNode.reactionView(value: value)
|
||||
|
|
@ -692,7 +830,13 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
|
|||
}
|
||||
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
// TODO: implement intra-page anchor scrolling
|
||||
let _ = anchor
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
if anchor.isEmpty {
|
||||
item.controllerInteraction.scrollToMessageId(item.message.index, 0.0)
|
||||
} else {
|
||||
item.controllerInteraction.scrollToMessageIdWithAnchor(item.message.index, anchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -832,8 +832,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
}
|
||||
|
||||
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
|
||||
animation.animator.updateFrame(layer: strongSelf.containerNode.layer, frame: CGRect(origin: CGPoint(), size: boundingSize), completion: nil)
|
||||
|
||||
animation.animator.updatePosition(layer: strongSelf.containerNode.layer, position: CGRect(origin: CGPoint(), size: boundingSize).center, completion: nil)
|
||||
animation.animator.updateBounds(layer: strongSelf.containerNode.layer, bounds: CGRect(origin: CGPoint(), size: boundingSize), completion: nil)
|
||||
|
||||
if let formattedDateUpdatePeriod {
|
||||
if strongSelf.relativeDateTimer?.period != formattedDateUpdatePeriod {
|
||||
|
|
@ -917,7 +917,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
}
|
||||
)
|
||||
))
|
||||
animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: realTextFrame, completion: nil)
|
||||
animation.animator.updatePosition(layer: strongSelf.textNode.textNode.layer, position: realTextFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: strongSelf.textNode.textNode.layer, bounds: CGRect(origin: CGPoint(), size: realTextFrame.size), completion: nil)
|
||||
|
||||
switch strongSelf.visibility {
|
||||
case .none:
|
||||
|
|
@ -971,7 +972,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
}
|
||||
statusNode.frame = statusFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil)
|
||||
animation.animator.updatePosition(layer: statusNode.layer, position: statusFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: statusNode.layer, bounds: CGRect(origin: CGPoint(), size: statusFrame.size), completion: nil)
|
||||
}
|
||||
} else if let statusNode = strongSelf.statusNode {
|
||||
strongSelf.statusNode = nil
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}
|
||||
}, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in
|
||||
self?.openUrl(url.url, progress: url.progress)
|
||||
}, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) {
|
||||
|
|
@ -656,7 +657,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}, requestMessageUpdate: { _, _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
|
|
|
|||
|
|
@ -426,7 +426,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||
return false
|
||||
}, editGif: { _, _ in
|
||||
}, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in
|
||||
}, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, presentControllerInCurrent: { _, _ in
|
||||
}, navigationController: {
|
||||
|
|
@ -503,7 +504,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
|||
}, requestMessageUpdate: { _, _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,22 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
}
|
||||
}
|
||||
|
||||
public struct OpenInstantPage {
|
||||
public var webpageId: MediaId
|
||||
public var url: String
|
||||
public var anchor: String?
|
||||
public var concealed: Bool
|
||||
public var progress: Promise<Bool>?
|
||||
|
||||
public init(webpageId: MediaId, url: String, anchor: String?, concealed: Bool, progress: Promise<Bool>? = nil) {
|
||||
self.webpageId = webpageId
|
||||
self.url = url
|
||||
self.anchor = anchor
|
||||
self.concealed = concealed
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
public struct LongTapParams {
|
||||
public var message: Message?
|
||||
public var contentNode: ContextExtractedContentContainingNode?
|
||||
|
|
@ -204,6 +220,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
|
||||
public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void
|
||||
public let openUrl: (OpenUrl) -> Void
|
||||
public let openExternalInstantPage: (OpenInstantPage) -> Void
|
||||
public let shareCurrentLocation: () -> Void
|
||||
public let shareAccountContact: () -> Void
|
||||
public let sendBotCommand: (MessageId?, String) -> Void
|
||||
|
|
@ -291,7 +308,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
public let requestMessageUpdate: (MessageId, Bool, ControlledTransition?) -> Void
|
||||
public let cancelInteractiveKeyboardGestures: () -> Void
|
||||
public let dismissTextInput: () -> Void
|
||||
public let scrollToMessageId: (MessageIndex) -> Void
|
||||
public let scrollToMessageId: (MessageIndex, CGFloat) -> Void
|
||||
public let scrollToMessageIdWithAnchor: (MessageIndex, String) -> Void
|
||||
public let navigateToStory: (Message, StoryId) -> Void
|
||||
public let attemptedNavigationToPrivateQuote: (Peer?) -> Void
|
||||
public let forceUpdateWarpContents: () -> Void
|
||||
|
|
@ -382,6 +400,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void,
|
||||
activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void,
|
||||
openUrl: @escaping (OpenUrl) -> Void,
|
||||
openExternalInstantPage: @escaping (OpenInstantPage) -> Void,
|
||||
shareCurrentLocation: @escaping () -> Void,
|
||||
shareAccountContact: @escaping () -> Void,
|
||||
sendBotCommand: @escaping (MessageId?, String) -> Void,
|
||||
|
|
@ -469,7 +488,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
requestMessageUpdate: @escaping (MessageId, Bool, ControlledTransition?) -> Void,
|
||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||
dismissTextInput: @escaping () -> Void,
|
||||
scrollToMessageId: @escaping (MessageIndex) -> Void,
|
||||
scrollToMessageId: @escaping (MessageIndex, CGFloat) -> Void,
|
||||
scrollToMessageIdWithAnchor: @escaping (MessageIndex, String) -> Void,
|
||||
navigateToStory: @escaping (Message, StoryId) -> Void,
|
||||
attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void,
|
||||
forceUpdateWarpContents: @escaping () -> Void,
|
||||
|
|
@ -512,6 +532,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
self.requestMessageActionUrlAuth = requestMessageActionUrlAuth
|
||||
self.activateSwitchInline = activateSwitchInline
|
||||
self.openUrl = openUrl
|
||||
self.openExternalInstantPage = openExternalInstantPage
|
||||
self.shareCurrentLocation = shareCurrentLocation
|
||||
self.shareAccountContact = shareAccountContact
|
||||
self.sendBotCommand = sendBotCommand
|
||||
|
|
@ -601,6 +622,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
self.dismissTextInput = dismissTextInput
|
||||
self.scrollToMessageId = scrollToMessageId
|
||||
self.scrollToMessageIdWithAnchor = scrollToMessageIdWithAnchor
|
||||
self.navigateToStory = navigateToStory
|
||||
self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote
|
||||
self.forceUpdateWarpContents = forceUpdateWarpContents
|
||||
|
|
|
|||
|
|
@ -3246,8 +3246,11 @@ final class TextContentItemLayer: SimpleLayer {
|
|||
}
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: self.renderNodeContainer, frame: effectiveContentFrame, completion: nil)
|
||||
animation.animator.updateFrame(layer: self.renderNode.layer, frame: CGRect(origin: CGPoint(), size: effectiveContentFrame.size), completion: nil)
|
||||
animation.animator.updatePosition(layer: self.renderNodeContainer, position: effectiveContentFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: self.renderNodeContainer, bounds: CGRect(origin: CGPoint(), size: effectiveContentFrame.size), completion: nil)
|
||||
|
||||
animation.animator.updatePosition(layer: self.renderNode.layer, position: effectiveContentFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: self.renderNode.layer, bounds: CGRect(origin: CGRect(origin: CGPoint(), size: effectiveContentFrame.size).center, size: effectiveContentFrame.size), completion: nil)
|
||||
|
||||
var staticContentMask = contentMask
|
||||
if let contentMask, self.isAnimating {
|
||||
|
|
|
|||
|
|
@ -1109,6 +1109,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
return
|
||||
}
|
||||
strongSelf.openUrl(url: url.url, concealed: url.concealed, external: url.external ?? false)
|
||||
}, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
|
|
@ -1274,7 +1275,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
}, requestMessageUpdate: { _, _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
|
|
|
|||
|
|
@ -3221,6 +3221,66 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
} else {
|
||||
strongSelf.openUrl(url, concealed: concealed, forceExternal: forceExternal, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress)
|
||||
}
|
||||
}, openExternalInstantPage: { [weak self] instantPage in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (webpagePreviewWithProgress(account: self.context.account, urls: [instantPage.url], webpageId: instantPage.webpageId)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] result in
|
||||
Task { @MainActor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case let .result(webpageResult):
|
||||
instantPage.progress?.set(.single(false))
|
||||
|
||||
guard let webpageResult else {
|
||||
self.openUrl(instantPage.url, concealed: true)
|
||||
return
|
||||
}
|
||||
|
||||
if case .Loaded = webpageResult.webpage.content {
|
||||
let sourceLocation: InstantPageSourceLocation
|
||||
|
||||
if let peerId = self.chatLocation.peerId {
|
||||
let (peer, isContact) = await self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.IsContact(id: peerId)
|
||||
).get()
|
||||
|
||||
if let peer {
|
||||
let peerType: MediaAutoDownloadPeerType
|
||||
switch peer {
|
||||
case .user:
|
||||
peerType = isContact ? .contact : .otherPrivate
|
||||
case let .channel(channel):
|
||||
if case .group = channel.info {
|
||||
peerType = .group
|
||||
} else {
|
||||
peerType = .channel
|
||||
}
|
||||
case .legacyGroup:
|
||||
peerType = .group
|
||||
case .secretChat:
|
||||
return
|
||||
}
|
||||
let peerLocation = MediaResourceUserLocation.peer(peer.id)
|
||||
sourceLocation = InstantPageSourceLocation(userLocation: peerLocation, peerType: peerType)
|
||||
} else {
|
||||
sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel)
|
||||
}
|
||||
} else {
|
||||
sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel)
|
||||
}
|
||||
|
||||
self.push(makeInstantPageControllerImpl(context: self.context, webPage: webpageResult.webpage, anchor: instantPage.anchor, sourceLocation: sourceLocation))
|
||||
}
|
||||
case .progress:
|
||||
instantPage.progress?.set(.single(true))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, shareCurrentLocation: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||||
|
|
@ -5334,8 +5394,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}
|
||||
}, dismissTextInput: { [weak self] in
|
||||
self?.chatDisplayNode.dismissTextInput()
|
||||
}, scrollToMessageId: { [weak self] index in
|
||||
self?.chatDisplayNode.historyNode.scrollToMessage(index: index)
|
||||
}, scrollToMessageId: { [weak self] index, offset in
|
||||
self?.chatDisplayNode.historyNode.scrollToMessage(index: index, offset: offset)
|
||||
}, scrollToMessageIdWithAnchor: { [weak self] index, anchor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
// Find the bubble for this message (it must be at least partially
|
||||
// visible — we got here from a tap inside it) and compute the
|
||||
// anchor's y in item-local coords. .bottom(anchorY) places the item
|
||||
// so the anchor lands at the visual top of the rotated chat list's
|
||||
// content area; .center(.custom) is bypassed for short items, so it
|
||||
// can't position the anchor uniformly.
|
||||
var anchorY: CGFloat?
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
guard anchorY == nil else {
|
||||
return
|
||||
}
|
||||
if let itemNode = itemNode as? ChatMessageBubbleItemNode,
|
||||
itemNode.item?.message.id == index.id,
|
||||
let rect = itemNode.getAnchorRect(anchor: anchor) {
|
||||
anchorY = rect.minY
|
||||
}
|
||||
}
|
||||
if let anchorY {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(
|
||||
from: index, to: index,
|
||||
animated: true, highlight: false,
|
||||
scrollPosition: .bottom(anchorY)
|
||||
)
|
||||
} else {
|
||||
self.chatDisplayNode.historyNode.scrollToMessage(index: index)
|
||||
}
|
||||
}, navigateToStory: { [weak self] message, storyId in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -5064,7 +5064,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH
|
|||
}
|
||||
}
|
||||
|
||||
func scrollToMessage(index: MessageIndex) {
|
||||
func scrollToMessage(index: MessageIndex, offset: CGFloat = 0.0) {
|
||||
self.appliedScrollToMessageId = nil
|
||||
self.scrollToMessageIdPromise.set(.single(index))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func makeInstantPageControllerImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? {
|
||||
public func makeInstantPageControllerImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? {
|
||||
guard let (webpage, anchor) = instantPageAndAnchor(message: message) else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -486,7 +486,7 @@ func makeInstantPageControllerImpl(context: AccountContext, message: Message, so
|
|||
return makeInstantPageControllerImpl(context: context, webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)
|
||||
}
|
||||
|
||||
func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController {
|
||||
public func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController {
|
||||
return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation, preloadedResources: nil))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
|||
}, requestMessageActionUrlAuth: { _, _ in
|
||||
}, activateSwitchInline: { _, _, _ in
|
||||
}, openUrl: { _ in
|
||||
}, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
|
|
@ -248,7 +249,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
|||
}, requestMessageUpdate: { _, _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, scrollToMessageId: { _, _ in
|
||||
}, scrollToMessageIdWithAnchor: { _, _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, forceUpdateWarpContents: {
|
||||
|
|
|
|||
|
|
@ -2392,6 +2392,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
requestMessageActionUrlAuth: { _, _ in },
|
||||
activateSwitchInline: { _, _, _ in },
|
||||
openUrl: { _ in },
|
||||
openExternalInstantPage: { _ in
|
||||
},
|
||||
shareCurrentLocation: {},
|
||||
shareAccountContact: {},
|
||||
sendBotCommand: { _, _ in },
|
||||
|
|
@ -2560,7 +2562,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
},
|
||||
dismissTextInput: {
|
||||
},
|
||||
scrollToMessageId: { _ in
|
||||
scrollToMessageId: { _, _ in
|
||||
},
|
||||
scrollToMessageIdWithAnchor: { _, _ in
|
||||
},
|
||||
navigateToStory: { _, _ in
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,17 +21,19 @@ public enum InstantPagePresentationFontSize: Int32 {
|
|||
}
|
||||
|
||||
public final class InstantPagePresentationSettings: Codable, Equatable {
|
||||
public static var defaultSettings = InstantPagePresentationSettings(themeType: .light, fontSize: .standard, forceSerif: false, autoNightMode: true, ignoreAutoNightModeUntil: 0)
|
||||
public static var defaultSettings = InstantPagePresentationSettings(themeType: .light, fontSize: .standard, lineSpacingFactor: 1.0, forceSerif: false, autoNightMode: true, ignoreAutoNightModeUntil: 0)
|
||||
|
||||
public var themeType: InstantPageThemeType
|
||||
public var fontSize: InstantPagePresentationFontSize
|
||||
public var lineSpacingFactor: CGFloat
|
||||
public var forceSerif: Bool
|
||||
public var autoNightMode: Bool
|
||||
public var ignoreAutoNightModeUntil: Int32
|
||||
|
||||
public init(themeType: InstantPageThemeType, fontSize: InstantPagePresentationFontSize, forceSerif: Bool, autoNightMode: Bool, ignoreAutoNightModeUntil: Int32) {
|
||||
public init(themeType: InstantPageThemeType, fontSize: InstantPagePresentationFontSize, lineSpacingFactor: CGFloat, forceSerif: Bool, autoNightMode: Bool, ignoreAutoNightModeUntil: Int32) {
|
||||
self.themeType = themeType
|
||||
self.fontSize = fontSize
|
||||
self.lineSpacingFactor = lineSpacingFactor
|
||||
self.forceSerif = forceSerif
|
||||
self.autoNightMode = autoNightMode
|
||||
self.ignoreAutoNightModeUntil = ignoreAutoNightModeUntil
|
||||
|
|
@ -42,6 +44,7 @@ public final class InstantPagePresentationSettings: Codable, Equatable {
|
|||
|
||||
self.themeType = InstantPageThemeType(rawValue: try container.decode(Int32.self, forKey: "themeType"))!
|
||||
self.fontSize = InstantPagePresentationFontSize(rawValue: try container.decode(Int32.self, forKey: "fontSize"))!
|
||||
self.lineSpacingFactor = try container.decodeIfPresent(Double.self, forKey: "lineSpacingFactor") ?? 1.0
|
||||
self.forceSerif = try container.decode(Int32.self, forKey: "forceSerif") != 0
|
||||
self.autoNightMode = try container.decode(Int32.self, forKey: "autoNightMode") != 0
|
||||
self.ignoreAutoNightModeUntil = try container.decode(Int32.self, forKey: "ignoreAutoNightModeUntil")
|
||||
|
|
@ -52,6 +55,7 @@ public final class InstantPagePresentationSettings: Codable, Equatable {
|
|||
|
||||
try container.encode(self.themeType.rawValue, forKey: "themeType")
|
||||
try container.encode(self.fontSize.rawValue, forKey: "fontSize")
|
||||
try container.encode(self.lineSpacingFactor, forKey: "lineSpacingFactor")
|
||||
try container.encode((self.forceSerif ? 1 : 0) as Int32, forKey: "forceSerif")
|
||||
try container.encode((self.autoNightMode ? 1 : 0) as Int32, forKey: "autoNightMode")
|
||||
try container.encode(self.ignoreAutoNightModeUntil, forKey: "ignoreAutoNightModeUntil")
|
||||
|
|
@ -64,6 +68,9 @@ public final class InstantPagePresentationSettings: Codable, Equatable {
|
|||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.lineSpacingFactor != rhs.lineSpacingFactor {
|
||||
return false
|
||||
}
|
||||
if lhs.forceSerif != rhs.forceSerif {
|
||||
return false
|
||||
}
|
||||
|
|
@ -77,23 +84,23 @@ public final class InstantPagePresentationSettings: Codable, Equatable {
|
|||
}
|
||||
|
||||
public func withUpdatedThemeType(_ themeType: InstantPageThemeType) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
return InstantPagePresentationSettings(themeType: themeType, fontSize: self.fontSize, lineSpacingFactor: self.lineSpacingFactor, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
}
|
||||
|
||||
public func withUpdatedFontSize(_ fontSize: InstantPagePresentationFontSize) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: fontSize, lineSpacingFactor: self.lineSpacingFactor, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
}
|
||||
|
||||
public func withUpdatedForceSerif(_ forceSerif: Bool) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, lineSpacingFactor: self.lineSpacingFactor, forceSerif: forceSerif, autoNightMode: self.autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
}
|
||||
|
||||
public func withUpdatedAutoNightMode(_ autoNightMode: Bool) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, lineSpacingFactor: self.lineSpacingFactor, forceSerif: self.forceSerif, autoNightMode: autoNightMode, ignoreAutoNightModeUntil: self.ignoreAutoNightModeUntil)
|
||||
}
|
||||
|
||||
public func withUpdatedIgnoreAutoNightModeUntil(_ ignoreAutoNightModeUntil: Int32) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: autoNightMode, ignoreAutoNightModeUntil: ignoreAutoNightModeUntil)
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, lineSpacingFactor: self.lineSpacingFactor, forceSerif: self.forceSerif, autoNightMode: autoNightMode, ignoreAutoNightModeUntil: ignoreAutoNightModeUntil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue