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:
isaac 2026-05-01 17:55:19 +02:00
parent f29af03cd7
commit fdb2f369ec
26 changed files with 1148 additions and 100 deletions

View file

@ -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: {

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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))
}
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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):

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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: {

View file

@ -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: {

View file

@ -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

View file

@ -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 {

View file

@ -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: {

View file

@ -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

View file

@ -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))
}

View file

@ -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))
}

View file

@ -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: {

View file

@ -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
},

View file

@ -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)
}
}