This commit is contained in:
isaac 2026-06-02 17:49:38 +02:00
commit cfe335d124
13 changed files with 284 additions and 209 deletions

View file

@ -16342,3 +16342,16 @@ Error: %8$@";
"MediaPicker.SetNewGroupPhoto" = "Set new group photo";
"MediaPicker.SetNewChannelPhoto" = "Set new channel photo";
"Attachment.Link" = "Link";
"Checkout.PaymentMethod.Proceed" = "Proceed";
"Checkout.ShippingMethod.Proceed" = "Proceed";
"OpenLinkConfirmation.Title" = "Open link?";
"OpenLinkConfirmation.Open" = "Open";
"Chat.ContextMenu.OpenInBrowser" = "Open in Browser";
"Chat.ContextMenu.OpenInApp" = "Open In-App";
"CreatePoll.Link.Description" = "Attach a link to this option.";

View file

@ -244,8 +244,7 @@ private final class AttachButtonComponent: CombinedComponent {
name = strings.Attachment_Audio
imageName = "Chat/Attach Menu/Audio"
case .link:
//TODO:localize
name = "Link"
name = strings.Attachment_Link
imageName = "Chat/Attach Menu/Link"
case let .app(bot):
botPeer = bot.peer
@ -2187,8 +2186,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
case .audio:
accessibilityTitle = self.presentationData.strings.Attachment_Audio
case .link:
//TODO:localize
accessibilityTitle = "Link"
accessibilityTitle = self.presentationData.strings.Attachment_Link
case let .app(bot):
accessibilityTitle = bot.shortName
case .standalone:

View file

@ -328,9 +328,9 @@ private final class BotCheckoutPaymentMethodScreenComponent: Component {
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in
guard let self, !self.isDismissing else {
@ -379,7 +379,7 @@ private final class BotCheckoutPaymentMethodScreenComponent: Component {
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_PaymentMethod,
string: environment.strings.Checkout_PaymentMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
@ -402,7 +402,6 @@ private final class BotCheckoutPaymentMethodScreenComponent: Component {
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
@ -413,7 +412,7 @@ private final class BotCheckoutPaymentMethodScreenComponent: Component {
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
text: environment.strings.Checkout_PaymentMethod_Proceed,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
@ -440,16 +439,16 @@ private final class BotCheckoutPaymentMethodScreenComponent: Component {
animateOut: self.animateOut
)),
environment: {
environmentValue
environment
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
statusBarHeight: environment.statusBarHeight,
safeInsets: environment.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.isVisible,
isCentered: environment.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in

View file

@ -225,9 +225,9 @@ private final class BotCheckoutShippingOptionScreenComponent: Component {
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let dismiss: (Bool) -> Void = { [weak self] animated in
guard let self, !self.isDismissing else {
@ -268,7 +268,7 @@ private final class BotCheckoutShippingOptionScreenComponent: Component {
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_ShippingMethod,
string: environment.strings.Checkout_ShippingMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
@ -291,7 +291,6 @@ private final class BotCheckoutShippingOptionScreenComponent: Component {
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
@ -302,7 +301,7 @@ private final class BotCheckoutShippingOptionScreenComponent: Component {
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
text: environment.strings.Checkout_ShippingMethod_Proceed,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
@ -323,16 +322,16 @@ private final class BotCheckoutShippingOptionScreenComponent: Component {
animateOut: self.animateOut
)),
environment: {
environmentValue
environment
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
statusBarHeight: environment.statusBarHeight,
safeInsets: environment.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.isVisible,
isCentered: environment.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in

View file

@ -5900,8 +5900,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
actions.append(.init(title: self.presentationData.strings.Common_Cancel))
//TODO:localize
let title: String = "Delete Chat"
let title: String = self.presentationData.strings.ChatList_DeleteChat
var text: String
if mainPeer.id == self.context.account.peerId {
text = self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmation
@ -5948,56 +5947,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
],
actions: actions
)
// let actionSheet = ActionSheetController(presentationData: self.presentationData)
// var items: [ActionSheetItem] = []
//
// items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
//
// if joined || mainPeer.isDeleted {
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// }))
// } else {
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// }))
// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// guard let strongSelf = self else {
// return
// }
// strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
// TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
// completion(false)
// }),
// TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
// removed()
// })
// completion(true)
// })
// ], parseMarkdown: true), in: .window(.root))
// }))
// }
// actionSheet.setItemGroups([
// ActionSheetItemGroup(items: items),
// ActionSheetItemGroup(items: [
// ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// completion(false)
// })
// ])
// ])
self.present(alertScreen, in: .window(.root))
} else if peer.peerId == self.context.account.peerId {
self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [

View file

@ -76,17 +76,17 @@ public struct ItemListRevealOption: Equatable {
private let titleFont = Font.regular(11.0)
private let iconlessTitleFont = Font.regular(13.0)
private let optionSpacing: CGFloat = 10.0
private let optionEdgeInset: CGFloat = 10.0
private let optionTitleSpacing: CGFloat = 4.0
private let optionRevealStartOverlap: CGFloat = 12.0
private let optionRevealEndDistance: CGFloat = 10.0
private let optionExpandedActivationWidthFactor: CGFloat = 3.0
private let optionExpandedTransitionDistance: CGFloat = 16.0
private let optionPillTitleHorizontalPadding: CGFloat = 6.0
private let optionIconlessTitleAdditionalHorizontalPadding: CGFloat = 8.0
private let optionIconAnimationResponse: CGFloat = 18.0
private let optionIconAnimationSnapDistance: CGFloat = 0.5
private let spacing: CGFloat = 10.0
private let edgeInset: CGFloat = 10.0
private let ritleSpacing: CGFloat = 4.0
private let revealStartOverlap: CGFloat = 12.0
private let revealEndDistance: CGFloat = 10.0
private let expandedActivationWidthFactor: CGFloat = 3.0
private let expandedTransitionDistance: CGFloat = 16.0
private let iconlessTitleExpandedHorizontalPadding: CGFloat = 8.0
private let iconlessTitleHorizontalPadding: CGFloat = 8.0
private let iconAnimationResponse: CGFloat = 18.0
private let iconAnimationSnapDistance: CGFloat = 0.5
private extension ItemListRevealOptionIcon {
var hasVisualIcon: Bool {
@ -108,7 +108,7 @@ private struct ItemListRevealOptionLayoutMetrics {
let expandedIconInset: CGFloat
var contentHeight: CGFloat {
return self.shapeSize.height + optionTitleSpacing + ceil(titleFont.lineHeight)
return self.shapeSize.height + ritleSpacing + ceil(titleFont.lineHeight)
}
var slotShapeInset: CGFloat {
@ -118,7 +118,7 @@ private struct ItemListRevealOptionLayoutMetrics {
static func metrics(for height: CGFloat, hasVisualIcons: Bool) -> ItemListRevealOptionLayoutMetrics {
let regularShapeSize = CGSize(width: 50.0, height: 50.0)
let compactShapeSize = CGSize(width: 60.0, height: 32.0)
let regularContentHeight = regularShapeSize.height + optionTitleSpacing + ceil(titleFont.lineHeight)
let regularContentHeight = regularShapeSize.height + ritleSpacing + ceil(titleFont.lineHeight)
if height < regularContentHeight || !hasVisualIcons {
return ItemListRevealOptionLayoutMetrics(shapeSize: compactShapeSize, slotWidth: 70.0, titleWidth: 70.0, iconMaxSide: 24.0, cornerRadius: 16.0, expandedIconInset: 16.0)
} else {
@ -127,16 +127,16 @@ private struct ItemListRevealOptionLayoutMetrics {
}
func withGroupTitleWidth(_ maxTitleWidth: CGFloat) -> ItemListRevealOptionLayoutMetrics {
if maxTitleWidth <= self.shapeSize.width - optionPillTitleHorizontalPadding {
if maxTitleWidth <= self.shapeSize.width - iconlessTitleExpandedHorizontalPadding {
return self
}
let updatedShapeWidth = ceil(maxTitleWidth + optionPillTitleHorizontalPadding)
let updatedShapeWidth = ceil(maxTitleWidth + iconlessTitleExpandedHorizontalPadding)
let slotWidthDelta = self.slotWidth - self.shapeSize.width
return ItemListRevealOptionLayoutMetrics(
shapeSize: CGSize(width: updatedShapeWidth, height: self.shapeSize.height),
slotWidth: updatedShapeWidth + slotWidthDelta,
titleWidth: max(self.titleWidth, updatedShapeWidth - optionPillTitleHorizontalPadding),
titleWidth: max(self.titleWidth, updatedShapeWidth - iconlessTitleExpandedHorizontalPadding),
iconMaxSide: self.iconMaxSide,
cornerRadius: self.cornerRadius,
expandedIconInset: self.expandedIconInset
@ -147,7 +147,7 @@ private struct ItemListRevealOptionLayoutMetrics {
if count == 0 {
return 0.0
}
return optionEdgeInset * 2.0 + self.shapeSize.width * CGFloat(count) + optionSpacing * CGFloat(count - 1)
return edgeInset * 2.0 + self.shapeSize.width * CGFloat(count) + spacing * CGFloat(count - 1)
}
}
@ -174,10 +174,10 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
private var animationNodeOffset: CGFloat = 0.0
private var animationNodeFlip = false
private var iconAnimationLink: SharedDisplayLinkDriver.Link?
private weak var manuallyAnimatedIconNode: ASDisplayNode?
private var currentIconCenter: CGPoint?
private var targetIconCenter: CGPoint?
private var contentAnimationLink: SharedDisplayLinkDriver.Link?
private weak var manuallyAnimatedContentNode: ASDisplayNode?
private var currentContentCenter: CGPoint?
private var targetContentCenter: CGPoint?
private var didApplyLayout = false
var isExpanded: Bool = false
@ -189,7 +189,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
var titleWidthForGroupPillSizing: CGFloat {
var titleWidth = self.titleNode.updateLayout(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width
if self.displaysTitleInsidePill {
titleWidth += optionIconlessTitleAdditionalHorizontalPadding
titleWidth += iconlessTitleHorizontalPadding
}
return titleWidth
}
@ -264,7 +264,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
}
deinit {
self.stopManualIconAnimation()
self.stopManualContentAnimation()
}
func setHighlighted(_ highlighted: Bool) {
@ -280,81 +280,81 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
func resetAnimation() {
self.animationNode?.reset()
self.stopManualIconAnimation()
self.stopManualContentAnimation()
}
private func currentIconPresentationCenter(iconNode: ASDisplayNode) -> CGPoint {
return iconNode.layer.presentation()?.position ?? iconNode.position
private func currentContentPresentationCenter(contentNode: ASDisplayNode) -> CGPoint {
return contentNode.layer.presentation()?.position ?? contentNode.position
}
private func isManualIconAnimationAtTarget(center: CGPoint) -> Bool {
guard let targetIconCenter = self.targetIconCenter else {
private func isManualContentAnimationAtTarget(center: CGPoint) -> Bool {
guard let targetContentCenter = self.targetContentCenter else {
return true
}
let centerDeltaX = targetIconCenter.x - center.x
let centerDeltaY = targetIconCenter.y - center.y
let centerDeltaX = targetContentCenter.x - center.x
let centerDeltaY = targetContentCenter.y - center.y
let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY)
return centerDistance <= optionIconAnimationSnapDistance
return centerDistance <= iconAnimationSnapDistance
}
private func stopManualIconAnimation() {
self.iconAnimationLink?.isPaused = true
self.iconAnimationLink?.invalidate()
self.iconAnimationLink = nil
self.manuallyAnimatedIconNode = nil
self.currentIconCenter = nil
self.targetIconCenter = nil
private func stopManualContentAnimation() {
self.contentAnimationLink?.isPaused = true
self.contentAnimationLink?.invalidate()
self.contentAnimationLink = nil
self.manuallyAnimatedContentNode = nil
self.currentContentCenter = nil
self.targetContentCenter = nil
}
private func updateManualIconCenter(iconNode: ASDisplayNode, targetCenter: CGPoint, forceImmediate: Bool) {
iconNode.layer.removeAnimation(forKey: "position")
private func updateManualContentCenter(contentNode: ASDisplayNode, targetCenter: CGPoint, forceImmediate: Bool) {
contentNode.layer.removeAnimation(forKey: "position")
if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil {
self.currentIconCenter = self.currentIconPresentationCenter(iconNode: iconNode)
self.manuallyAnimatedIconNode = iconNode
if self.manuallyAnimatedContentNode !== contentNode || self.currentContentCenter == nil {
self.currentContentCenter = self.currentContentPresentationCenter(contentNode: contentNode)
self.manuallyAnimatedContentNode = contentNode
}
self.targetIconCenter = targetCenter
self.targetContentCenter = targetCenter
if forceImmediate {
iconNode.position = targetCenter
self.stopManualIconAnimation()
contentNode.position = targetCenter
self.stopManualContentAnimation()
return
}
if let currentIconCenter = self.currentIconCenter, self.isManualIconAnimationAtTarget(center: currentIconCenter) {
iconNode.position = targetCenter
self.stopManualIconAnimation()
if let currentContentCenter = self.currentContentCenter, self.isManualContentAnimationAtTarget(center: currentContentCenter) {
contentNode.position = targetCenter
self.stopManualContentAnimation()
return
}
if self.iconAnimationLink == nil {
self.iconAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
self?.tickManualIconAnimation(deltaTime: deltaTime)
if self.contentAnimationLink == nil {
self.contentAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
self?.tickManualContentAnimation(deltaTime: deltaTime)
})
self.iconAnimationLink?.isPaused = false
self.contentAnimationLink?.isPaused = false
}
}
private func tickManualIconAnimation(deltaTime: CGFloat) {
guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter else {
self.stopManualIconAnimation()
private func tickManualContentAnimation(deltaTime: CGFloat) {
guard let contentNode = self.manuallyAnimatedContentNode, let currentContentCenter = self.currentContentCenter, let targetContentCenter = self.targetContentCenter else {
self.stopManualContentAnimation()
return
}
let clampedDeltaTime = min(0.05, max(0.0, deltaTime))
let progress = 1.0 - exp(-clampedDeltaTime * optionIconAnimationResponse)
let progress = 1.0 - exp(-clampedDeltaTime * iconAnimationResponse)
let updatedCenter = CGPoint(
x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress,
y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress
x: currentContentCenter.x + (targetContentCenter.x - currentContentCenter.x) * progress,
y: currentContentCenter.y + (targetContentCenter.y - currentContentCenter.y) * progress
)
if self.isManualIconAnimationAtTarget(center: updatedCenter) {
iconNode.position = targetIconCenter
self.stopManualIconAnimation()
if self.isManualContentAnimationAtTarget(center: updatedCenter) {
contentNode.position = targetContentCenter
self.stopManualContentAnimation()
} else {
self.currentIconCenter = updatedCenter
iconNode.position = updatedCenter
self.currentContentCenter = updatedCenter
contentNode.position = updatedCenter
}
}
@ -375,7 +375,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
if self.displaysTitleInsidePill {
shapeY = floorToScreenPixels((bounds.height - pillSize.height) / 2.0)
} else {
let contentHeight = pillSize.height + optionTitleSpacing + titleSize.height
let contentHeight = pillSize.height + ritleSpacing + titleSize.height
shapeY = floorToScreenPixels((bounds.height - contentHeight) / 2.0)
}
@ -412,7 +412,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale))
let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0
var didApplyManualIconCenter = false
var didApplyManualContentCenter = false
let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
let iconCenterX: CGFloat
@ -440,15 +440,15 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + (6.0 + self.animationNodeOffset) * scaleFraction), size: imageSize)
if isPrimary {
didApplyManualIconCenter = true
didApplyManualContentCenter = true
transition.updateBounds(node: animationNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
let targetCenter = frameCenter(iconFrame)
if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne {
self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
} else if self.manuallyAnimatedIconNode === animationNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
self.updateManualContentCenter(contentNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
} else if self.manuallyAnimatedContentNode === animationNode && self.contentAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
self.updateManualContentCenter(contentNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
} else {
self.stopManualIconAnimation()
self.stopManualContentAnimation()
transition.updatePosition(node: animationNode, position: targetCenter)
}
} else {
@ -470,35 +470,63 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize)
if isPrimary {
didApplyManualIconCenter = true
didApplyManualContentCenter = true
transition.updateBounds(node: iconNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
let targetCenter = frameCenter(iconFrame)
if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne {
self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
} else if self.manuallyAnimatedIconNode === iconNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
self.updateManualContentCenter(contentNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
} else if self.manuallyAnimatedContentNode === iconNode && self.contentAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
self.updateManualContentCenter(contentNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
} else {
self.stopManualIconAnimation()
self.stopManualContentAnimation()
transition.updatePosition(node: iconNode, position: targetCenter)
}
} else {
transition.updateFrame(node: iconNode, frame: iconFrame)
}
}
if !didApplyManualIconCenter {
self.stopManualIconAnimation()
}
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
let titleFrame: CGRect
if self.displaysTitleInsidePill {
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - titleSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - titleSize.height / 2.0)), size: titleSize)
if isPrimary {
didApplyManualContentCenter = true
transition.updateBounds(node: self.titleNode, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
let titleCenterX: CGFloat
if expandedProgress > 0.0 {
let titleEdgeInset = max(metrics.expandedIconInset, titleSize.width / 2.0 + iconlessTitleExpandedHorizontalPadding)
let expandedTitleCenterX: CGFloat
if isLeft {
expandedTitleCenterX = backgroundFrame.maxX - titleEdgeInset
} else {
expandedTitleCenterX = backgroundFrame.minX + titleEdgeInset
}
titleCenterX = backgroundFrame.midX + (expandedTitleCenterX - backgroundFrame.midX) * expandedProgress
} else {
titleCenterX = backgroundFrame.midX
}
let targetCenter = CGPoint(x: titleCenterX, y: backgroundFrame.midY)
if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne {
self.updateManualContentCenter(contentNode: self.titleNode, targetCenter: targetCenter, forceImmediate: false)
} else if self.manuallyAnimatedContentNode === self.titleNode && self.contentAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
self.updateManualContentCenter(contentNode: self.titleNode, targetCenter: targetCenter, forceImmediate: false)
} else {
self.stopManualContentAnimation()
transition.updatePosition(node: self.titleNode, position: targetCenter)
}
} else {
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
} else {
let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize)
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + ritleSpacing), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
if !didApplyManualContentCenter {
self.stopManualContentAnimation()
}
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
@ -616,10 +644,10 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
let revealedDistance = abs(self.revealOffset)
let boundedRevealedDistance = min(revealedDistance, size.width)
let overswipeDistance = max(0.0, revealedDistance - size.width)
let overswipeProgress = clampToUnitInterval(overswipeDistance / optionExpandedTransitionDistance)
let expandedActivationDistance = 50.0 * (optionExpandedActivationWidthFactor - 1.0)
let overswipeProgress = clampToUnitInterval(overswipeDistance / expandedTransitionDistance)
let expandedActivationDistance = 50.0 * (expandedActivationWidthFactor - 1.0)
let primaryIndex = self.isLeft ? 0 : self.optionNodes.count - 1
let stride = metrics.shapeSize.width + optionSpacing
let stride = metrics.shapeSize.width + spacing
let clippingFrameX: CGFloat
if self.isLeft {
@ -657,17 +685,17 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
let revealProgress: CGFloat
if self.isLeft {
let baseCircleLeft = size.width - boundedRevealedDistance + self.sideInset + optionEdgeInset + CGFloat(i) * stride
let baseCircleLeft = size.width - boundedRevealedDistance + self.sideInset + edgeInset + CGFloat(i) * stride
baseCircleFrame = CGRect(origin: CGPoint(x: baseCircleLeft, y: 0.0), size: metrics.shapeSize)
let distanceFromShutterEdge = size.width - baseCircleFrame.maxX
revealProgress = clampToUnitInterval((distanceFromShutterEdge + optionRevealStartOverlap) / (optionRevealStartOverlap + optionRevealEndDistance))
revealProgress = clampToUnitInterval((distanceFromShutterEdge + revealStartOverlap) / (revealStartOverlap + revealEndDistance))
if isStretched {
let primaryLeft = size.width - boundedRevealedDistance + self.sideInset + optionEdgeInset
let primaryLeft = size.width - boundedRevealedDistance + self.sideInset + edgeInset
let primaryRight: CGFloat
if self.optionNodes.count > 1 {
let neighborLeft = primaryLeft + stride + overswipeDistance
primaryRight = max(primaryLeft + metrics.shapeSize.width, neighborLeft - optionSpacing)
primaryRight = max(primaryLeft + metrics.shapeSize.width, neighborLeft - spacing)
} else {
primaryRight = primaryLeft + metrics.shapeSize.width + overswipeDistance
}
@ -677,16 +705,16 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
nodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(circleLeft - metrics.slotShapeInset), y: 0.0), size: CGSize(width: metrics.slotWidth, height: size.height))
}
} else {
let baseCircleRight = revealedDistance + self.sideInset - optionEdgeInset - CGFloat(self.optionNodes.count - 1 - i) * stride
let baseCircleRight = revealedDistance + self.sideInset - edgeInset - CGFloat(self.optionNodes.count - 1 - i) * stride
baseCircleFrame = CGRect(origin: CGPoint(x: baseCircleRight - metrics.shapeSize.width, y: 0.0), size: metrics.shapeSize)
revealProgress = clampToUnitInterval((baseCircleFrame.minX + optionRevealStartOverlap) / (optionRevealStartOverlap + optionRevealEndDistance))
revealProgress = clampToUnitInterval((baseCircleFrame.minX + revealStartOverlap) / (revealStartOverlap + revealEndDistance))
if isStretched {
let primaryRight = revealedDistance + self.sideInset - optionEdgeInset
let primaryRight = revealedDistance + self.sideInset - edgeInset
let primaryLeft: CGFloat
if self.optionNodes.count > 1 {
let neighborRight = primaryRight - stride - overswipeDistance
primaryLeft = min(primaryRight - metrics.shapeSize.width, neighborRight + optionSpacing)
primaryLeft = min(primaryRight - metrics.shapeSize.width, neighborRight + spacing)
} else {
primaryLeft = primaryRight - metrics.shapeSize.width - overswipeDistance
}

View file

@ -737,6 +737,14 @@ NSString *suffix = @"";
[platform isEqualToString:@"iPad16,6"])
return @"iPad Pro 12.9 inch (7th gen)";
if ([platform isEqualToString:@"iPad16,8"] ||
[platform isEqualToString:@"iPad16,9"])
return @"iPad Air 11 inch (8th gen)";
if ([platform isEqualToString:@"iPad16,10"] ||
[platform isEqualToString:@"iPad16,11"])
return @"iPad Air 13 inch (8th gen)";
if ([platform hasPrefix:@"iPhone"])
return @"Unknown iPhone";
if ([platform hasPrefix:@"iPod"])

View file

@ -760,7 +760,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
var canTransfer = false
var canDismiss = false
let canEditProcessJoinRequests: Bool
if case let .user(user) = admin, user.botInfo?.flags.contains(.isGuardBot) == true {
if !isChannel, case let .user(user) = admin, user.botInfo?.flags.contains(.isGuardBot) == true {
canEditProcessJoinRequests = canEdit && user.id != accountPeerId
} else {
canEditProcessJoinRequests = false

View file

@ -13,6 +13,15 @@ private enum RevealOptionKey: Int32 {
case delete
}
private func webBrowserDomainExceptionPlaceholderLetter(_ title: String) -> String {
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
if let firstCharacter = trimmedTitle.first {
return String(firstCharacter).uppercased()
} else {
return "#"
}
}
final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
@ -92,6 +101,8 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
private let maskNode: ASImageNode
let iconNode: TransformImageNode
private let iconPlaceholderNode: ASDisplayNode
private let iconPlaceholderTextNode: TextNode
let titleNode: TextNode
let labelNode: TextNode
@ -126,6 +137,15 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.iconPlaceholderNode = ASDisplayNode()
self.iconPlaceholderNode.clipsToBounds = true
self.iconPlaceholderNode.cornerRadius = 7.0
self.iconPlaceholderTextNode = TextNode()
self.iconPlaceholderTextNode.isUserInteractionEnabled = false
self.iconPlaceholderTextNode.contentMode = .center
self.iconPlaceholderTextNode.contentsScale = UIScreen.main.scale
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
@ -136,6 +156,8 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.iconPlaceholderNode)
self.iconPlaceholderNode.addSubnode(self.iconPlaceholderTextNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
@ -150,6 +172,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeIconPlaceholderTextLayout = TextNode.asyncLayout(self.iconPlaceholderTextNode)
let currentItem = self.item
@ -163,6 +186,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let iconSize = CGSize(width: 30.0, height: 30.0)
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
@ -181,6 +205,10 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let iconPlaceholderText = webBrowserDomainExceptionPlaceholderLetter(item.title)
let iconPlaceholderFont = Font.with(size: 17.0, design: .round, weight: .semibold)
let (iconPlaceholderTextLayout, iconPlaceholderTextApply) = makeIconPlaceholderTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: iconPlaceholderText, font: iconPlaceholderFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: iconSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
@ -220,12 +248,13 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.iconPlaceholderNode.backgroundColor = item.presentationData.theme.list.mediaPlaceholderColor
}
let iconSize = CGSize(width: 30.0, height: 30.0)
if currentItem?.favicon != item.favicon {
strongSelf.currentIconFile = nil
strongSelf.iconDisposable.set(nil)
strongSelf.iconNode.reset()
if let favicon = item.favicon {
strongSelf.iconDisposable.set((item.context.engine.stickers.resolveInlineStickers(fileIds: [favicon])
@ -244,6 +273,8 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
strongSelf.iconNode.setSignal(chatMessageSticker(account: item.context.account, userLocation: .other, file: file, small: false, fetched: true))
}
strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: resolvedImageSize, boundingSize: iconSize, intrinsicInsets: .zero))()
strongSelf.iconPlaceholderNode.isHidden = true
strongSelf.iconNode.isHidden = false
}))
}
}
@ -251,9 +282,11 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
if strongSelf.currentIconFile?.fileId.id == item.favicon, let dimensions = strongSelf.currentIconFile?.dimensions?.cgSize {
imageSize = dimensions.aspectFilled(imageSize)
}
let hasResolvedIcon = item.favicon != nil && strongSelf.currentIconFile?.fileId.id == item.favicon
let _ = titleApply()
let _ = labelApply()
let _ = iconPlaceholderTextApply()
switch item.style {
case .plain:
@ -324,6 +357,10 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
strongSelf.labelNode.frame = labelFrame
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 16.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
strongSelf.iconPlaceholderNode.frame = iconFrame
strongSelf.iconPlaceholderTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((iconSize.width - iconPlaceholderTextLayout.size.width) / 2.0) + UIScreenPixel, y: floorToScreenPixels((iconSize.height - iconPlaceholderTextLayout.size.height) / 2.0) + 1.0), size: iconPlaceholderTextLayout.size)
strongSelf.iconPlaceholderNode.isHidden = hasResolvedIcon
strongSelf.iconNode.isHidden = !hasResolvedIcon
strongSelf.iconNode.frame = iconFrame
strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))()
@ -360,6 +397,10 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It
iconFrame.origin.x = params.leftInset + 16.0 + offset
transition.updateFrame(node: self.iconNode, frame: iconFrame)
var iconPlaceholderFrame = self.iconPlaceholderNode.frame
iconPlaceholderFrame.origin.x = params.leftInset + 16.0 + offset
transition.updateFrame(node: self.iconPlaceholderNode, frame: iconPlaceholderFrame)
var titleFrame = self.titleNode.frame
titleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.titleNode, frame: titleFrame)

View file

@ -37,6 +37,7 @@ import EdgeEffect
import LocationUI
import CountrySelectionUI
import ShareWithPeersScreen
import ChatTextLinkEditUI
public final class ComposedPoll {
public struct Text {
@ -918,6 +919,11 @@ final class ComposePollScreenComponent: Component {
self.deactivateInput()
if !replace, case .pollOption = subject, let webpage = self.attachedMedia(for: subject)?.media.media as? TelegramMediaWebpage, let link = webpage.content.url {
self.openAttachedLinkMedia(subject: subject, link: link)
return
}
guard replace || !self.openAttachMediaContextMenu(subject: subject) else {
return
}
@ -967,6 +973,38 @@ final class ComposePollScreenComponent: Component {
})
}
private func openAttachedLinkMedia(subject: MediaAttachSubject, link: String) {
guard let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let controller = chatTextLinkEditController(
context: component.context,
updatedPresentationData: (presentationData, .never()),
text: presentationData.strings.CreatePoll_Link_Description,
link: link,
preview: true,
apply: { [weak self] link, webpage in
guard let self, let link else {
return
}
if link.isEmpty {
self.setAttachedMedia(nil, for: subject)
self.state?.updated(transition: .easeInOut(duration: 0.2))
return
}
let attachedMedia = AttachedMedia(media: .standalone(media: webpage ?? makePollAttachmentLinkWebpage(link: link)))
self.setAttachedMedia(attachedMedia, for: subject)
self.uploadAttachedMediaIfNeeded(attachedMedia)
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
)
(self.environment?.controller() as? ComposePollScreen)?.parentController()?.present(controller, in: .window(.root))
}
private func uploadAttachedMediaIfNeeded(_ media: AttachedMedia) {
guard let component = self.component, media.requiresUpload, media.uploadDisposable == nil else {
return

View file

@ -23,6 +23,34 @@ public enum PollAttachmentSubject {
case option
}
func makePollAttachmentLinkWebpage(link: String) -> TelegramMediaWebpage {
let mediaId = Int64.random(in: Int64.min ... Int64.max)
return TelegramMediaWebpage(
webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: mediaId),
content: .Loaded(TelegramMediaWebpageLoadedContent(
url: link,
displayUrl: link,
hash: 0,
type: nil,
websiteName: nil,
title: nil,
text: nil,
embedUrl: nil,
embedType: nil,
embedSize: nil,
duration: nil,
author: nil,
isMediaLargeByDefault: nil,
imageIsVideoCover: false,
image: nil,
file: nil,
story: nil,
attributes: [],
instantPage: nil
))
)
}
public func presentPollAttachmentScreen(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
@ -236,11 +264,11 @@ public func presentPollAttachmentScreen(
case .link:
attachmentController?.dismiss(animated: true)
//TODO:localize
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let controller = chatTextLinkEditController(
context: context,
updatedPresentationData: updatedPresentationData,
text: "Attach a link to this option.",
text: presentationData.strings.CreatePoll_Link_Description,
link: nil,
preview: true,
apply: { link, webpage in
@ -251,31 +279,7 @@ public func presentPollAttachmentScreen(
completion(.standalone(media: webpage))
return
}
let mediaId = Int64.random(in: Int64.min ... Int64.max)
let webPage = TelegramMediaWebpage(
webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: mediaId),
content: .Loaded(TelegramMediaWebpageLoadedContent(
url: link,
displayUrl: link,
hash: 0,
type: nil,
websiteName: nil,
title: nil,
text: nil,
embedUrl: nil,
embedType: nil,
embedSize: nil,
duration: nil,
author: nil,
isMediaLargeByDefault: nil,
imageIsVideoCover: false,
image: nil,
file: nil,
story: nil,
attributes: [],
instantPage: nil
)))
completion(.standalone(media: webPage))
completion(.standalone(media: makePollAttachmentLinkWebpage(link: link)))
}
)
present(controller, false)

View file

@ -125,9 +125,8 @@ public func openUserGeneratedUrl(
let disposable = MetaDisposable()
let checkState = concealedAlertOption.map { _ in AlertCheckComponent.ExternalState() }
//TODO:localize
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(AlertTitleComponent(title: "Open link?"))))
content.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(AlertTitleComponent(title: presentationData.strings.OpenLinkConfirmation_Title))))
content.append(AnyComponentWithIdentity(id: "text", component: AnyComponent(AlertTextComponent(
content: .plain(displayUrl),
alignment: .center,
@ -158,7 +157,7 @@ public func openUserGeneratedUrl(
content: content,
actions: [
.init(title: presentationData.strings.Common_Cancel),
.init(title: "Open", type: .default, action: {
.init(title: presentationData.strings.OpenLinkConfirmation_Open, type: .default, action: {
if checkState?.value == true {
concealedAlertOption?.action()
}

View file

@ -250,8 +250,7 @@ extension ChatControllerImpl {
}))
if let openMode {
//TODO:localize
let reverseText = openMode.shouldOpenInApp ? "Open in Browser" : "Open In-App"
let reverseText = openMode.shouldOpenInApp ? self.presentationData.strings.Chat_ContextMenu_OpenInBrowser : self.presentationData.strings.Chat_ContextMenu_OpenInApp
items.append(ActionSheetButtonItem(title: reverseText, color: .accent, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self else {