Merge commit 'c2a10931b6555f868db6d1527d3f6a6583864c48'

This commit is contained in:
Isaac 2025-06-29 14:01:34 +02:00
commit ea9f53acb8
20 changed files with 141 additions and 61 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -14504,4 +14504,6 @@ Sorry for the inconvenience.";
"Bot.AddToGroup.Title" = "Add to Group";
"Bot.AddToChannel.Title" = "Add to Channel";
"ScheduledMessages.TodoUnavailable" = "Voting will become available after the message is published.";
"ScheduledMessages.TodoUnavailable" = "Completing tasks will become available after the message is published.";
"Attachment.DiscardTodoAlertText" = "Discard checklist items?";

View File

@ -1178,6 +1178,10 @@ private enum StatsEntry: ItemListNodeEntry {
} else if transaction.flags.contains(.isRefund) {
title = NSAttributedString(string: presentationData.strings.Monetization_Transaction_Refund, font: font, textColor: theme.list.itemPrimaryTextColor)
detailText = stringForMediumCompactDate(timestamp: transaction.date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
} else if case .peer = transaction.peer {
return StarsTransactionItem(context: arguments.context, presentationData: presentationData, transaction: transaction, action: {
arguments.openStarsTransaction(transaction)
}, sectionId: self.section, style: .blocks)
} else {
title = NSAttributedString()
detailText = ""

View File

@ -278,17 +278,12 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
}
let itemLabel: NSAttributedString
let labelString: String
let formattedLabel = formatCurrencyAmountText(item.transaction.count, dateTimeFormat: item.presentationData.dateTimeFormat, showPlus: true)
let absCount = StarsAmount(value: abs(item.transaction.count.amount.value), nanos: abs(item.transaction.count.amount.nanos))
let formattedLabel = presentationStringsFormattedNumber(absCount, item.presentationData.dateTimeFormat.groupingSeparator)
if item.transaction.count.amount < StarsAmount.zero {
labelString = "- \(formattedLabel)"
} else {
labelString = "+ \(formattedLabel)"
}
let itemLabelColor = labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: itemLabelColor)
let smallLabelFont = Font.with(size: floor(fontBaseDisplaySize / 17.0 * 13.0))
let labelFont = Font.medium(fontBaseDisplaySize)
let labelColor = formattedLabel.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
itemLabel = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
var itemDateColor = item.presentationData.theme.list.itemSecondaryTextColor
itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
@ -334,6 +329,18 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
maximumNumberOfLines: 1
)))
)
let itemIconName: String
let itemIconColor: UIColor?
switch item.transaction.count.currency {
case .stars:
itemIconName = "Premium/Stars/StarMedium"
itemIconColor = nil
case .ton:
itemIconName = "Ads/TonAbout"
itemIconColor = labelColor
}
let itemSize = strongSelf.componentView.update(
transition: .immediate,
component: AnyComponent(ListActionItemComponent(
@ -342,7 +349,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, iconName: itemIconName, iconColor: itemIconColor))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in
guard let self, let item = self.item else {
return

View File

@ -1447,7 +1447,7 @@ public struct PresentationResourcesChat {
public static func chatServiceMessageTodoAppendedIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatServiceMessageTodoAppendedIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ServiceTodoIncompleted"), color: .white)
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ServiceTodoAppended"), color: .white)
})
}

View File

@ -442,7 +442,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
}
}
for i in 0 ..< labelRects.count {
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
labelRects[i].size.height = 22.0
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
}
@ -458,7 +458,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
backgroundMaskImage = (currentOffset, currentImage)
} else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .white, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .white, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskUpdated = true
}
}

View File

@ -417,21 +417,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count))
text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string
case let .giftTon(currency, amount, cryptoCurrency, cryptoAmount, _):
months = 1000
case let .giftTon(_, amount, _, cryptoAmount, _):
if amount < 10000000000 {
months = 1000
} else if amount < 50000000000 {
months = 2000
} else {
months = 3000
}
var peerName = ""
if let peer = item.message.peers[item.message.id.peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
//TODO:localize
let _ = currency
let _ = amount
let _ = cryptoCurrency
let cryptoAmount = cryptoAmount ?? 0
title = "$ \(formatTonAmountText(cryptoAmount, dateTimeFormat: item.presentationData.dateTimeFormat))"
title = "$ \(formatTonAmountText(cryptoAmount, dateTimeFormat: item.presentationData.dateTimeFormat, maxDecimalPositions: 3))"
text = incoming ? "Use TON to submit post suggestions to channels." : "With TON, \(peerName) will be able to submit post suggestions to channels."
buttonTitle = ""
case let .prizeStars(count, _, channelId, _, _):
@ -644,7 +645,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
switch months {
case 1000:
animationName = "GiftDiamond"
animationName = "GiftDiamond1"
case 2000:
animationName = "GiftDiamond2"
case 3000:
animationName = "GiftDiamond3"
case 12:
animationName = "Gift12"
case 6:
@ -787,8 +792,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
for i in 0 ..< labelRects.count {
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0))
labelRects[i].size.height = 20.0
labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
labelRects[i].size.height = 22.0
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
}
@ -798,7 +803,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
backgroundMaskImage = (currentOffset, currentImage)
} else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskUpdated = true
}
} else {
@ -807,7 +812,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var backgroundSize = giftSize
if hasServiceMessage {
backgroundSize.height += labelLayout.size.height + 18.0
backgroundSize.height += labelLayout.size.height + 20.0
} else {
backgroundSize.height += 4.0
}
@ -828,7 +833,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance && uniquePatternFile == nil ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 12.0 : 0.0), size: giftSize)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 13.0 : 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
var iconSize = CGSize(width: 160.0, height: 160.0)

View File

@ -376,6 +376,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
private var backgroundWallpaperNode: ChatMessageBubbleBackdrop?
private var backgroundNode: ChatMessageBackground?
private var extractedRadioView: UIView?
private var extractedIconView: UIView?
private var extractedAvatarView: UIView?
private var extractedTitleNode: TextNodeWithEntities?
private var extractedNameView: UIView?
@ -549,6 +550,11 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
// self.backgroundWallpaperNode?.update(rect: mappedRect, within: containerSize)
// }
if let extractedIconView = self.iconNode?.view.snapshotContentTree() {
self.extractedIconView = extractedIconView
self.contextSourceNode.contentNode.view.addSubview(extractedIconView)
}
if let extractedRadioView = self.radioNode?.view.snapshotContentTree() {
self.extractedRadioView = extractedRadioView
self.contextSourceNode.contentNode.view.addSubview(extractedRadioView)
@ -585,6 +591,8 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
transition.updateAlpha(node: backgroundNode, alpha: 0.0, completion: { [weak backgroundNode] _ in
self.extractedRadioView?.removeFromSuperview()
self.extractedRadioView = nil
self.extractedIconView?.removeFromSuperview()
self.extractedIconView = nil
self.extractedAvatarView?.removeFromSuperview()
self.extractedAvatarView = nil
self.extractedTitleNode?.textNode.removeFromSupernode()
@ -737,7 +745,7 @@ private final class ChatMessageTodoItemNode: ASDisplayNode {
} else {
titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size)
}
if let _ = completion, canMark && todo.flags.contains(.othersCanComplete) {
if let _ = completion, todo.flags.contains(.othersCanComplete) {
titleNodeFrame = titleNodeFrame.offsetBy(dx: 0.0, dy: -6.0)
}

View File

@ -28,6 +28,7 @@ import TextFormat
import TextFieldComponent
import ListComposePollOptionComponent
import Markdown
import PresentationDataUtils
final class ComposeTodoScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -1750,10 +1751,30 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
}
public func requestDismiss(completion: @escaping () -> Void) {
completion()
guard let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let input = componentView.validatedInput(), !input.text.isEmpty || !input.items.isEmpty {
let text = presentationData.strings.Attachment_DiscardTodoAlertText
let controller = textAlertController(context: self.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Attachment_CancelSelectionAlertNo, action: {
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Attachment_CancelSelectionAlertYes, action: {
completion()
})])
self.present(controller, in: .window(.root))
} else {
completion()
}
}
public func shouldDismissImmediately() -> Bool {
return true
guard let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else {
return true
}
if let input = componentView.validatedInput(), !input.text.isEmpty || !input.items.isEmpty {
return false
} else {
return true
}
}
}

View File

@ -802,8 +802,13 @@ public final class ListComposePollOptionComponent: Component {
}
if let deleteAction = component.deleteAction {
if self.deleteRevealView == nil {
let deleteRevealView = DeleteRevealView(title: component.strings.Common_Delete, color: component.theme.list.itemDisclosureActions.destructive.fillColor)
var deleteRevealViewTransition = transition
let deleteRevealView: DeleteRevealView
if let current = self.deleteRevealView {
deleteRevealView = current
} else {
deleteRevealViewTransition = .immediate
deleteRevealView = DeleteRevealView(title: component.strings.Common_Delete, color: component.theme.list.itemDisclosureActions.destructive.fillColor)
deleteRevealView.tapped = { [weak self] action in
guard let self else {
return
@ -818,14 +823,17 @@ public final class ListComposePollOptionComponent: Component {
}
self.deleteRevealView = deleteRevealView
self.addSubview(deleteRevealView)
if self.recognizer == nil {
let recognizer = RevealOptionsGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
recognizer.delegate = self
self.addGestureRecognizer(recognizer)
self.recognizer = recognizer
}
}
if self.recognizer == nil {
let recognizer = RevealOptionsGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
recognizer.delegate = self
self.addGestureRecognizer(recognizer)
self.recognizer = recognizer
}
let _ = deleteRevealView.updateLayout(availableSize: size, revealOffset: self.revealOffset, transition: deleteRevealViewTransition)
deleteRevealView.frame = CGRect(origin: .zero, size: size)
} else {
if let deleteRevealView = self.deleteRevealView {
self.deleteRevealView = nil
@ -841,11 +849,6 @@ public final class ListComposePollOptionComponent: Component {
self.revealOffset = 0.0
}
if let deleteRevealView = self.deleteRevealView {
let _ = deleteRevealView.updateLayout(availableSize: size, revealOffset: self.revealOffset, transition: transition)
deleteRevealView.frame = CGRect(origin: .zero, size: size)
}
self.separatorInset = leftInset
return size

View File

@ -851,10 +851,26 @@ public final class StarsImageComponent: Component {
if let current = self.animationNode {
animationNode = current
} else {
let stickerName: String = count == 1000 ? "GiftDiamond" : "Gift\(count)"
let animationName: String
switch count {
case 1000:
animationName = "GiftDiamond1"
case 2000:
animationName = "GiftDiamond2"
case 3000:
animationName = "GiftDiamond3"
case 12:
animationName = "Gift12"
case 6:
animationName = "Gift6"
case 3:
animationName = "Gift3"
default:
animationName = "Gift3"
}
animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = true
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: stickerName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
animationNode.visibility = true
containerNode.view.addSubview(animationNode.view)
self.animationNode = animationNode

View File

@ -222,14 +222,12 @@ extension ChatControllerImpl {
self.canReadHistory.set(false)
//TODO:localize
var sources: [ContextController.Source] = []
sources.append(
ContextController.Source(
id: AnyHashable(OptionsId.item),
title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionTask,
footer: self.presentationData.strings.Chat_Todo_ContextMenu_SectionsInfo,
//source: .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
source: .extracted(ChatTodoItemContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)),
items: .single(ContextController.Items(content: .list(items)))
)

View File

@ -978,6 +978,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let incompletedTaskId = incompleted.first {
todoTaskId = incompletedTaskId
}
} else if case let .todoAppendTasks(tasks) = action.action {
if let task = tasks.first {
todoTaskId = task.id
}
}
self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, todoTaskId: todoTaskId)))
break

View File

@ -2132,7 +2132,19 @@ extension ChatControllerImpl {
guard let self else {
return
}
if canEdit {
func areItemsOnlyAppended(existing: [TelegramMediaTodo.Item], updated: [TelegramMediaTodo.Item]) -> Bool {
guard updated.count >= existing.count else {
return false
}
for (index, existingItem) in existing.enumerated() {
if index >= updated.count || updated[index] != existingItem {
return false
}
}
return true
}
if canEdit && !areItemsOnlyAppended(existing: existingTodo.items, updated: todo.items) {
let _ = self.context.engine.messages.requestEditMessage(
messageId: messageId,
text: "",

View File

@ -90,9 +90,11 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.selectAll ? nil : self.message.stableId) {
result = ContextControllerTakeViewInfo(containingItem: .node(contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
if self.snapshot, let snapshotView = contentNode.contentNode.view.snapshotContentTree(unhide: false, keepPortals: true, keepTransform: true) {
contentNode.view.superview?.addSubview(snapshotView)
self.snapshotView = snapshotView
Queue.mainQueue().justDispatch {
if self.snapshot, let snapshotView = contentNode.contentNode.view.snapshotContentTree(unhide: false, keepPortals: true, keepTransform: true) {
contentNode.view.superview?.addSubview(snapshotView)
self.snapshotView = snapshotView
}
}
}
}

View File

@ -328,11 +328,6 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
params.blockInteraction.set(.single(true))
var presentInCurrent = false
if let channel = params.message.peers[params.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
presentInCurrent = true
}
let _ = (gallery
|> deliverOnMainQueue).startStandalone(next: { gallery in
params.blockInteraction.set(.single(false))
@ -340,13 +335,16 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
gallery.centralItemUpdated = { messageId in
params.centralItemUpdated?(messageId)
}
params.present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in
let arguments = GalleryControllerPresentationArguments(transitionArguments: { messageId, media in
let selectedTransitionNode = params.transitionNode(messageId, media, false)
if let selectedTransitionNode = selectedTransitionNode {
return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface)
}
return nil
}), presentInCurrent ? .current : .window(.root))
})
params.present(gallery, arguments, .window(.root))
})
return true
case let .secretGallery(gallery):