Various improvements

This commit is contained in:
Ilya Laktyushin 2026-01-27 22:24:31 +04:00
parent 8be8491b84
commit 142d59cf8f
55 changed files with 1851 additions and 1293 deletions

View file

@ -15669,3 +15669,7 @@ Error: %8$@";
"Conversation.Summary.Limit.Title" = "AI Summary";
"Conversation.Summary.Limit.Text" = "Summarize large messages with AI unlimited with Telegram Premium.";
"Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter";
"Notification.StarsGift.Crafted" = "You crafted a new unique collectible";

View file

@ -1294,6 +1294,7 @@ public protocol SharedAccountContext: AnyObject {
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
var currentStickerSettings: Atomic<StickerSettings> { get }
var currentMediaDisplaySettings: Atomic<MediaDisplaySettings> { get }
var currentChatSettings: Atomic<ChatSettings> { get }
var energyUsageSettings: EnergyUsageSettings { get }

View file

@ -1489,7 +1489,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
public func chatInputTextNodeShouldReturn() -> Bool {
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
self.sendButtonPressed()
}
@ -1497,7 +1497,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
@objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldReturn()
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
}
private func applyUpdateSendButtonIcon() {

View file

@ -530,6 +530,11 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let view = leftItemView.view {
if view.superview == nil {
self.navigationBarContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
leftItemTransition.setFrame(view: view, frame: leftItemFrame)
}
@ -587,17 +592,19 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
}
if let bottomItem = component.bottomItem {
var bottomItemTransition = transition
let bottomItemView: ComponentView<Empty>
if let current = self.bottomItemView {
bottomItemView = current
} else {
bottomItemTransition = .immediate
bottomItemView = ComponentView<Empty>()
self.bottomItemView = bottomItemView
}
let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let bottomItemSize = bottomItemView.update(
transition: transition,
transition: bottomItemTransition,
component: bottomItem,
environment: {},
containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0)
@ -606,12 +613,24 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let view = bottomItemView.view {
if view.superview == nil {
self.bottomContainer.addSubview(view)
if !transition.animation.isImmediate {
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
transition.setFrame(view: view, frame: bottomItemFrame)
bottomItemTransition.setFrame(view: view, frame: bottomItemFrame)
}
} else if let bottomItemView = self.bottomItemView {
self.bottomItemView = nil
bottomItemView.view?.removeFromSuperview()
if !transition.animation.isImmediate {
bottomItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
bottomItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
bottomItemView.view?.removeFromSuperview()
})
} else {
bottomItemView.view?.removeFromSuperview()
}
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - edgeEffectHeight), size: CGSize(width: fillingSize, height: edgeEffectHeight))

View file

@ -89,6 +89,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem {
public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
@ -107,7 +108,10 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -132,6 +136,20 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
self.addSubnode(self.activateArea)
}
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -211,6 +229,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor
}
let _ = titleApply()
@ -273,6 +292,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))

View file

@ -139,6 +139,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerSelectionScreen",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/Utils/DeviceModel",
],
visibility = [
"//visibility:public",

View file

@ -224,7 +224,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in
pushControllerImpl?(controller)
presentControllerImpl?(controller, nil)
}, dismissInput: {}, contentContext: nil, progress: nil, completion: nil)
})
}

View file

@ -623,15 +623,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
let removeSessionImpl: (Int64, @escaping () -> Void) -> Void = { sessionId, completion in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateSessionText),
ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: {
dismissAction()
let controller = textAlertController(
context: context,
title: nil,
text: presentationData.strings.AuthSessions_TerminateSessionText,
actions: [
TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateSession, action: {
completion()
updateState {
@ -649,11 +646,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
}
context.sharedContext.updateNotificationTokensRegistration()
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical
)
presentControllerImpl?(controller, nil)
}
let removeWebSessionImpl: (Int64) -> Void = { sessionId in
@ -690,16 +688,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
removeSessionImpl(sessionId, {})
}, terminateOtherSessions: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateOtherSessionsText),
ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateOtherSessions, color: .destructive, action: {
dismissAction()
let controller = textAlertController(
context: context,
title: nil,
text: presentationData.strings.AuthSessions_TerminateOtherSessionsText,
actions: [
TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateOtherSessions, action: {
updateState {
return $0.withUpdatedTerminatingOtherSessions(true)
}
@ -715,11 +709,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
}
context.sharedContext.updateNotificationTokensRegistration()
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical
)
presentControllerImpl?(controller, nil)
}, openSession: { session in
let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in
updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start())

View file

@ -30,6 +30,7 @@ import ContextUI
import QuickReactionSetupController
import AvatarEditorScreen
import PeerSelectionScreen
import DeviceModel
enum SettingsSearchableItemIcon {
case profile
@ -3895,7 +3896,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag))
}
return [
var items: [SettingsSearchableItem] = [
SettingsSearchableItem(
id: "appearance",
title: strings.Settings_Appearance,
@ -4094,14 +4095,31 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear
),
SettingsSearchableItem(
id: "appearance/tap-for-next-media",
title: strings.Appearance_ShowNextMediaOnTap,
icon: icon,
breadcrumbs: [strings.Settings_Appearance],
isVisible: false,
present: { context, _, present in
presentAppearanceSettings(context, present, .tapForNextMedia)
}
),
)
]
if DeviceModel.current.isIpad {
items.append(
SettingsSearchableItem(
id: "appearance/send-with-cmd-enter",
title: strings.Appearance_SendWithCmdEnter,
icon: icon,
breadcrumbs: [strings.Settings_Appearance],
isVisible: false,
present: { context, _, present in
presentAppearanceSettings(context, present, .sendWithCmdEnter)
}
)
)
}
return items
}
private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] {

View file

@ -12,12 +12,7 @@ import AppBundle
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.fillEllipse(in: bounds)
context.setBlendMode(.normal)
context.clear(bounds)
let lineWidth: CGFloat
if selected {
@ -113,8 +108,10 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode {
override init() {
self.iconNode = ASImageNode()
self.iconNode.clipsToBounds = true
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
self.iconNode.isLayerBacked = true
self.iconNode.cornerRadius = 15.0
self.overlayNode = ASImageNode()
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
@ -199,6 +196,7 @@ private let selectedTextFont = Font.medium(12.0)
class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
@ -219,6 +217,9 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -234,6 +235,20 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
self.addSubnode(self.containerNode)
}
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
@ -280,6 +295,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -321,6 +337,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))

View file

@ -38,13 +38,34 @@ private final class ThemeSettingsControllerArguments {
let openBubbleSettings: () -> Void
let openPowerSavingSettings: () -> Void
let openStickersAndEmoji: () -> Void
let toggleSendWithCmdEnter: (Bool) -> Void
let toggleShowNextMediaOnTap: (Bool) -> Void
let selectAppIcon: (PresentationAppIcon) -> Void
let editTheme: (PresentationCloudTheme) -> Void
let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void
let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, openNameColorSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) {
init(
context: AccountContext,
selectTheme: @escaping (PresentationThemeReference) -> Void,
openThemeSettings: @escaping () -> Void,
openWallpaperSettings: @escaping () -> Void,
openNameColorSettings: @escaping () -> Void,
selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void,
openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void,
toggleNightTheme: @escaping (Bool) -> Void,
openAutoNightTheme: @escaping () -> Void,
openTextSize: @escaping () -> Void,
openBubbleSettings: @escaping () -> Void,
openPowerSavingSettings: @escaping () -> Void,
openStickersAndEmoji: @escaping () -> Void,
toggleSendWithCmdEnter: @escaping (Bool) -> Void,
toggleShowNextMediaOnTap: @escaping (Bool) -> Void,
selectAppIcon: @escaping (PresentationAppIcon) -> Void,
editTheme: @escaping (PresentationCloudTheme) -> Void,
themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void,
colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
) {
self.context = context
self.selectTheme = selectTheme
self.openThemeSettings = openThemeSettings
@ -58,6 +79,7 @@ private final class ThemeSettingsControllerArguments {
self.openBubbleSettings = openBubbleSettings
self.openPowerSavingSettings = openPowerSavingSettings
self.openStickersAndEmoji = openStickersAndEmoji
self.toggleSendWithCmdEnter = toggleSendWithCmdEnter
self.toggleShowNextMediaOnTap = toggleShowNextMediaOnTap
self.selectAppIcon = selectAppIcon
self.editTheme = editTheme
@ -84,6 +106,7 @@ public enum ThemeSettingsEntryTag: ItemListItemTag {
case powerSaving
case stickersAndEmoji
case animations
case sendWithCmdEnter
case tapForNextMedia
case nightMode
@ -112,6 +135,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
case powerSaving
case stickersAndEmoji
case otherHeader(PresentationTheme, String)
case sendWithCmdEnter(PresentationTheme, String, Bool)
case showNextMediaOnTap(PresentationTheme, String, Bool)
case showNextMediaOnTapInfo(PresentationTheme, String)
@ -127,7 +151,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
return ThemeSettingsControllerSection.icon.rawValue
case .powerSaving, .stickersAndEmoji:
return ThemeSettingsControllerSection.message.rawValue
case .otherHeader, .showNextMediaOnTap, .showNextMediaOnTapInfo:
case .otherHeader, .sendWithCmdEnter, .showNextMediaOnTap, .showNextMediaOnTapInfo:
return ThemeSettingsControllerSection.other.rawValue
}
}
@ -164,10 +188,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
return 13
case .otherHeader:
return 14
case .showNextMediaOnTap:
case .sendWithCmdEnter:
return 15
case .showNextMediaOnTapInfo:
case .showNextMediaOnTap:
return 16
case .showNextMediaOnTapInfo:
return 17
}
}
@ -263,6 +289,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} else {
return false
}
case let .sendWithCmdEnter(lhsTheme, lhsTitle, lhsValue):
if case let .sendWithCmdEnter(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .showNextMediaOnTap(lhsTheme, lhsTitle, lhsValue):
if case let .showNextMediaOnTap(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
@ -351,6 +383,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
})
case let .otherHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .sendWithCmdEnter(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleSendWithCmdEnter(value)
}, tag: ThemeSettingsEntryTag.sendWithCmdEnter)
case let .showNextMediaOnTap(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleShowNextMediaOnTap(value)
@ -361,7 +397,21 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
}
}
private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?, nameColors: PeerNameColors) -> [ThemeSettingsControllerEntry] {
private func themeSettingsControllerEntries(
presentationData: PresentationData,
presentationThemeSettings: PresentationThemeSettings,
chatSettings: ChatSettings,
mediaSettings: MediaDisplaySettings,
themeReference: PresentationThemeReference,
availableThemes: [PresentationThemeReference],
availableAppIcons: [PresentationAppIcon],
currentAppIconName: String?,
isPremium: Bool,
chatThemes: [PresentationThemeReference],
animatedEmojiStickers: [String: [StickerPackItem]],
accountPeer: EnginePeer?,
nameColors: PeerNameColors
) -> [ThemeSettingsControllerEntry] {
var entries: [ThemeSettingsControllerEntry] = []
let strings = presentationData.strings
@ -437,6 +487,7 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
}
entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased()))
entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter))
entries.append(.showNextMediaOnTap(presentationData.theme, strings.Appearance_ShowNextMediaOnTap, mediaSettings.showNextMediaOnTap))
entries.append(.showNextMediaOnTapInfo(presentationData.theme, strings.Appearance_ShowNextMediaOnTapInfo))
@ -557,6 +608,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
pushControllerImpl?(installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in
}))
})
}, toggleSendWithCmdEnter: { value in
let _ = updateChatSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedSendWithCmdEnter(value)
}).start()
}, toggleShowNextMediaOnTap: { value in
let _ = updateMediaDisplaySettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedShowNextMediaOnTap(value)
@ -1035,9 +1090,26 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
})
})
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)))
let signal = combineLatest(
queue: .mainQueue(),
context.sharedContext.presentationData,
context.sharedContext.accountManager.sharedData(keys: [
ApplicationSpecificSharedDataKeys.presentationThemeSettings,
ApplicationSpecificSharedDataKeys.chatSettings,
ApplicationSpecificSharedDataKeys.mediaDisplaySettings,
SharedDataKeys.chatThemes
]),
cloudThemes.get(),
availableAppIcons,
currentAppIconName.get(),
removedThemeIndexesPromise.get(),
animatedEmojiStickers,
context.account.postbox.peerView(id: context.account.peerId),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let chatSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) ?? ChatSettings.defaultSettings
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
@ -1077,7 +1149,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
chatThemes.insert(.builtin(.dayClassic), at: 0)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, chatSettings: chatSettings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}

View file

@ -289,9 +289,9 @@ private final class StickerPackContainer: ASDisplayNode {
self.addSubnode(self.gridNode)
self.titleContainer.addSubnode(self.titleNode)
self.addSubnode(self.topContainerNode)
self.addSubnode(self.titleContainer)
self.addSubnode(self.topContainerNode)
self.addSubnode(self.bottomContainerNode)
self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
@ -654,6 +654,13 @@ private final class StickerPackContainer: ASDisplayNode {
self.reorderingGestureRecognizer = reorderingGestureRecognizer
self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer)
self.gridNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.gridNode.clipsToBounds = true
self.gridNode.layer.cornerRadius = 40.0
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
self.topContainerNode.view.addSubview(self.topEdgeEffectView)
self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView)
}
@ -1891,7 +1898,7 @@ private final class StickerPackContainer: ASDisplayNode {
transition.updateFrame(node: self.bottomContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: 90.0)))
let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset))
let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top))
let itemsPerRow = 5
let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
@ -1923,12 +1930,13 @@ private final class StickerPackContainer: ASDisplayNode {
let initialRevealedRowCount: CGFloat = 4.5
let topInset: CGFloat
var topInset: CGFloat
if case .regular = layout.metrics.widthClass {
topInset = 0.0
topInset = titleAreaInset
} else {
topInset = insets.top + max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset)
}
let additionalGridBottomInset = max(0.0, gridFrame.size.height - actionAreaHeight - contentHeight)
let gridInsets = UIEdgeInsets(top: topInset, left: gridLeftInset, bottom: actionAreaHeight + additionalGridBottomInset, right: layout.size.width - fillingWidth - gridLeftInset)
@ -2112,6 +2120,10 @@ private final class StickerPackContainer: ASDisplayNode {
buttonView.frame = buttonFrame
}
let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 90.0))
transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame)
self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0))
transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
@ -2133,7 +2145,7 @@ private final class StickerPackContainer: ASDisplayNode {
return
}
let minBackgroundY = gridFrame.minY - titleAreaInset
let minBackgroundY = gridFrame.minY
let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset
let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top
@ -2173,7 +2185,7 @@ private final class StickerPackContainer: ASDisplayNode {
var titleContainerFrame: CGRect
if case .regular = layout.metrics.widthClass {
backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y)
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize())
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0) + 10.0), size: CGSize())
} else {
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0) + 10.0), size: CGSize())
}
@ -3103,12 +3115,12 @@ private final class StickerPackContextReferenceContentSource: ContextReferenceCo
private func generateShadowImage() -> UIImage? {
return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in
return generateImage(CGSize(width: 220.0, height: 220.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.saveGState()
context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)
let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath
let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 100.0, height: 100.0), cornerRadius: 40.0).cgPath
context.addPath(path)
context.fillPath()
@ -3117,7 +3129,7 @@ private func generateShadowImage() -> UIImage? {
context.setBlendMode(.clear)
context.addPath(path)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70)
})?.stretchableImage(withLeftCapWidth: 110, topCapHeight: 110)
}
private func generateArrowImage(color: UIColor) -> UIImage? {
@ -3290,38 +3302,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
}
}
private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? {
return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
// context.saveGState()
context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)
var rectCorners: UIRectCorner = []
if corners.contains(.layerMinXMinYCorner) {
rectCorners.insert(.topLeft)
}
if corners.contains(.layerMaxXMinYCorner) {
rectCorners.insert(.topRight)
}
if corners.contains(.layerMinXMaxYCorner) {
rectCorners.insert(.bottomLeft)
}
if corners.contains(.layerMaxXMaxYCorner) {
rectCorners.insert(.bottomRight)
}
let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath
context.addPath(path)
context.fillPath()
// context.restoreGState()
// context.setBlendMode(.clear)
// context.addPath(path)
// context.fillPath()
})?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
}
private final class CopyView: UIView {
let shadow: UIImageView
var snapshotView: UIView?

View file

@ -248,7 +248,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt))
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt, isCrafted: (flags & (1 << 16)) != 0))
case let .messageActionPaidMessagesRefunded(messageActionPaidMessagesRefundedData):
let (count, stars) = (messageActionPaidMessagesRefundedData.count, messageActionPaidMessagesRefundedData.stars)
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))

View file

@ -256,7 +256,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?, number: Int32?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?, isCrafted: Bool)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
case conferenceCall(ConferenceCall)
@ -404,7 +404,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") {
resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars)
}
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt"))
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt"), isCrafted: decoder.decodeBoolForKey("isCrafted", orElse: false))
case 46:
self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 47:
@ -803,7 +803,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "number")
}
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt):
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt, isCrafted):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
encoder.encodeBool(isUpgrade, forKey: "isUpgrade")
@ -864,6 +864,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "canCraftAt")
}
encoder.encodeBool(isCrafted, forKey: "isCrafted")
case let .paidMessagesRefunded(count, stars):
encoder.encodeInt32(46, forKey: "_rawValue")
encoder.encodeInt32(count, forKey: "count")
@ -971,7 +973,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(toPeerId)
}
return peerIds
case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)

View file

@ -1524,7 +1524,7 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var media = currentMessage.media
if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift {
if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
media = [
TelegramMediaAction(
@ -1546,7 +1546,8 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift
dropOriginalDetailsStars: nil,
assigned: assigned,
fromOffer: fromOffer,
canCraftAt: canCraftAt
canCraftAt: canCraftAt,
isCrafted: isCrafted
)
)
]
@ -1653,7 +1654,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
let message = updateNewMessageData.message
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -3186,12 +3187,6 @@ private final class CraftGiftsContextImpl {
return _internal_craftStarGift(account: self.account, references: references)
}
func addGift(gift: ProfileGiftsContext.State.StarGift) {
self.gifts.insert(gift, at: 0)
self.count = self.count + 1
self.pushState()
}
func removeGifts(references: [StarGiftReference]) {
let referencesSet = Set(references)
self.gifts.removeAll { gift in
@ -3325,12 +3320,6 @@ public final class CraftGiftsContext {
}
}
public func addGift(gift: ProfileGiftsContext.State.StarGift) {
self.impl.with { impl in
impl.addGift(gift: gift)
}
}
public func removeGifts(references: [StarGiftReference]) {
self.impl.with { impl in
impl.removeGifts(references: references)
@ -3395,7 +3384,7 @@ func _internal_craftStarGift(account: Account, references: [StarGiftReference])
let message = updateNewMessageData.message
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)

View file

@ -1658,7 +1658,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid:
receiptMessageId = nil
}
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id {
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)

View file

@ -217,9 +217,10 @@ public final class InitialPresentationDataAndSettings {
public let mediaInputSettings: MediaInputSettings
public let mediaDisplaySettings: MediaDisplaySettings
public let stickerSettings: StickerSettings
public let chatSettings: ChatSettings
public let experimentalUISettings: ExperimentalUISettings
public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, experimentalUISettings: ExperimentalUISettings) {
public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, chatSettings: ChatSettings, experimentalUISettings: ExperimentalUISettings) {
self.presentationData = presentationData
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
self.autodownloadSettings = autodownloadSettings
@ -228,6 +229,7 @@ public final class InitialPresentationDataAndSettings {
self.mediaInputSettings = mediaInputSettings
self.mediaDisplaySettings = mediaDisplaySettings
self.stickerSettings = stickerSettings
self.chatSettings = chatSettings
self.experimentalUISettings = experimentalUISettings
}
}
@ -245,6 +247,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
var experimentalUISettings: PreferencesEntry?
var contactSynchronizationSettings: PreferencesEntry?
var stickerSettings: PreferencesEntry?
var chatSettings: PreferencesEntry?
init(
localizationSettings: PreferencesEntry?,
@ -257,7 +260,8 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
mediaDisplaySettings: PreferencesEntry?,
experimentalUISettings: PreferencesEntry?,
contactSynchronizationSettings: PreferencesEntry?,
stickerSettings: PreferencesEntry?
stickerSettings: PreferencesEntry?,
chatSettings: PreferencesEntry?
) {
self.localizationSettings = localizationSettings
self.presentationThemeSettings = presentationThemeSettings
@ -270,6 +274,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
self.experimentalUISettings = experimentalUISettings
self.contactSynchronizationSettings = contactSynchronizationSettings
self.stickerSettings = stickerSettings
self.chatSettings = chatSettings
}
}
@ -285,6 +290,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
let experimentalUISettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings)
let contactSynchronizationSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.contactSynchronizationSettings)
let stickerSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings)
let chatSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.chatSettings)
return InternalData(
localizationSettings: localizationSettings,
@ -297,7 +303,8 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
mediaDisplaySettings: mediaDisplaySettings,
experimentalUISettings: experimentalUISettings,
contactSynchronizationSettings: contactSynchronizationSettings,
stickerSettings: stickerSettings
stickerSettings: stickerSettings,
chatSettings: chatSettings
)
}
|> deliverOn(Queue(name: "PresentationData-Load", qos: .userInteractive))
@ -365,6 +372,13 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
stickerSettings = StickerSettings.defaultSettings
}
let chatSettings: ChatSettings
if let value = internalData.chatSettings?.get(ChatSettings.self) {
chatSettings = value
} else {
chatSettings = ChatSettings.defaultSettings
}
let experimentalUISettings: ExperimentalUISettings = internalData.experimentalUISettings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
let contactSettings: ContactSynchronizationSettings = internalData.contactSynchronizationSettings?.get(ContactSynchronizationSettings.self) ?? ContactSynchronizationSettings.defaultSettings
@ -413,7 +427,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager<Te
let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(themeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(themeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: themeSettings.chatBubbleSettings.mergeBubbleCorners)
return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: theme, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, reduceMotion: themeSettings.reduceMotion, largeEmoji: themeSettings.largeEmoji), automaticMediaDownloadSettings: automaticMediaDownloadSettings, autodownloadSettings: autodownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, mediaDisplaySettings: mediaDisplaySettings, stickerSettings: stickerSettings, experimentalUISettings: experimentalUISettings)
return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: theme, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, reduceMotion: themeSettings.reduceMotion, largeEmoji: themeSettings.largeEmoji), automaticMediaDownloadSettings: automaticMediaDownloadSettings, autodownloadSettings: autodownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, mediaDisplaySettings: mediaDisplaySettings, stickerSettings: stickerSettings, chatSettings: chatSettings, experimentalUISettings: experimentalUISettings)
}
}

View file

@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer, _, isCrafted):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
@ -1338,7 +1338,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtYou(giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
}
} else {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor)
if isCrafted {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_Crafted, font: titleFont, textColor: primaryTextColor)
} else {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor)
}
}
} else if let senderId, let peer = message.peers[senderId] {
if let peerId, let targetPeer = message.peers[peerId] {

View file

@ -51,6 +51,8 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
private let context: AccountContext
private let subject: MessageActionUrlAuthResult
var peer: EnginePeer?
fileprivate var inProgress = false
var allowWrite = true
weak var controller: ViewController?
@ -58,8 +60,17 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
init(context: AccountContext, subject: MessageActionUrlAuthResult) {
self.context = context
self.subject = subject
super.init()
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.peer = peer
self.updated()
})
}
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
@ -99,13 +110,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let peerButton = Child(AvatarComponent.self)
let avatar = Child(AvatarComponent.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let clientSection = Child(ListSectionComponent.self)
let optionsSection = Child(ListSectionComponent.self)
let cancelButton = Child(ButtonComponent.self)
let doneButton = Child(ButtonComponent.self)
@ -147,6 +157,24 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
if let peer = state.peer {
let peerButton = peerButton.update(
component: AvatarComponent(
context: component.context,
theme: environment.theme,
peer: peer
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(peerButton
.position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0))
)
}
var contentHeight: CGFloat = 32.0
let avatar = avatar.update(
@ -320,7 +348,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
contentHeight += clientSection.size.height
if flags.contains(.requestWriteAccess) {
contentHeight += 22.0
contentHeight += 38.0
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent(
@ -441,10 +469,6 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
contentHeight += doneButton.size.height
contentHeight += buttonInsets.bottom
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}

View file

@ -18,7 +18,7 @@
@property (nonatomic, copy) bool (^ _Nullable shouldPaste)();
@property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable);
@property (nonatomic, copy) ChatInputTextViewImplTargetForAction * _Nullable (^ _Nullable targetForAction)(SEL _Nullable);
@property (nonatomic, copy) bool (^ _Nullable shouldReturn)();
@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(UIKeyModifierFlags);
@property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)();
@property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)();

View file

@ -136,15 +136,22 @@
}
- (NSArray *)keyCommands {
UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)];
UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handleReturn:)];
UIKeyCommand *cmdReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:UIKeyModifierCommand action:@selector(handleReturn:)];
return @[
plainReturn
plainReturn,
cmdReturn
];
}
- (void)handlePlainReturn:(id)__unused sender {
- (void)handleReturn:(UIKeyCommand *)__unused sender {
UIKeyModifierFlags modifierFlags = 0;
if ([sender isKindOfClass:[UIKeyCommand class]]) {
modifierFlags = sender.modifierFlags;
}
if (_shouldReturn) {
_shouldReturn();
_shouldReturn(modifierFlags);
}
}

View file

@ -11,7 +11,7 @@ import TextNodeWithEntities
public protocol ChatInputTextNodeDelegate: AnyObject {
func chatInputTextNodeDidUpdateText()
func chatInputTextNodeShouldReturn() -> Bool
func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool
func chatInputTextNodeDidChangeSelection(dueToEditing: Bool)
func chatInputTextNodeDidBeginEditing()
func chatInputTextNodeDidFinishEditing()
@ -1180,11 +1180,11 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate,
}
return self.customDelegate?.chatInputTextNodeShouldPaste() ?? true
}
self.shouldReturn = { [weak self] in
self.shouldReturn = { [weak self] modifierFlags in
guard let self else {
return true
}
return self.customDelegate?.chatInputTextNodeShouldReturn() ?? true
return self.customDelegate?.chatInputTextNodeShouldReturn(modifierFlags: modifierFlags) ?? true
}
self.backspaceWhileEmpty = { [weak self] in
guard let self else {

View file

@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
switch action.action {
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
default:
break
@ -690,7 +690,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _, _):
if case let .unique(uniqueGift) = gift {
isStarGift = true

View file

@ -4528,15 +4528,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
public func chatInputTextNodeShouldReturn() -> Bool {
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
var shouldSendMessage = false
if self.sendActionButtons.sendButton.supernode != nil && !self.sendActionButtons.sendButton.isHidden && !self.sendActionButtons.sendContainerNode.alpha.isZero {
self.sendButtonPressed()
if let context = self.context, context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter {
if modifierFlags.contains(.command) {
shouldSendMessage = true
}
} else {
if modifierFlags.isEmpty {
shouldSendMessage = true
}
}
}
return false
if shouldSendMessage {
self.sendButtonPressed()
return false
}
return true
}
@objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldReturn()
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
}
private func applyUpdateSendButtonIcon() {

View file

@ -18,6 +18,7 @@ import UIKitRuntimeUtils
public final class GiftCompositionComponent: Component {
public class ExternalState {
public fileprivate(set) var previewPatternColor: UIColor?
public fileprivate(set) var backgroundColor: UIColor?
public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute?
public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute?
public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute?
@ -766,6 +767,7 @@ public final class GiftCompositionComponent: Component {
}
}
component.externalState?.backgroundColor = backgroundColor
component.externalState?.previewPatternColor = secondBackgroundColor
var animateBackdropSwipe = false

View file

@ -108,7 +108,7 @@ final class CraftTableComponent: Component {
fatalError("init(coder:) has not been implemented")
}
func setupFailAnimation() {
func setupFailureAnimation() {
guard !self.didSetupFinishAnimation else {
return
}
@ -127,41 +127,29 @@ final class CraftTableComponent: Component {
}
for i in 0 ..< min(2, availableStickers.count) {
if let sticker = availableStickers[i].view {
self.animationView.setSticker(sticker, face: 3 - i, mirror: isUpsideDown)
let face: Int
if isUpsideDown {
face = i + 1
} else {
face = 3 - i
}
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown)
}
}
self.state?.updated()
self.flipFaces = isUpsideDown
Queue.mainQueue().after(0.3, {
self.failWillFinish = true
self.component?.willFinish(false)
})
if let failOverlayView = self.failOverlay.view as? LottieComponent.View {
failOverlayView.isHidden = false
failOverlayView.onFrameUpdate = { [weak self] frameIndex in
guard let self else {
return
}
if frameIndex >= 5 && !self.failDidStartCrossAnimation {
self.failDidStartCrossAnimation = true
self.craftFailPlayOnce.invoke(Void())
}
if frameIndex >= 65 && !self.failDidBringToFront {
self.failDidBringToFront = true
failOverlayView.superview?.bringSubviewToFront(failOverlayView)
self.animationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
if frameIndex >= 75 && !self.failWillFinish {
self.failWillFinish = true
self.component?.willFinish(false)
}
if frameIndex >= 82 && !self.failDidFinish {
self.failDidFinish = true
failOverlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.component?.finished(nil)
}
}
}
self.craftFailOverlayPlayOnce.invoke(Void())
Queue.mainQueue().after(0.5, {
self.failDidFinish = true
self.component?.finished(nil)
})
self.state?.updated(transition: .easeInOut(duration: 0.4))
}
}
@ -171,6 +159,8 @@ final class CraftTableComponent: Component {
}
self.didSetupFinishAnimation = true
self.animationView.isSuccess = true
self.animationView.onFinishApproach = { [weak self] isUpsideDown in
guard let self else {
return
@ -270,10 +260,10 @@ final class CraftTableComponent: Component {
if index == 0 {
faceItems.append(
AnyComponentWithIdentity(id: "background", component: AnyComponent(
RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0)
FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true)
))
)
if !component.isCrafting {
if !component.isCrafting || self.isFailed {
faceItems.append(
AnyComponentWithIdentity(id: "glass", component: AnyComponent(
GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)))
@ -282,11 +272,20 @@ final class CraftTableComponent: Component {
}
if self.isFailed {
faceItems.append(
AnyComponentWithIdentity(id: "fail", component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(name: "CraftFail"),
size: CGSize(width: 96.0, height: 96.0),
playOnce: self.craftFailPlayOnce
AnyComponentWithIdentity(id: "faildial", component: AnyComponent(
DialIndicatorComponent(
content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(BundleIconComponent(name: "Premium/GiftCrash", tintColor: .white))),
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 84.0,
contentSize: CGSize(width: 44.0, height: 44.0),
lineWidth: 5.0,
fontSize: 18.0,
progress: 0.0,
value: component.gifts.count,
suffix: "",
isVisible: true,
isFlipped: self.flipFaces
)
))
)
@ -300,7 +299,9 @@ final class CraftTableComponent: Component {
diameter: 84.0,
lineWidth: 5.0,
fontSize: 18.0,
percentage: permilleValue / 10,
progress: CGFloat(permilleValue / 10 / 100),
value: permilleValue / 10,
suffix: "%",
isVisible: !component.isCrafting
)
))
@ -318,7 +319,7 @@ final class CraftTableComponent: Component {
} else {
faceItems.append(
AnyComponentWithIdentity(id: "background", component: AnyComponent(
RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0)
FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true)
))
)
faceItems.append(
@ -412,35 +413,36 @@ final class CraftTableComponent: Component {
self.setupSuccessAnimation(uniqueGift)
}
case .fail:
self.setupFailAnimation()
self.setupFailureAnimation()
default:
break
}
})
}
if self.isFailed {
let failOverlaySize = self.failOverlay.update(
transition: .immediate,
component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"),
size: CGSize(width: availableSize.width, height: availableSize.width),
playOnce: self.craftFailOverlayPlayOnce
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.width)
)
let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize)
if let failOverlayView = self.failOverlay.view {
if failOverlayView.superview == nil {
failOverlayView.isHidden = true
self.insertSubview(failOverlayView, belowSubview: self.animationView)
}
failOverlayView.frame = failOverlayFrame
}
}
// if self.isFailed {
// let failOverlaySize = self.failOverlay.update(
// transition: .immediate,
// component: AnyComponent(
// LottieComponent(
// content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"),
// size: CGSize(width: availableSize.width, height: availableSize.width),
// playOnce: self.craftFailOverlayPlayOnce
// )
// ),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: availableSize.width)
// )
// let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize)
// if let failOverlayView = self.failOverlay.view {
// if failOverlayView.superview == nil {
// failOverlayView.isHidden = true
// self.insertSubview(failOverlayView, belowSubview: self.animationView)
// }
// failOverlayView.frame = failOverlayFrame
// }
// }
return availableSize
}
@ -546,7 +548,7 @@ final class GiftSlotComponent: Component {
let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), transition: .immediate)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if component.gift == nil && component.isCrafting {
if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false {
transition.setBlur(layer: self.backgroundView.layer, radius: 10.0)
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
transition.setBlur(layer: self.addIcon.layer, radius: 10.0)

View file

@ -79,6 +79,8 @@ final class CubeAnimationView: UIView {
private var finishDelayTimerY: Timer?
private var cubeScale: Float = 1.0
private var hasFiredFinishApproach = false
var isSuccess = false
var onFinishApproach: ((Bool) -> Void)?
@ -485,7 +487,7 @@ final class CubeAnimationView: UIView {
let duration: TimeInterval = 0.2
let startQuad = Quad(rect: sticker.frame)
let animationView: UIView
if let snapshot = sticker.snapshotView(afterScreenUpdates: true) {
if let snapshot = sticker.snapshotView(afterScreenUpdates: false) {
self.warpSnapshot?.removeFromSuperview()
self.warpSnapshot = snapshot
@ -606,7 +608,7 @@ final class CubeAnimationView: UIView {
}
let stickerIndex = indices[index]
if self.stickers.indices.contains(stickerIndex) {
let isLast = stickerIndex == indices.count - 1
let isLast = index == indices.count - 1
self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast)
}
self.scheduleStickerSequence(from: index + 1, indices: indices)
@ -697,10 +699,12 @@ final class CubeAnimationView: UIView {
let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2)
self.onFinishApproach?(upsideDown)
}
if absRemaining <= self.finishSuccessScaleTriggerAngle {
if self.isSuccess, absRemaining <= self.finishSuccessScaleTriggerAngle {
let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle
let eased = raw * raw * (3 - 2 * raw)
self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased
} else if !self.isSuccess {
self.cubeScale = 1.0
}
if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 {
self.finishRotationY = self.finishTargetYUnwrapped

View file

@ -13,29 +13,41 @@ final class DialIndicatorComponent: Component {
let backgroundColor: UIColor
let foregroundColor: UIColor
let diameter: CGFloat
let contentSize: CGSize?
let lineWidth: CGFloat
let fontSize: CGFloat
let percentage: Int
let progress: CGFloat
let value: Int
let suffix: String
let isVisible: Bool
let isFlipped: Bool
public init(
content: AnyComponentWithIdentity<Empty>,
backgroundColor: UIColor,
foregroundColor: UIColor,
diameter: CGFloat,
contentSize: CGSize? = nil,
lineWidth: CGFloat,
fontSize: CGFloat,
percentage: Int,
isVisible: Bool = true
progress: CGFloat,
value: Int,
suffix: String,
isVisible: Bool = true,
isFlipped: Bool = false,
) {
self.content = content
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.diameter = diameter
self.contentSize = contentSize
self.lineWidth = lineWidth
self.fontSize = fontSize
self.percentage = percentage
self.progress = progress
self.value = value
self.suffix = suffix
self.isVisible = isVisible
self.isFlipped = isFlipped
}
public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool {
@ -51,18 +63,30 @@ final class DialIndicatorComponent: Component {
if lhs.diameter != rhs.diameter {
return false
}
if lhs.contentSize != rhs.contentSize {
return false
}
if lhs.lineWidth != rhs.lineWidth {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.percentage != rhs.percentage {
if lhs.progress != rhs.progress {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.suffix != rhs.suffix {
return false
}
if lhs.isVisible != rhs.isVisible {
return false
}
if lhs.isFlipped != rhs.isFlipped {
return false
}
return true
}
@ -119,7 +143,7 @@ final class DialIndicatorComponent: Component {
self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil)
self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
self.foregroundLayer.strokeStart = strokeStart
transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * (CGFloat(component.percentage) / 100.0))
transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * component.progress)
self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize)
if previousComponent?.content.id != component.content.id {
@ -136,7 +160,8 @@ final class DialIndicatorComponent: Component {
self.content = ComponentView()
}
let contentFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0))
let contentSize = component.contentSize ?? CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - contentSize.width) / 2.0), y: floorToScreenPixels((pathSize.height - contentSize.height) / 2.0)), size: contentSize)
let _ = self.content.update(
transition: .immediate,
component: component.content.component,
@ -154,10 +179,12 @@ final class DialIndicatorComponent: Component {
contentView.frame = contentFrame
}
let labelItems: [AnimatedTextComponent.Item] = [
AnimatedTextComponent.Item(id: "percent", content: .number(component.percentage, minDigits: 1)),
AnimatedTextComponent.Item(id: "suffix", content: .text("%"))
var labelItems: [AnimatedTextComponent.Item] = [
AnimatedTextComponent.Item(id: "percent", content: .number(component.value, minDigits: 1))
]
if !component.suffix.isEmpty {
labelItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(component.suffix)))
}
let labelSize = self.label.update(
transition: transition,
@ -181,6 +208,8 @@ final class DialIndicatorComponent: Component {
transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0)
transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0)
self.containerView.transform = CGAffineTransform(rotationAngle: component.isFlipped ? .pi : 0.0)
self.containerView.frame = CGRect(origin: .zero, size: pathSize)
return pathSize

View file

@ -42,27 +42,35 @@ private final class CraftGiftPageContent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
class ExternalState {
fileprivate(set) var giftMap: [Int64: GiftItem]
fileprivate(set) var giftsMap: [Int64: GiftItem]
fileprivate(set) var starGiftsMap: [Int64: StarGift.Gift] = [:]
fileprivate(set) var displayFailure = false
fileprivate(set) var testFailOrSuccess: Bool?
public init() {
self.giftMap = [:]
self.giftsMap = [:]
}
}
enum DisplayState {
case `default`
case crafting
case failure
}
let context: AccountContext
let craftContext: CraftGiftsContext
let resaleContext: () -> ResaleGiftsContext?
let colors: (UIColor, UIColor, UIColor, UIColor, UIColor)
let gift: StarGift.UniqueGift
let selectedGiftIds: [Int32: Int64]
let displayCraftInfo: Bool
let isCrafting: Bool
let inProgress: Bool
let displayState: DisplayState
let displayInfo: Bool
let result: CraftTableComponent.Result?
let screenSize: CGSize
let externalState: ExternalState
let starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>
let selectGift: (Int32, GiftItem) -> Void
let removeGift: (Int32) -> Void
let dismiss: () -> Void
@ -74,12 +82,12 @@ private final class CraftGiftPageContent: Component {
colors: (UIColor, UIColor, UIColor, UIColor, UIColor),
gift: StarGift.UniqueGift,
selectedGiftIds: [Int32: Int64],
displayCraftInfo: Bool,
isCrafting: Bool,
inProgress: Bool,
displayState: DisplayState,
displayInfo: Bool,
result: CraftTableComponent.Result?,
screenSize: CGSize,
externalState: ExternalState,
starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>,
selectGift: @escaping (Int32, GiftItem) -> Void,
removeGift: @escaping (Int32) -> Void,
dismiss: @escaping () -> Void
@ -90,12 +98,12 @@ private final class CraftGiftPageContent: Component {
self.colors = colors
self.gift = gift
self.selectedGiftIds = selectedGiftIds
self.displayCraftInfo = displayCraftInfo
self.isCrafting = isCrafting
self.inProgress = inProgress
self.displayState = displayState
self.displayInfo = displayInfo
self.result = result
self.screenSize = screenSize
self.externalState = externalState
self.starsTopUpOptionsPromise = starsTopUpOptionsPromise
self.selectGift = selectGift
self.removeGift = removeGift
self.dismiss = dismiss
@ -114,13 +122,10 @@ private final class CraftGiftPageContent: Component {
if lhs.selectedGiftIds != rhs.selectedGiftIds {
return false
}
if lhs.displayCraftInfo != rhs.displayCraftInfo {
if lhs.displayState != rhs.displayState {
return false
}
if lhs.isCrafting != rhs.isCrafting {
return false
}
if lhs.inProgress != rhs.inProgress {
if lhs.displayInfo != rhs.displayInfo {
return false
}
if lhs.screenSize != rhs.screenSize {
@ -138,19 +143,22 @@ private final class CraftGiftPageContent: Component {
private let title = ComponentView<Empty>()
private let descriptionText = ComponentView<Empty>()
private let craftingTitle = ComponentView<Empty>()
private let craftingSubtitle = ComponentView<Empty>()
private let craftingDescription = ComponentView<Empty>()
private let craftingProbability = ComponentView<Empty>()
private var craftingProbabilityMeasure = ComponentView<Empty>()
private var craftTable = ComponentView<Empty>()
private var backdropDial = ComponentView<Empty>()
private var symbolDial = ComponentView<Empty>()
private var variantsButton = ComponentView<Empty>()
private var variantsButtonMeasure = ComponentView<Empty>()
private var craftTable = ComponentView<Empty>()
private var selectedGifts: [AnyHashable: ComponentView<Empty>] = [:]
private let craftingTitle = ComponentView<Empty>()
private let craftingSubtitle = ComponentView<Empty>()
private let craftingDescription = ComponentView<Empty>()
private let craftingProbability = ComponentView<Empty>()
private var craftingProbabilityMeasure = ComponentView<Empty>()
private let failureTitle = ComponentView<Empty>()
private let failureDescription = ComponentView<Empty>()
private var failedGifts: [AnyHashable: ComponentView<Empty>] = [:]
private let infoContainer = UIView()
private var infoBackground = SimpleLayer()
@ -167,13 +175,10 @@ private final class CraftGiftPageContent: Component {
private let upgradePreviewDisposable = DisposableSet()
private var upgradePreview: [StarGift.UniqueGift.Attribute]?
private var starGiftsMap: [Int64: StarGift.Gift] = [:]
private let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil)
private var availableGifts: [GiftItem] = []
private var giftMap: [Int64: GiftItem] = [:]
private var isCrafting = false
private var isFailing = false
private var component: CraftGiftPageContent?
private weak var state: EmptyComponentState?
@ -260,7 +265,7 @@ private final class CraftGiftPageContent: Component {
initialGiftItem
]
self.giftMap = [initialGiftItem.gift.id: initialGiftItem]
component.externalState.giftMap = self.giftMap
component.externalState.giftsMap = self.giftMap
self.craftStateDisposable = (component.craftContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
@ -296,7 +301,7 @@ private final class CraftGiftPageContent: Component {
}
self.availableGifts = items
self.giftMap = map
self.component?.externalState.giftMap = self.giftMap
self.component?.externalState.giftsMap = self.giftMap
self.state?.updated(transition: .spring(duration: 0.4))
})
@ -322,7 +327,7 @@ private final class CraftGiftPageContent: Component {
self.upgradePreviewDisposable.add((.single(nil) |> then(component.context.engine.payments.cachedStarGifts())
|> deliverOnMainQueue).start(next: { [weak self] starGifts in
guard let self, let starGifts else {
guard let self, let component = self.component, let starGifts else {
return
}
var starGiftsMap: [Int64: StarGift.Gift] = [:]
@ -332,9 +337,8 @@ private final class CraftGiftPageContent: Component {
}
}
self.starGiftsMap = starGiftsMap
component.externalState.starGiftsMap = starGiftsMap
}))
self.starsTopUpOptionsPromise.set(component.context.engine.payments.starsTopUpOptions() |> map(Optional.init))
}
transition.setGradientColors(layer: self.background, colors: [component.colors.0, component.colors.1])
@ -348,6 +352,8 @@ private final class CraftGiftPageContent: Component {
self.state = state
self.environment = environment
let isCrafting = [.crafting, .failure].contains(component.displayState)
var selectedGifts: [Int32: GiftItem] = [:]
for (index, giftId) in component.selectedGiftIds {
if let gift = self.giftMap[giftId] {
@ -388,14 +394,14 @@ private final class CraftGiftPageContent: Component {
avatarScale: 1.0,
defaultHeight: 300.0,
gradientOnTop: true,
avatarTransitionFraction: self.isFailing ? 1.0 : 0.0,
avatarTransitionFraction: 0.0,
patternTransitionFraction: 0.0,
patternIconScale: 1.5
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 169.0 * 2.0)
)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize)
if let backgroundView = self.pattern.view {
if backgroundView.layer.superlayer == nil {
backgroundTransition = .immediate
@ -420,8 +426,8 @@ private final class CraftGiftPageContent: Component {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
transition.setAlpha(view: titleView, alpha: component.isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0)
transition.setAlpha(view: titleView, alpha: 1.0)
//transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0)
}
let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))"
@ -431,7 +437,7 @@ private final class CraftGiftPageContent: Component {
let descriptionFont = Font.regular(13.0)
let descriptionBoldFont = Font.semibold(13.0)
let descriptionColor = UIColor.white
let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all selected gifts\nwill be consumed."
let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all used gifts\nwill be lost."
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
if let gift = self.starGiftsMap[component.gift.giftId] {
@ -565,7 +571,9 @@ private final class CraftGiftPageContent: Component {
diameter: 48.0,
lineWidth: 4.0,
fontSize: 10.0,
percentage: backdropPermille / 10
progress: CGFloat(backdropPermille / 10 / 100),
value: backdropPermille / 10,
suffix: "%"
)
),
action: { [weak self] in
@ -609,9 +617,12 @@ private final class CraftGiftPageContent: Component {
backgroundColor: .white.withAlphaComponent(0.1),
foregroundColor: .white,
diameter: 48.0,
contentSize: CGSize(width: 28.0, height: 28.0),
lineWidth: 4.0,
fontSize: 10.0,
percentage: symbolPermille / 10
progress: CGFloat(symbolPermille / 10 / 100),
value: symbolPermille / 10,
suffix: "%"
)
),
action: { [weak self] in
@ -726,7 +737,7 @@ private final class CraftGiftPageContent: Component {
craftContentHeight += 160.0
let originalCraftContentHeight = craftContentHeight
if component.isCrafting {
if !"".isEmpty, isCrafting {
craftContentHeight = component.screenSize.height
}
@ -736,8 +747,8 @@ private final class CraftGiftPageContent: Component {
self.addSubview(descriptionTextView)
}
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
transition.setAlpha(view: descriptionTextView, alpha: component.isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: descriptionTextView.layer, radius: component.isCrafting ? 10.0 : 0.0)
transition.setAlpha(view: descriptionTextView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: descriptionTextView.layer, radius: isCrafting ? 10.0 : 0.0)
}
let backdropDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 - 9.0 - backdropDialSize.width, y: craftContentHeight - 145.0 - 78.0), size: backdropDialSize)
@ -746,8 +757,8 @@ private final class CraftGiftPageContent: Component {
self.addSubview(backdropDialView)
}
transition.setFrame(view: backdropDialView, frame: backdropDialFrame)
transition.setAlpha(view: backdropDialView, alpha: component.isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: backdropDialView.layer, radius: component.isCrafting ? 10.0 : 0.0)
transition.setAlpha(view: backdropDialView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: backdropDialView.layer, radius: isCrafting ? 10.0 : 0.0)
}
let symbolDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 + 9.0, y: craftContentHeight - 145.0 - 78.0), size: symbolDialSize)
@ -756,14 +767,14 @@ private final class CraftGiftPageContent: Component {
self.addSubview(symbolDialView)
}
transition.setFrame(view: symbolDialView, frame: symbolDialFrame)
transition.setAlpha(view: symbolDialView, alpha: component.isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: symbolDialView.layer, radius: component.isCrafting ? 10.0 : 0.0)
transition.setAlpha(view: symbolDialView, alpha: isCrafting ? 0.0 : 1.0)
transition.setBlur(layer: symbolDialView.layer, radius: isCrafting ? 10.0 : 0.0)
}
let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight - 145.0), size: variantsButtonSize)
var varitantsButtonTransition = transition
if let variantsButtonView = self.variantsButton.view {
if variantsButtonView.superview == nil && !component.isCrafting {
if variantsButtonView.superview == nil && component.displayState == .default {
varitantsButtonTransition = .immediate
if let symbolDialView = self.symbolDial.view {
self.insertSubview(variantsButtonView, aboveSubview: symbolDialView)
@ -772,9 +783,9 @@ private final class CraftGiftPageContent: Component {
}
}
varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame)
varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: component.isCrafting ? 10.0 : 0.0)
if component.isCrafting {
variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.41, removeOnCompletion: false, completion: { _ in
varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0)
if component.displayState == .crafting {
variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
variantsButtonView.removeFromSuperview()
})
}
@ -782,11 +793,12 @@ private final class CraftGiftPageContent: Component {
let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) })
if component.isCrafting {
var craftingOriginY = craftContentHeight * 0.5 + 160.0
if component.displayState == .crafting {
//var craftingOriginY = craftContentHeight * 0.5 + 160.0
var craftingOriginY = craftContentHeight * 0.5 - 16.0
let offset = -(craftContentHeight - originalCraftContentHeight)
let titleSize = self.craftingTitle.update(
let craftingTitleSize = self.craftingTitle.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting", font: Font.bold(20.0), textColor: .white)))
@ -794,21 +806,21 @@ private final class CraftGiftPageContent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: craftingOriginY), size: titleSize)
if let titleView = self.craftingTitle.view {
if titleView.superview == nil {
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
transition.animateBlur(layer: titleView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: titleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
let craftingTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingTitleSize.width) * 0.5), y: craftingOriginY), size: craftingTitleSize)
if let craftingTitleView = self.craftingTitle.view {
if craftingTitleView.superview == nil {
transition.animateAlpha(view: craftingTitleView, from: 0.0, to: 1.0)
transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: craftingTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
self.addSubview(titleView)
self.addSubview(craftingTitleView)
}
titleView.frame = titleFrame
craftingTitleView.frame = craftingTitleFrame
}
craftingOriginY += titleSize.height
craftingOriginY += craftingTitleSize.height
craftingOriginY += 7.0
let subtitleSize = self.craftingSubtitle.update(
let craftingSubtitleSize = self.craftingSubtitle.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.5))))
@ -816,34 +828,30 @@ private final class CraftGiftPageContent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) * 0.5), y: craftingOriginY), size: subtitleSize)
if let subtitleView = self.craftingSubtitle.view {
if subtitleView.superview == nil {
transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0)
transition.animateBlur(layer: subtitleView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: subtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
let craftingSubtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingSubtitleSize.width) * 0.5), y: craftingOriginY), size: craftingSubtitleSize)
if let craftingSubtitleView = self.craftingSubtitle.view {
if craftingSubtitleView.superview == nil {
transition.animateAlpha(view: craftingSubtitleView, from: 0.0, to: 1.0)
transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: craftingSubtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
self.addSubview(subtitleView)
self.addSubview(craftingSubtitleView)
}
subtitleView.frame = subtitleFrame
craftingSubtitleView.frame = craftingSubtitleFrame
}
craftingOriginY += subtitleSize.height
craftingOriginY += craftingSubtitleSize.height
craftingOriginY += 21.0
let descriptionFont = Font.regular(13.0)
let descriptionBoldFont = Font.semibold(13.0)
let descriptionColor = UIColor.white.withAlphaComponent(0.5)
let rawDescriptionString = "If crafting fails, all selected gifts\nwill be consumed."
let rawDescriptionString = "If crafting fails, all used gifts\nwill be lost."
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
let craftingDescriptionSize = self.craftingDescription.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white.withAlphaComponent(0.3),
MultilineTextComponent(
text: .plain(descriptionString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
@ -904,6 +912,165 @@ private final class CraftGiftPageContent: Component {
}
craftingProbabilityView.frame = craftingProbabilityFrame
}
} else {
if let craftingTitleView = self.craftingTitle.view {
transition.setAlpha(view: craftingTitleView, alpha: 0.0, completion: { _ in
craftingTitleView.removeFromSuperview()
})
transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 0.0, toRadius: 10.0)
}
if let craftingSubtitleView = self.craftingSubtitle.view {
transition.setAlpha(view: craftingSubtitleView, alpha: 0.0, completion: { _ in
craftingSubtitleView.removeFromSuperview()
})
transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 0.0, toRadius: 10.0)
}
if let craftingDescriptionView = self.craftingDescription.view {
transition.setAlpha(view: craftingDescriptionView, alpha: 0.0, completion: { _ in
craftingDescriptionView.removeFromSuperview()
})
transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 0.0, toRadius: 10.0)
}
if let craftingProbabilityView = self.craftingProbability.view {
craftingProbabilityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
craftingProbabilityView.removeFromSuperview()
})
transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 0.0, toRadius: 10.0)
}
}
if component.displayState == .failure {
var failureOriginY = craftContentHeight * 0.5 - 16.0
let offset = -(craftContentHeight - originalCraftContentHeight)
let failureTitleSize = self.failureTitle.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting Failed", font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d))))
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let failureTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureTitleSize.width) * 0.5), y: failureOriginY), size: failureTitleSize)
if let failureTitleView = self.failureTitle.view {
if failureTitleView.superview == nil {
transition.animateAlpha(view: failureTitleView, from: 0.0, to: 1.0)
transition.animateBlur(layer: failureTitleView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: failureTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
self.addSubview(failureTitleView)
}
failureTitleView.frame = failureTitleFrame
}
failureOriginY += failureTitleSize.height
failureOriginY += 17.0
let descriptionFont = Font.regular(13.0)
let descriptionBoldFont = Font.semibold(13.0)
let descriptionColor = UIColor(rgb: 0xf7af8c)
let rawDescriptionString = "This crafting attempt was unsuccessful.\n**\(component.selectedGiftIds.count) gifts** were lost."
let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString
let failureDescriptionSize = self.failureDescription.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(descriptionString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let failureDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureDescriptionSize.width) * 0.5), y: failureOriginY), size: failureDescriptionSize)
if let failureDescriptionView = self.failureDescription.view {
if failureDescriptionView.superview == nil {
transition.animateAlpha(view: failureDescriptionView, from: 0.0, to: 1.0)
transition.animateBlur(layer: failureDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0)
transition.animatePosition(view: failureDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true)
self.addSubview(failureDescriptionView)
}
failureDescriptionView.frame = failureDescriptionFrame
}
failureOriginY += failureDescriptionSize.height
failureOriginY += 34.0
var indices: [Int] = []
for index in component.selectedGiftIds.keys.sorted() {
indices.append(Int(index))
}
var lostGifts: [GiftItem] = []
for index in indices {
if let giftId = component.selectedGiftIds[Int32(index)], let gift = self.giftMap[giftId] {
lostGifts.append(gift)
}
}
let itemSize = CGSize(width: 80.0, height: 80.0)
let itemSpacing: CGFloat = 16.0
var itemDelay: Double = 0.2
let totalItemsWidth: CGFloat = itemSize.width * CGFloat(lostGifts.count) + itemSpacing * CGFloat(lostGifts.count - 1)
var itemOriginX: CGFloat = floor((availableSize.width - totalItemsWidth) / 2.0)
for gift in lostGifts {
let itemId = AnyHashable(gift.gift.id)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.failedGifts[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.failedGifts[itemId] = visibleItem
itemTransition = .immediate
}
let ribbonText = "#\(gift.gift.number)"
let ribbonColor: GiftItemComponent.Ribbon.Color = .custom(0xff645b, 0xff645b)
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
GiftItemComponent(
context: component.context,
style: .glass,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: .uniqueGift(gift: gift.gift, price: nil),
ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil),
badge: nil,
resellPrice: nil,
isHidden: false,
isSelected: false,
isPinned: false,
isEditing: false,
mode: .grid,
action: nil,
contextAction: nil
)
),
environment: {},
containerSize: itemSize
)
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: failureOriginY), size: itemSize)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.addSubview(itemView)
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: itemDelay)
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: itemDelay)
}
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
itemOriginX += itemSize.width + itemSpacing
itemDelay += 0.07
}
}
let tableSize = CGSize(width: availableSize.width, height: 320.0)
@ -914,7 +1081,7 @@ private final class CraftGiftPageContent: Component {
context: component.context,
gifts: selectedGifts,
buttonColor: component.colors.3,
isCrafting: component.isCrafting,
isCrafting: isCrafting,
result: component.result,
select: { [weak self] index in
guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else {
@ -927,7 +1094,7 @@ private final class CraftGiftPageContent: Component {
gift: component.gift,
genericGift: genericGift,
selectedGiftIds: Set(component.selectedGiftIds.values),
starsTopUpOptions: self.starsTopUpOptionsPromise.get(),
starsTopUpOptions: component.starsTopUpOptionsPromise.get(),
selectGift: { [weak self] item in
guard let self, let component = self.component else {
return
@ -947,11 +1114,11 @@ private final class CraftGiftPageContent: Component {
self.component?.removeGift(index)
},
willFinish: { [weak self] success in
guard let self else {
guard let self, let component = self.component else {
return
}
if !success {
self.isFailing = true
component.externalState.displayFailure = true
}
self.state?.updated(transition: .easeInOut(duration: 0.5))
},
@ -972,13 +1139,7 @@ private final class CraftGiftPageContent: Component {
controller.dismiss()
})
} else {
if let navigationController = controller.navigationController {
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in
controller.dismiss()
})
let previousController = navigationController.viewControllers[max(0, navigationController.viewControllers.count - 2)]
animateRipple(parentView: previousController.view, screenCornerRadius: environment.deviceMetrics.screenCornerRadius, location: CGPoint(x: previousController.view.bounds.midX, y: previousController.view.bounds.midY))
}
}
}
)
@ -986,7 +1147,7 @@ private final class CraftGiftPageContent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width, height: tableSize.height)
)
let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize)
let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize)
if let craftTableView = self.craftTable.view {
if craftTableView.superview == nil {
craftTableView.layer.cornerRadius = 40.0
@ -997,7 +1158,7 @@ private final class CraftGiftPageContent: Component {
transition.setFrame(view: craftTableView, frame: craftTableFrame)
}
transition.setAlpha(view: self.infoContainer, alpha: component.displayCraftInfo ? 1.0 : 0.0)
transition.setAlpha(view: self.infoContainer, alpha: component.displayInfo ? 1.0 : 0.0)
let infoHeaderSize = self.infoHeader.update(
transition: transition,
@ -1033,6 +1194,10 @@ private final class CraftGiftPageContent: Component {
self.infoContainer.addSubview(infoHeaderView)
}
transition.setFrame(view: infoHeaderView, frame: infoHeaderFrame)
if self.subviews.last !== self.infoContainer {
self.bringSubviewToFront(self.infoContainer)
}
}
infoContentHeight += infoHeaderSize.height
infoContentHeight += 16.0
@ -1149,17 +1314,17 @@ private final class CraftGiftPageContent: Component {
transition.setFrame(view: infoListView, frame: infoListFrame)
}
if component.displayCraftInfo {
if component.displayInfo {
infoContentHeight += infoListSize.height
infoContentHeight += 95.0
}
transition.setFrame(view: self.infoContainer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: infoContentHeight)))
transition.setFrame(layer: self.background, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: craftContentHeight)))
transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width)))
transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width)))
let effectiveContentHeight: CGFloat
if component.displayCraftInfo {
if component.displayInfo {
effectiveContentHeight = infoContentHeight
} else {
effectiveContentHeight = craftContentHeight
@ -1209,11 +1374,14 @@ private final class SheetContainerComponent: CombinedComponent {
private let context: AccountContext
private let giftId: Int64
var displayCraftInfo = false
var displayInfo = false
var isCrafting = false
var inProgress = false
var displayFailure = false
var result: CraftTableComponent.Result?
var selectedGiftIds: [Int32: Int64] = [:]
let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil)
private var _resaleContext: ResaleGiftsContext?
var resaleContext: ResaleGiftsContext {
@ -1241,12 +1409,14 @@ private final class SheetContainerComponent: CombinedComponent {
return
}
if count < 1 {
self.displayCraftInfo = true
self.displayInfo = true
self.updated()
let _ = ApplicationSpecificNotice.incrementGiftCraftingTips(accountManager: context.sharedContext.accountManager).start()
}
})
self.starsTopUpOptionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init))
}
deinit {
@ -1269,6 +1439,13 @@ private final class SheetContainerComponent: CombinedComponent {
let environment = context.environment[EnvironmentType.self]
let state = context.state
let strings = environment.strings
if externalState.displayFailure {
state.displayFailure = true
state.inProgress = false
}
let controller = environment.controller
let craftContext = context.component.craftContext
@ -1290,11 +1467,15 @@ private final class SheetContainerComponent: CombinedComponent {
let theme = environment.theme
var colors: (UIColor, UIColor, UIColor, UIColor, UIColor) = (
UIColor(rgb: 0x263245), UIColor(rgb: 0x232e3f), UIColor(rgb: 0x304059), UIColor(rgb: 0x425168), theme.list.itemCheckColors.fillColor
UIColor(rgb: 0x263245),
UIColor(rgb: 0x232e3f),
UIColor(rgb: 0x304059),
UIColor(rgb: 0x425168),
theme.list.itemCheckColors.fillColor
)
var permilleValue: Int32 = 0
for id in state.selectedGiftIds.values {
if let gift = externalState.giftMap[id] {
if let gift = externalState.giftsMap[id] {
permilleValue += gift.gift.craftChancePermille ?? 0
}
}
@ -1303,34 +1484,47 @@ private final class SheetContainerComponent: CombinedComponent {
colors.1 = UIColor(rgb: 0x1a2f38)
colors.2 = UIColor(rgb: 0x22464a)
colors.3 = UIColor(rgb: 0x2d4e50)
if !state.displayCraftInfo {
if !state.displayInfo {
colors.4 = UIColor(rgb: 0x33bf54)
}
}
if state.displayFailure {
colors.0 = UIColor(rgb: 0x46231a)
colors.1 = UIColor(rgb: 0x381b1a)
colors.2 = UIColor(rgb: 0x51291f)
colors.3 = UIColor(rgb: 0x683e34)
if !state.displayInfo {
colors.4 = UIColor(rgb: 0x683e34)
}
}
var buttonColor = colors.3
if state.displayCraftInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in
if state.displayInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
}), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
}
var backgroundColor = colors.1
if state.displayCraftInfo {
if state.displayInfo {
backgroundColor = environment.theme.list.plainBackgroundColor
}
let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))"
let buttonContent: AnyComponentWithIdentity<Empty>
if state.displayCraftInfo {
if state.displayInfo {
buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))
))
} else if state.displayFailure {
buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Another Gift", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))
))
} else {
var buttonAnimatedItems: [AnimatedTextComponent.Item] = []
buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "percent", content: .number(Int(permilleValue / 10), minDigits: 1)))
@ -1364,6 +1558,15 @@ private final class SheetContainerComponent: CombinedComponent {
))
}
var displayState: CraftGiftPageContent.DisplayState = .default
if state.displayFailure {
displayState = .failure
} else if state.isCrafting {
displayState = .crafting
}
let hideButtons = displayState == .crafting
let sheet = sheet.update(
component: ResizableSheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(
@ -1376,12 +1579,12 @@ private final class SheetContainerComponent: CombinedComponent {
colors: colors,
gift: component.gift,
selectedGiftIds: state.selectedGiftIds,
displayCraftInfo: state.displayCraftInfo,
isCrafting: state.isCrafting,
inProgress: state.inProgress,
displayState: displayState,
displayInfo: state.displayInfo,
result: state.result,
screenSize: context.availableSize,
externalState: externalState,
starsTopUpOptionsPromise: state.starsTopUpOptionsPromise,
selectGift: { [weak state] index, gift in
guard let state else {
return
@ -1401,7 +1604,7 @@ private final class SheetContainerComponent: CombinedComponent {
}
)
),
leftItem: state.isCrafting ? nil : AnyComponent(
leftItem: hideButtons ? nil : AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: buttonColor,
@ -1418,7 +1621,7 @@ private final class SheetContainerComponent: CombinedComponent {
}
)
),
rightItem: state.isCrafting || state.displayCraftInfo ? nil : AnyComponent(
rightItem: hideButtons || state.displayInfo ? nil : AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: buttonColor,
@ -1431,16 +1634,16 @@ private final class SheetContainerComponent: CombinedComponent {
)
)),
action: { [weak state] _ in
guard let state, !state.isCrafting else {
guard let state, !state.inProgress else {
return
}
state.displayCraftInfo = !state.displayCraftInfo
state.displayInfo = !state.displayInfo
state.updated(transition: .spring(duration: 0.3))
}
)
),
hasTopEdgeEffect: false,
bottomItem: state.isCrafting ? nil : AnyComponent(
bottomItem: hideButtons ? nil : AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
@ -1449,15 +1652,33 @@ private final class SheetContainerComponent: CombinedComponent {
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: buttonContent,
isEnabled: state.displayCraftInfo ? true : state.selectedGiftIds.count > 0,
isEnabled: state.displayInfo ? true : state.selectedGiftIds.count > 0,
displaysProgress: state.inProgress,
action: { [weak state] in
guard let state else {
return
}
if state.displayCraftInfo {
state.displayCraftInfo = false
if state.displayInfo {
state.displayInfo = false
state.updated(transition: .spring(duration: 0.3))
} else if state.displayFailure, let genericGift = externalState.starGiftsMap[component.gift.giftId] {
let selectController = SelectCraftGiftScreen(
context: component.context,
craftContext: component.craftContext,
resaleContext: state.resaleContext,
gift: component.gift,
genericGift: genericGift,
selectedGiftIds: Set(),
starsTopUpOptions: state.starsTopUpOptionsPromise.get(),
selectGift: { item in
let craftController = GiftCraftScreen(context: component.context, gift: item.gift)
if let controller = controller() as? GiftCraftScreen, let navigationController = controller.navigationController as? NavigationController {
controller.dismissAnimated()
navigationController.pushViewController(craftController)
}
}
)
environment.controller()?.push(selectController)
} else {
state.inProgress = true
state.updated(transition: .spring(duration: 0.3))
@ -1481,7 +1702,7 @@ private final class SheetContainerComponent: CombinedComponent {
}
var references: [StarGiftReference] = []
for index in indices {
if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftMap[giftId] {
if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftsMap[giftId] {
references.append(gift.reference)
}
}
@ -1512,10 +1733,14 @@ private final class SheetContainerComponent: CombinedComponent {
state.isCrafting = true
state.result = .fail
state.updated(transition: .spring(duration: 0.8))
Queue.mainQueue().after(1.0) {
craftContext.reload()
}
default:
if let navigationController = controller()?.navigationController {
dismiss(true)
let alertController = textAlertController(context: component.context, title: nil, text: "Unknown Error", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})])
let alertController = textAlertController(context: component.context, title: nil, text: strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
(navigationController.topViewController as? ViewController)?.present(alertController, in: .window(.root))
}
}
@ -1525,7 +1750,7 @@ private final class SheetContainerComponent: CombinedComponent {
)
),
backgroundColor: .color(backgroundColor),
isFullscreen: state.isCrafting,
isFullscreen: false, //state.isCrafting,
animateOut: animateOut
),
environment: {

View file

@ -379,6 +379,7 @@ final class SelectGiftPageContent: Component {
starsContext: component.context.starsContext!,
peerId: component.context.account.peerId,
gift: component.genericGift,
isPlain: true,
confirmPurchaseImmediately: true,
starsTopUpOptions: component.starsTopUpOptions,
scrollToTop: {},
@ -530,9 +531,9 @@ private final class SheetContainerComponent: CombinedComponent {
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "back", component: AnyComponent(
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Back",
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),

View file

@ -24,6 +24,7 @@ swift_library(
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/ContextUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",

View file

@ -0,0 +1,174 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import TextFormat
import TelegramStringFormatting
import Markdown
final class BalanceComponent: Component {
private let context: AccountContext
private let theme: PresentationTheme
private let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.action = action
}
static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private var text = ComponentView<Empty>()
private var component: BalanceComponent?
private var componentState: EmptyComponentState?
private var starsBalance: Int64 = 0
private var tonBalance: Int64 = 0
private var balanceDisposable: Disposable?
override public init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.balanceDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
private var isUpdating = false
func update(component: BalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.componentState = state
if self.balanceDisposable == nil {
if let starsContext = component.context.starsContext, let tonContext = component.context.tonContext {
self.balanceDisposable = combineLatest(queue: Queue.mainQueue(),
starsContext.state,
tonContext.state
).start(next: { [weak self] starsState, tonState in
guard let self else {
return
}
self.starsBalance = starsState?.balance.value ?? 0
self.tonBalance = tonState?.balance.value ?? 0
if !self.isUpdating {
self.componentState?.updated()
}
})
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var rawString: String = ""
let starsBalanceString = "**⭐️\(presentationStringsFormattedNumber(Int32(clamping: self.starsBalance), presentationData.dateTimeFormat.groupingSeparator))**"
if self.tonBalance > 0 {
let tonBalanceString = "**💎\(formatTonAmountText(self.tonBalance, dateTimeFormat: presentationData.dateTimeFormat))**"
rawString = starsBalanceString + "\n" + tonBalanceString
} else {
rawString = presentationData.strings.Stars_Purchase_Balance + "\n" + starsBalanceString
}
let attributedText = parseMarkdownIntoAttributedString(
rawString,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
linkAttribute: { _ in
return nil
}
),
textAlignment: .right
).mutableCopy() as! NSMutableAttributedString
let starRange = (attributedText.string as NSString).range(of: "⭐️")
if starRange.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: starRange)
attributedText.addAttribute(.baselineOffset, value: 1.0, range: starRange)
}
let tonRange = (attributedText.string as NSString).range(of: "💎")
if tonRange.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: tonRange)
attributedText.addAttribute(.baselineOffset, value: 1.0, range: tonRange)
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(attributedText),
horizontalAlignment: .right,
maximumNumberOfLines: 2,
lineSpacing: 0.1,
displaysAsynchronously: false
)
),
environment: {},
containerSize: availableSize
)
let inset: CGFloat = 12.0
let size = CGSize(width: textSize.width + inset * 2.0, height: 44.0)
if let textView = self.text.view {
if textView.superview == nil {
textView.isUserInteractionEnabled = false
self.addSubview(textView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: textView, from: 0.0, to: 1.0)
}
}
textView.frame = CGRect(origin: CGPoint(x: inset, y: 8.0 - UIScreenPixel), size: textSize)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -29,6 +29,7 @@ import LottieComponent
import GiftLoadingShimmerView
import EdgeEffect
import GlassBackgroundComponent
import ContextUI
private let minimumCountToDisplayFilters = 18
@ -45,6 +46,7 @@ public final class GiftStoreContentComponent: Component {
let starsContext: StarsContext
let peerId: EnginePeer.Id
let gift: StarGift.Gift
let isPlain: Bool
let confirmPurchaseImmediately: Bool
let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>?
let scrollToTop: () -> Void
@ -64,6 +66,7 @@ public final class GiftStoreContentComponent: Component {
starsContext: StarsContext,
peerId: EnginePeer.Id,
gift: StarGift.Gift,
isPlain: Bool,
confirmPurchaseImmediately: Bool,
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>?,
scrollToTop: @escaping () -> Void,
@ -82,6 +85,7 @@ public final class GiftStoreContentComponent: Component {
self.starsContext = starsContext
self.peerId = peerId
self.gift = gift
self.isPlain = isPlain
self.confirmPurchaseImmediately = confirmPurchaseImmediately
self.starsTopUpOptions = starsTopUpOptions
self.scrollToTop = scrollToTop
@ -1075,7 +1079,7 @@ public final class GiftStoreContentComponent: Component {
let loadingSize = CGSize(width: availableSize.width, height: min(1000.0, availableSize.height))
if isLoading && self.showLoading {
self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: true, transition: .immediate)
self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: component.isPlain, transition: .immediate)
loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
} else {
loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0)
@ -1145,10 +1149,8 @@ final class GiftStoreScreenComponent: Component {
private let edgeEffectView: EdgeEffectView
private let balanceBackgroundView: GlassBackgroundView
private let balanceTitle = ComponentView<Empty>()
private let balanceValue = ComponentView<Empty>()
private let balanceIcon = ComponentView<Empty>()
private let balance = ComponentView<Empty>()
private let balanceBackgroundView: GlassContextExtractableContainer
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
@ -1165,7 +1167,7 @@ final class GiftStoreScreenComponent: Component {
private var isUpdating: Bool = false
override init(frame: CGRect) {
self.balanceBackgroundView = GlassBackgroundView()
self.balanceBackgroundView = GlassContextExtractableContainer()
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -1211,6 +1213,68 @@ final class GiftStoreScreenComponent: Component {
contentView.updateScrolling(bounds: bounds, interactive: interactive, transition: transition)
}
}
func presentBalanceMenu() {
guard let component = self.component, let starsContext = component.context.starsContext, let tonContext = component.context.tonContext, let controller = self.environment?.controller() else {
return
}
let tonBalance = tonContext.currentState?.balance.value ?? 0
if tonBalance == 0 {
let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext)
controller.push(controller)
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let sourceView = self.balanceBackgroundView
let items: Signal<[ContextMenuItem], NoError> = combineLatest(
queue: Queue.mainQueue(),
starsContext.state,
tonContext.state
)
|> take(1)
|> map { starsState, tonState -> [ContextMenuItem] in
let starsBalance = starsState?.balance ?? .zero
let tonBalance = tonState?.balance.value ?? 0
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(
text: "My Stars",
textLayout: .secondLineWithValue(formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)),
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stars"), color: theme.contextMenu.primaryColor) },
action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component, let environment = self.environment else {
return
}
let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: starsContext)
environment.controller()?.push(controller)
}
)))
items.append(.action(ContextMenuActionItem(
text: "My TON",
textLayout: .secondLineWithValue(formatTonAmountText(tonBalance, dateTimeFormat: presentationData.dateTimeFormat)),
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Ton"), color: theme.contextMenu.primaryColor) },
action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component, let environment = self.environment else {
return
}
let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext)
environment.controller()?.push(controller)
}
)))
return items
}
let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
@ -1255,42 +1319,37 @@ final class GiftStoreScreenComponent: Component {
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, rect: edgeEffectFrame, edge: .top, edgeSize: min(30, edgeEffectFrame.height), transition: transition)
let balanceTitleSize = self.balanceTitle.update(
let balanceSize = self.balance.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Stars_Purchase_Balance,
font: Font.regular(14.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
maximumNumberOfLines: 1
)),
component: AnyComponent(
BalanceComponent(
context: component.context,
theme: environment.theme,
action: { [weak self] in
guard let self else {
return
}
self.presentBalanceMenu()
}
)
),
environment: {},
containerSize: availableSize
)
let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat)
let smallLabelFont = Font.regular(11.0)
let labelFont = Font.semibold(14.0)
let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator)
let balanceValueSize = self.balanceValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(balanceText),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: availableSize
)
let balanceIconSize = self.balanceIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)),
environment: {},
containerSize: availableSize
)
let balanceFrame = CGRect(origin: .zero, size: balanceSize)
if let balanceView = self.balance.view {
if balanceView.superview == nil {
self.balanceBackgroundView.contentView.addSubview(balanceView)
}
balanceView.frame = balanceFrame
let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceSize)
transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame)
self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
}
if self.balanceBackgroundView.superview == nil {
component.overNavigationContainer.addSubview(self.balanceBackgroundView)
}
@ -1300,32 +1359,6 @@ final class GiftStoreScreenComponent: Component {
topInset = environment.statusBarHeight - 6.0
}
if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view {
if balanceTitleView.superview == nil {
self.balanceBackgroundView.contentView.addSubview(balanceTitleView)
self.balanceBackgroundView.contentView.addSubview(balanceValueView)
self.balanceBackgroundView.contentView.addSubview(balanceIconView)
}
let topBalanceOriginY = (44.0 - balanceTitleSize.height - balanceValueSize.height) / 2.0
let balanceSideInset: CGFloat = 12.0
var balanceBackgroundSize = CGSize(width: balanceTitleSize.width + balanceSideInset * 2.0, height: 44.0)
balanceBackgroundSize.width = max(balanceBackgroundSize.width, balanceValueSize.width + balanceIconSize.width + 2.0 + balanceSideInset * 2.0)
let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceBackgroundSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceBackgroundSize)
transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame)
self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition)
balanceTitleView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0)
balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize)
balanceValueView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0)
balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize)
balanceIconView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel)
balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize)
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
@ -1360,6 +1393,7 @@ final class GiftStoreScreenComponent: Component {
starsContext: component.starsContext,
peerId: component.peerId,
gift: component.gift,
isPlain: false,
confirmPurchaseImmediately: false,
starsTopUpOptions: nil,
scrollToTop: { [weak self] in

View file

@ -746,8 +746,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
}), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
}

View file

@ -294,8 +294,8 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
}), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
}

View file

@ -739,7 +739,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
return false
}
}), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)
badgeColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
let outer = UIColor(rgb: UInt32(bitPattern: outerColor))

View file

@ -748,7 +748,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes))))
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift {
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
let updatedMedia: [Media] = [
TelegramMediaAction(
@ -770,7 +770,8 @@ private final class GiftViewSheetContent: CombinedComponent {
dropOriginalDetailsStars: nil,
assigned: assigned,
fromOffer: fromOffer,
canCraftAt: canCraftAt
canCraftAt: canCraftAt,
isCrafted: isCrafted
)
)
]
@ -2909,8 +2910,8 @@ private final class GiftViewSheetContent: CombinedComponent {
}
var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16)
if let previewPatternColor = giftCompositionExternalState.previewPatternColor {
buttonColor = previewPatternColor
if let backgroundColor = giftCompositionExternalState.backgroundColor {
buttonColor = backgroundColor.mixedWith(.white, alpha: 0.2)
}
let variantsMeasureDescription = variantsMeasureDescription.update(
@ -5129,10 +5130,10 @@ private final class GiftViewSheetContent: CombinedComponent {
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05))
} else if showUpgradePreview, let previewPatternColor = giftCompositionExternalState.previewPatternColor {
buttonsBackground = .color(previewPatternColor.withMultipliedBrightnessBy(1.05))
}), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute {
buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2))
} else if showUpgradePreview, let backgroundColor = giftCompositionExternalState.backgroundColor {
buttonsBackground = .color(backgroundColor.mixedWith(.white, alpha: 0.2))
}
var isBackButton = false
@ -5449,7 +5450,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let fromPeerId = senderId ?? message.author?.id
return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number, nil)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate):
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate, _):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)

View file

@ -3734,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
} else if case let .gift(gift) = subject {
isGift = true
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil))]
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil, isCrafted: false))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages = .single([message])
} else {

View file

@ -197,6 +197,8 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode {
}
private let backgroundNode: ASDisplayNode
private let highlightNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
@ -225,6 +227,9 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.highlightNode = ASDisplayNode()
self.highlightNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -297,6 +302,20 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode {
self.item?.openSetCustom?()
}
public func displayHighlight() {
if self.backgroundNode.supernode != nil {
self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
} else {
self.insertSubnode(self.highlightNode, at: 0)
}
Queue.mainQueue().after(1.2, {
self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
self.highlightNode.removeFromSupernode()
})
})
}
func asyncLayout() -> (_ item: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
@ -329,6 +348,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
@ -370,6 +390,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))

View file

@ -587,8 +587,7 @@ public final class PeerInfoCoverComponent: Component {
self.avatarPatternContentLayers.append(itemLayer)
}
//itemLayer.frame = itemFrame
transition.setFrame(layer: itemLayer, frame: itemFrame)
itemLayer.frame = itemFrame
itemLayer.layerTintColor = UIColor(white: 0.0, alpha: 0.8).cgColor
transition.setAlpha(layer: itemLayer, alpha: 1.0 - itemScaleFraction)

View file

@ -589,7 +589,7 @@ public final class TextFieldComponent: Component {
}
}
public func chatInputTextNodeShouldReturn() -> Bool {
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
guard let component = self.component else {
return true
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "star (3).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ton (3).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "giftcrash.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -15,16 +15,15 @@ import ChatControllerInteraction
extension ChatControllerImpl {
var keyShortcutsInternal: [KeyShortcut] {
if !self.traceVisibility() || !isTopmostChatController(self) {
if !isTopmostChatController(self) {
return []
}
let strings = self.presentationData.strings
var inputShortcuts: [KeyShortcut]
var inputShortcuts: [KeyShortcut] = []
if self.chatDisplayNode.isInputViewFocused {
inputShortcuts = [
KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}),
KeyShortcut(input: "B", modifiers: [.command], action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
@ -80,6 +79,16 @@ extension ChatControllerImpl {
}
})
]
if self.context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter {
inputShortcuts.append(
KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", modifiers: [.command], action: {})
)
} else {
inputShortcuts.append(
KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {})
)
}
} else if UIResponder.currentFirst() == nil {
inputShortcuts = [
KeyShortcut(title: strings.KeyCommand_FocusOnInputField, input: "\r", action: { [weak self] in

View file

@ -1136,7 +1136,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let sendGiftTitle: String
var isIncoming = message.effectivelyIncoming(context.account.peerId)
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action {
if isUpgrade && message.author?.id == context.account.peerId {
isIncoming = true
}

View file

@ -255,6 +255,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public let currentMediaDisplaySettings: Atomic<MediaDisplaySettings>
private var mediaDisplaySettingsDisposable: Disposable?
public let currentChatSettings: Atomic<ChatSettings>
private var chatSettingsDisposable: Disposable?
public let currentStickerSettings: Atomic<StickerSettings>
private var stickerSettingsDisposable: Disposable?
@ -339,6 +342,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.currentMediaDisplaySettings = Atomic(value: initialPresentationDataAndSettings.mediaDisplaySettings)
self.currentStickerSettings = Atomic(value: initialPresentationDataAndSettings.stickerSettings)
self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings)
self.currentChatSettings = Atomic(value: initialPresentationDataAndSettings.chatSettings)
if automaticEnergyUsageShouldBeOnNow(settings: self.currentAutomaticMediaDownloadSettings) {
self.energyUsageSettings = EnergyUsageSettings.powerSavingDefault
@ -485,6 +489,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
})
self.chatSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.chatSettings])
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
if let strongSelf = self {
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) {
let _ = strongSelf.currentChatSettings.swap(settings)
}
}
})
let immediateExperimentalUISettingsValue = self.immediateExperimentalUISettingsValue
let _ = immediateExperimentalUISettingsValue.swap(initialPresentationDataAndSettings.experimentalUISettings)
@ -1078,6 +1091,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.inAppNotificationSettingsDisposable?.dispose()
self.mediaInputSettingsDisposable?.dispose()
self.mediaDisplaySettingsDisposable?.dispose()
self.chatSettingsDisposable?.dispose()
self.stickerSettingsDisposable?.dispose()
self.callDisposable?.dispose()
self.groupCallDisposable?.dispose()
self.callStateDisposable?.dispose()

View file

@ -0,0 +1,49 @@
import Foundation
import TelegramCore
import SwiftSignalKit
public struct ChatSettings: Codable, Equatable {
public let sendWithCmdEnter: Bool
public static var defaultSettings: ChatSettings {
return ChatSettings(sendWithCmdEnter: false)
}
public init(sendWithCmdEnter: Bool) {
self.sendWithCmdEnter = sendWithCmdEnter
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.sendWithCmdEnter = (try container.decode(Int32.self, forKey: "sendWithCmdEnter")) != 0
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode((self.sendWithCmdEnter ? 1 : 0) as Int32, forKey: "sendWithCmdEnter")
}
public static func ==(lhs: ChatSettings, rhs: ChatSettings) -> Bool {
return lhs.sendWithCmdEnter == rhs.sendWithCmdEnter
}
public func withUpdatedSendWithCmdEnter(_ sendWithCmdEnter: Bool) -> ChatSettings {
return ChatSettings(sendWithCmdEnter: sendWithCmdEnter)
}
}
public func updateChatSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (ChatSettings) -> ChatSettings) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.chatSettings, { entry in
let currentSettings: ChatSettings
if let entry = entry?.get(ChatSettings.self) {
currentSettings = entry
} else {
currentSettings = ChatSettings.defaultSettings
}
return SharedPreferencesEntry(f(currentSettings))
})
}
}

View file

@ -43,6 +43,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 {
case drawingSettings = 19
case mediaDisplaySettings = 20
case updateSettings = 21
case chatSettings = 22
}
public struct ApplicationSpecificSharedDataKeys {
@ -68,6 +69,7 @@ public struct ApplicationSpecificSharedDataKeys {
public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue)
public static let mediaDisplaySettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.mediaDisplaySettings.rawValue)
public static let updateSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.updateSettings.rawValue)
public static let chatSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.chatSettings.rawValue)
}
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {