Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mikhail Filimonov 2024-02-27 16:47:41 +04:00
commit 26554ed959
104 changed files with 3598 additions and 1570 deletions

View File

@ -11218,6 +11218,7 @@ Sorry for the inconvenience.";
"GroupBoost.AdditionalFeatures" = "Additional Features";
"GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features.";
"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features.";
"Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group";
"Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members.";

View File

@ -850,6 +850,9 @@ public protocol TelegramRootControllerInterface: NavigationController {
public protocol QuickReplySetupScreenInitialData: AnyObject {
}
public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject {
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }
@ -935,12 +938,13 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController
func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController
func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError>
func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController
func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError>
func navigateToChatController(_ params: NavigateToChatControllerParams)

View File

@ -70,6 +70,14 @@ public enum PremiumDemoSubject {
case messageTags
case lastSeen
case messagePrivacy
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum PremiumLimitSubject {

View File

@ -19,6 +19,7 @@ public enum AttachmentButtonType: Equatable {
case gallery
case file
case location
case quickReply
case contact
case poll
case app(AttachMenuBot)
@ -27,54 +28,60 @@ public enum AttachmentButtonType: Equatable {
public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool {
switch lhs {
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .quickReply:
if case .quickReply = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
}
}
}

View File

@ -217,6 +217,10 @@ private final class AttachButtonComponent: CombinedComponent {
name = ""
imageName = ""
imageFile = nil
case .quickReply:
//TODO:localize
name = "Reply"
imageName = "Chat/Attach Menu/Location"
}
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
@ -1183,6 +1187,9 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
accessibilityTitle = bot.shortName
case .standalone:
accessibilityTitle = ""
case .quickReply:
//TODO:localize
accessibilityTitle = "Reply"
}
buttonView.isAccessibilityElement = true
buttonView.accessibilityLabel = accessibilityTitle

View File

@ -5617,7 +5617,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, dismissed: { [weak self] in
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in
self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
})
navigationController.pushViewController(controller)

View File

@ -41,6 +41,19 @@ private enum ChatListFilterPresetListSection: Int32 {
case tags
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChatListFilterPresetListEntryTag, self == other {
return true
} else {
return false
}
}
}
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
@ -192,7 +205,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
//TODO:localize
return ItemListSwitchItem(presentationData: presentationData, title: "Show Folder Tags", value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateDisplayTags(value)
})
}, tag: ChatListFilterPresetListEntryTag.displayTags)
case .displayTagsFooter:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section)
@ -285,7 +298,7 @@ public enum ChatListFilterPresetListControllerMode {
case modal
}
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController {
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -627,7 +640,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments))
}

View File

@ -1156,6 +1156,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode?
var avatarTapRecognizer: UITapGestureRecognizer?
var avatarMediaNode: AvatarVideoNode?
private var inlineNavigationMarkLayer: SimpleLayer?
@ -1441,7 +1442,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.displaysAsynchronously = true
self.textNode.textNode.layer.anchorPoint = CGPoint()
self.textNode.textNode.anchorPoint = CGPoint()
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false

View File

@ -60,6 +60,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
private let hasBin: Bool
private let isStickerEditor: Bool
weak var drawingView: DrawingView?
public weak var selectionContainerView: DrawingSelectionContainerView?
@ -95,16 +96,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let yAxisView = UIView()
private let angleLayer = SimpleShapeLayer()
private let bin = ComponentView<Empty>()
private let stickerOverlayLayer = SimpleShapeLayer()
private let stickerFrameLayer = SimpleShapeLayer()
public var onInteractionUpdated: (Bool) -> Void = { _ in }
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, size: CGSize, hasBin: Bool = false) {
public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) {
self.context = context
self.size = size
self.hasBin = hasBin
self.isStickerEditor = isStickerEditor
super.init(frame: CGRect(origin: .zero, size: size))
@ -140,6 +145,13 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.opacity = 0.0
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor
self.stickerFrameLayer.fillColor = UIColor.clear.cgColor
self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber]
self.stickerFrameLayer.lineCap = .round
self.addSubview(self.topEdgeView)
self.addSubview(self.leftEdgeView)
self.addSubview(self.rightEdgeView)
@ -148,12 +160,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.addSubview(self.xAxisView)
self.addSubview(self.yAxisView)
self.layer.addSublayer(self.angleLayer)
if isStickerEditor {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func addSubview(_ view: UIView) {
super.addSubview(view)
if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
public override func layoutSubviews() {
super.layoutSubviews()
@ -189,6 +214,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.path = anglePath
self.angleLayer.lineWidth = width
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
let frameWidth = floor(self.bounds.width * 0.97)
let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth))
self.stickerOverlayLayer.frame = self.bounds
let overlayOuterRect = UIBezierPath(rect: self.bounds)
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil))
let overlayLineWidth: CGFloat = 2.0 * 2.2
overlayOuterRect.append(overlayInnerRect)
overlayOuterRect.usesEvenOddFillRule = true
self.stickerOverlayLayer.path = overlayOuterRect.cgPath
self.stickerOverlayLayer.fillRule = .evenOdd
self.stickerFrameLayer.frame = self.bounds
self.stickerFrameLayer.lineWidth = overlayLineWidth
self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil)
}
public var entities: [DrawingEntity] {
@ -841,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
self.selectEntity(viewToSelect.entity, animate: false)
self.onInteractionUpdated(true)
} else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
} else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePan(gestureRecognizer)
}
}

View File

@ -192,6 +192,7 @@ private final class StickerSelectionComponent: Component {
insertText: { _ in
},
backwardsDeleteText: {},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self, let controller = self.component?.getController() {
controller.present(c, in: .window(.root), with: a)

View File

@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
})
if case let .share(_, selfPeer, _) = self.mode {
switch self.mode {
case let .share(_, selfPeer, _):
if let selfPeer {
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer)
}
self.headerNode.mapNode.hasPickerAnnotation = true
case .pick:
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true)
self.headerNode.mapNode.hasPickerAnnotation = true
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in

View File

@ -155,6 +155,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
case wallpaper
case story
case addImage
case createSticker
}
case assets(PHAssetCollection?, AssetsMode)
@ -273,7 +274,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.presentationData = controller.presentationData
var assetType: PHAssetMediaType?
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) {
assetType = .image
}
let mediaAssetsContext = MediaAssetsContext(assetType: assetType)
@ -432,7 +433,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) {
} else {
let selectionGesture = MediaPickerGridSelectionGesture<TGMediaSelectableItem>()
@ -1566,7 +1567,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
} else {
switch mode {
case .default:
case .default, .createSticker:
self.titleView.title = presentationData.strings.MediaPicker_Recents
self.titleView.isEnabled = true
case .story:
@ -2258,15 +2259,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return self.controllerNode.defaultTransitionView()
}
fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource)
}
fileprivate func transitionImage(for identifier: String) -> UIImage? {
public func transitionImage(for identifier: String) -> UIImage? {
return self.controllerNode.transitionImage(for: identifier)
}
func updateHiddenMediaId(_ id: String?) {
public func updateHiddenMediaId(_ id: String?) {
self.controllerNode.hiddenMediaId.set(.single(id))
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,61 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
private let sceneVersion: Int = 1
final class BadgeBusinessView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "business", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View File

@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "badge", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "emoji", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "tag", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -12,7 +12,7 @@ import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
public final class BoostHeaderBackgroundComponent: Component {
let isVisible: Bool
@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component {
private func setup() {
guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else {
return
}

View File

@ -13,8 +13,6 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
private let sceneVersion: Int = 3
class EmojiHeaderComponent: Component {
let context: AccountContext
let animationCache: AnimationCache

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -14,7 +14,7 @@ import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
final class GiftAvatarComponent: Component {
let context: AccountContext
@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component {
}
private func setup() {
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else {
return
}

View File

@ -371,6 +371,7 @@ final class PhoneDemoComponent: Component {
case emoji
case hello
case tag
case business
}
enum Model {
@ -547,6 +548,13 @@ final class PhoneDemoComponent: Component {
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
case .business:
if let _ = self.decorationView as? BadgeBusinessView {
} else {
let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
}
self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position)

View File

@ -698,7 +698,7 @@ private final class SheetContent: CombinedComponent {
isCurrent = mode == .current
}
case .features:
textString = strings.GroupBoost_AdditionalFeaturesText
textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText
}
let defaultTitle = strings.ChannelBoost_Level("\(level)").string

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
private let sceneVersion: Int = 2
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -223,24 +223,7 @@ class PremiumCoinComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "coin", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "coin_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "coin", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else {
return
}
@ -316,8 +299,8 @@ class PremiumCoinComponent: Component {
return
}
let fromScale: Float = 0.85
let toScale: Float = 0.9
let fromScale: Float = 0.9
let toScale: Float = 1.0
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0

View File

@ -1177,7 +1177,7 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_LastSeenInfo
case .messagePrivacy:
text = strings.Premium_MessagePrivacyInfo
case .doubleLimits, .stories, .business:
default:
text = ""
}
@ -1392,6 +1392,14 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum Source: Equatable {

View File

@ -533,6 +533,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let buttonText: String

View File

@ -439,6 +439,15 @@ public enum PremiumPerk: CaseIterable {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
public static var allCases: [PremiumPerk] {
return [
@ -520,6 +529,8 @@ public enum PremiumPerk: CaseIterable {
return "message_privacy"
case .business:
return "business"
default:
return ""
}
}
@ -567,6 +578,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacy
case .business:
return strings.Premium_Business
default:
return ""
}
}
@ -614,6 +627,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacyInfo
case .business:
return strings.Premium_BusinessInfo
default:
return ""
}
}
@ -661,6 +676,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/MessagePrivacy"
case .business:
return "Premium/Perk/Business"
default:
return ""
}
}
}
@ -1948,75 +1965,77 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
))),
action: { [weak state] _ in
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
))))
i += 1
@ -2100,43 +2119,92 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
foregroundColor: .white,
iconName: perk.iconName
))),
action: { _ in
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
action: { [weak state] _ in
let isPremium = state?.isPremium == true
if isPremium {
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
})
case .awayMessages:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
})
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}
} else {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .location:
demoSubject = .businessLocation
case .hours:
demoSubject = .businessHours
case .quickReplies:
demoSubject = .businessQuickReplies
case .greetings:
demoSubject = .businessGreetingMessage
case .awayMessages:
demoSubject = .businessAwayMessage
case .chatbots:
demoSubject = .businessChatBots
}
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: [.businessLocation, .businessHours, .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, .businessChatBots], buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false))
case .awayMessages:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true))
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
}
}
))))
@ -2238,7 +2306,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
iconName: "Premium/BusinessPerk/Tag"
))),
action: { _ in
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, dismissed: nil))
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
}
))))

View File

@ -833,6 +833,129 @@ public class PremiumLimitsListScreen: ViewController {
)
)
)
availableItems[.businessLocation] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessLocation,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_location"],
decoration: .business
)),
title: strings.Business_Location,
text: strings.Business_LocationInfo,
textColor: textColor
)
)
)
)
availableItems[.businessHours] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessHours,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_hours"],
decoration: .business
)),
title: strings.Business_OpeningHours,
text: strings.Business_OpeningHoursInfo,
textColor: textColor
)
)
)
)
availableItems[.businessQuickReplies] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessQuickReplies,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_QuickReplies,
text: strings.Business_QuickRepliesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessGreetingMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessGreetingMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_GreetingMessages,
text: strings.Business_GreetingMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessAwayMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessAwayMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["away_message"],
decoration: .business
)),
title: strings.Business_AwayMessages,
text: strings.Business_AwayMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessChatBots] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessChatBots,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_bots"],
decoration: .business
)),
title: strings.Business_Chatbots,
text: strings.Business_ChatbotsInfo,
textColor: textColor
)
)
)
)
if let order = controller.order {
var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] }

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 6
private let sceneVersion: Int = 7
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -45,7 +45,31 @@ private func generateDiffuseTexture() -> UIImage {
})!
}
class PremiumStarComponent: Component {
func loadCompressedScene(name: String, version: Int) -> SCNScene? {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: name, withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "\(name)_\(version).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: name, withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return nil
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
return nil
}
return scene
}
final class PremiumStarComponent: Component {
let isIntro: Bool
let isVisible: Bool
let hasIdleAnimations: Bool
@ -251,24 +275,7 @@ class PremiumStarComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "star", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "star_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "star", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else {
return
}

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import SwiftSignalKit
private let sceneVersion: Int = 1
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class SwirlStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -876,7 +876,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = messages[0].0.threadId, !"".isEmpty {
if let threadId = messages[0].0.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1008,7 +1008,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = messages[0].0.threadId, !"".isEmpty {
if let threadId = messages[0].0.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1319,7 +1319,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1394,7 +1394,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1413,7 +1413,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1487,7 +1487,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)

View File

@ -314,19 +314,37 @@ public final class TelegramBusinessHours: Equatable, Codable {
public func splitIntoWeekDays() -> [WeekDay] {
var mappedDays: [[WorkingTimeInterval]] = Array(repeating: [], count: 7)
var weekMinutes = IndexSet()
for interval in self.weeklyTimeIntervals {
let startDayIndex = interval.startMinute / (24 * 60)
if startDayIndex < 0 || startDayIndex >= 7 {
continue
weekMinutes.insert(integersIn: interval.startMinute ..< interval.endMinute)
}
for i in 0 ..< mappedDays.count {
let dayRange = i * 24 * 60 ..< (i + 1) * 24 * 60
var removeMinutes = IndexSet()
inner: for range in weekMinutes.rangeView {
if range.lowerBound >= dayRange.upperBound {
break inner
} else {
let clippedRange: Range<Int>
if range.lowerBound == dayRange.lowerBound {
clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound)
} else {
clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound + 12 * 60)
}
let startTimeInsideDay = clippedRange.lowerBound - i * (24 * 60)
let endTimeInsideDay = clippedRange.upperBound - i * (24 * 60)
mappedDays[i].append(WorkingTimeInterval(
startMinute: startTimeInsideDay,
endMinute: endTimeInsideDay
))
removeMinutes.insert(integersIn: clippedRange)
}
}
let startTimeInsideDay = interval.startMinute - startDayIndex * (24 * 60)
let endTimeInsideDay = interval.endMinute - startDayIndex * (24 * 60)
mappedDays[startDayIndex].append(WorkingTimeInterval(
startMinute: startTimeInsideDay,
endMinute: endTimeInsideDay
))
weekMinutes.subtract(removeMinutes)
}
return mappedDays.map { day -> WeekDay in

View File

@ -1191,7 +1191,7 @@ public extension TelegramEngine {
}
var selectedMedia: EngineMedia
if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), !preferHighQuality {
if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = EngineMedia(media)

View File

@ -755,7 +755,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
var displayTranscribe = false
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
displayTranscribe = false
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
if arguments.associatedData.isPremium {
displayTranscribe = true

View File

@ -572,6 +572,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -232,6 +232,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void
public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void
public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void
public let openStickerEditor: () -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void
@ -355,6 +356,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void,
openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void,
openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void,
openStickerEditor: @escaping () -> Void,
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
dismissTextInput: @escaping () -> Void,
@ -458,6 +460,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.openPremiumStatusInfo = openPremiumStatusInfo
self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu
self.openGroupBoostInfo = openGroupBoostInfo
self.openStickerEditor = openStickerEditor
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures

View File

@ -56,6 +56,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let dismissTextInput: () -> Void
let insertText: (NSAttributedString) -> Void
let backwardsDeleteText: () -> Void
let openStickerEditor: () -> Void
let presentController: (ViewController, Any?) -> Void
let presentGlobalOverlayController: (ViewController, Any?) -> Void
let getNavigationController: () -> NavigationController?
@ -72,6 +73,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
dismissTextInput: @escaping () -> Void,
insertText: @escaping (NSAttributedString) -> Void,
backwardsDeleteText: @escaping () -> Void,
openStickerEditor: @escaping () -> Void,
presentController: @escaping (ViewController, Any?) -> Void,
presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void,
getNavigationController: @escaping () -> NavigationController?,
@ -86,6 +88,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.dismissTextInput = dismissTextInput
self.insertText = insertText
self.backwardsDeleteText = backwardsDeleteText
self.openStickerEditor = openStickerEditor
self.presentController = presentController
self.presentGlobalOverlayController = presentGlobalOverlayController
self.getNavigationController = getNavigationController
@ -106,6 +109,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.dismissTextInput = chatControllerInteraction.dismissTextInput
self.insertText = panelInteraction.insertText
self.backwardsDeleteText = panelInteraction.backwardsDeleteText
self.openStickerEditor = chatControllerInteraction.openStickerEditor
self.presentController = chatControllerInteraction.presentController
self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController
self.getNavigationController = chatControllerInteraction.navigationController
@ -1140,6 +1144,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
guard let file = item.itemFile else {
if groupId == AnyHashable("recent"), case .icon(.add) = item.content {
interaction.openStickerEditor()
}
return
}

View File

@ -16,6 +16,8 @@ public final class EmptyStateIndicatorComponent: Component {
public let text: String
public let actionTitle: String?
public let action: () -> Void
public let additionalActionTitle: String?
public let additionalAction: () -> Void
public init(
context: AccountContext,
@ -24,7 +26,9 @@ public final class EmptyStateIndicatorComponent: Component {
title: String,
text: String,
actionTitle: String?,
action: @escaping () -> Void
action: @escaping () -> Void,
additionalActionTitle: String?,
additionalAction: @escaping () -> Void
) {
self.context = context
self.theme = theme
@ -33,6 +37,8 @@ public final class EmptyStateIndicatorComponent: Component {
self.text = text
self.actionTitle = actionTitle
self.action = action
self.additionalActionTitle = additionalActionTitle
self.additionalAction = additionalAction
}
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
@ -54,6 +60,9 @@ public final class EmptyStateIndicatorComponent: Component {
if lhs.actionTitle != rhs.actionTitle {
return false
}
if lhs.additionalActionTitle != rhs.additionalActionTitle {
return false
}
return true
}
@ -65,6 +74,7 @@ public final class EmptyStateIndicatorComponent: Component {
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var button: ComponentView<Empty>?
private var additionalButton: ComponentView<Empty>?
override public init(frame: CGRect) {
super.init(frame: frame)
@ -139,7 +149,7 @@ public final class EmptyStateIndicatorComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
containerSize: CGSize(width: 260.0, height: 50.0)
)
} else {
if let button = self.button {
@ -148,14 +158,52 @@ public final class EmptyStateIndicatorComponent: Component {
}
}
var additionalButtonSize: CGSize?
if let additionalActionTitle = component.additionalActionTitle {
let additionalButton: ComponentView<Empty>
if let current = self.additionalButton {
additionalButton = current
} else {
additionalButton = ComponentView()
self.additionalButton = additionalButton
}
additionalButtonSize = additionalButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(
text: additionalActionTitle, font:
Font.regular(17.0),
color: component.theme.list.itemAccentColor)
),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.additionalAction()
}
)),
environment: {},
containerSize: CGSize(width: 262.0, height: 50.0)
)
} else {
if let additionalButton = self.additionalButton {
self.additionalButton = nil
additionalButton.view?.removeFromSuperview()
}
}
let animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 21.0
var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height
if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height
}
if let additionalButtonSize {
totalHeight += buttonSpacing + additionalButtonSize.height
}
var contentY = floor((availableSize.height - totalHeight) * 0.5)
@ -185,7 +233,14 @@ public final class EmptyStateIndicatorComponent: Component {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
contentY += buttonSize.height
contentY += buttonSize.height + buttonSpacing
}
if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
if additionalButtonView.superview == nil {
self.addSubview(additionalButtonView)
}
transition.setFrame(view: additionalButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - additionalButtonSize.width) * 0.5), y: contentY), size: additionalButtonSize))
contentY += additionalButtonSize.height
}
return availableSize

View File

@ -2519,6 +2519,7 @@ public final class EmojiPagerContentComponent: Component {
case premiumStar
case topic(String, Int32)
case stop
case add
}
case animation(EntityKeyboardAnimationData)
@ -3559,6 +3560,15 @@ public final class EmojiPagerContentComponent: Component {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .add:
context.setFillColor(UIColor.black.withAlphaComponent(0.08).cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0))
context.setFillColor(UIColor.black.withAlphaComponent(0.16).cgColor)
let plusSize = CGSize(width: 4.5, height: 31.5)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath)
context.fillPath()
}
UIGraphicsPopContext()

View File

@ -1784,6 +1784,7 @@ public extension EmojiPagerContentComponent {
}
if let recentStickers = recentStickers {
let groupId = "recent"
for item in recentStickers.items {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
@ -1807,7 +1808,6 @@ public extension EmojiPagerContentComponent {
tintMode: tintMode
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {

View File

@ -66,6 +66,7 @@ public final class ListActionItemComponent: Component {
public let theme: PresentationTheme
public let title: AnyComponent<Empty>
public let contentInsets: UIEdgeInsets
public let leftIcon: AnyComponentWithIdentity<Empty>?
public let icon: Icon?
public let accessory: Accessory?
@ -74,6 +75,7 @@ public final class ListActionItemComponent: Component {
public init(
theme: PresentationTheme,
title: AnyComponent<Empty>,
contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0),
leftIcon: AnyComponentWithIdentity<Empty>? = nil,
icon: Icon? = nil,
accessory: Accessory? = .arrow,
@ -81,6 +83,7 @@ public final class ListActionItemComponent: Component {
) {
self.theme = theme
self.title = title
self.contentInsets = contentInsets
self.leftIcon = leftIcon
self.icon = icon
self.accessory = accessory
@ -94,6 +97,9 @@ public final class ListActionItemComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.leftIcon != rhs.leftIcon {
return false
}
@ -172,13 +178,15 @@ public final class ListActionItemComponent: Component {
let themeUpdated = component.theme !== previousComponent?.theme
let verticalInset: CGFloat = 12.0
var contentLeftInset: CGFloat = 16.0
let contentRightInset: CGFloat
switch component.accessory {
case .none:
contentRightInset = 16.0
if let _ = component.icon {
contentRightInset = 42.0
} else {
contentRightInset = 16.0
}
case .arrow:
contentRightInset = 30.0
case .toggle:
@ -186,10 +194,10 @@ public final class ListActionItemComponent: Component {
}
var contentHeight: CGFloat = 0.0
contentHeight += verticalInset
contentHeight += component.contentInsets.top
if component.leftIcon != nil {
contentLeftInset += 46.0
contentLeftInset += 52.0
}
let titleSize = self.title.update(
@ -198,7 +206,7 @@ public final class ListActionItemComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - contentLeftInset - contentRightInset, height: availableSize.height)
)
let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize)
let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
@ -208,7 +216,7 @@ public final class ListActionItemComponent: Component {
}
contentHeight += titleSize.height
contentHeight += verticalInset
contentHeight += component.contentInsets.bottom
if let iconValue = component.icon {
if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon {
@ -239,7 +247,7 @@ public final class ListActionItemComponent: Component {
var iconOffset: CGFloat = 0.0
if case .none = component.accessory {
iconOffset = 6.0
iconOffset = 26.0
}
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)

View File

@ -208,7 +208,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
containerSize: availableSize
)
let size = textFieldSize
let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0)
let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize)
if let textFieldView = self.textField.view {

View File

@ -25,8 +25,8 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]],
fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]],
texture2d<half, access::sample> texture [[texture(0)]]) {
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
half3 color = texture.sample(samplr, in.texCoord).rgb;
return half4(color, 1.0);
half4 color = texture.sample(samplr, in.texCoord);
return color;
}
fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
@ -39,12 +39,12 @@ fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
}
typedef struct {
float3 topColor;
float3 bottomColor;
float4 topColor;
float4 bottomColor;
} GradientColors;
fragment half4 gradientFragmentShader(RasterizerData in [[stage_in]],
constant GradientColors& colors [[buffer(0)]]) {
return half4(half3(mix(colors.topColor, colors.bottomColor, in.texCoord.y)), 1.0);
return half4(half3(mix(colors.topColor.rgb, colors.bottomColor.rgb, in.texCoord.y)), 1.0);
}

View File

@ -8,7 +8,7 @@ import VideoToolbox
private let queue = Queue()
public func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> Signal<UIImage?, NoError> {
if #available(iOS 17.0, *) {
guard let cgImage = image.cgImage else {
return .single(nil)
@ -23,21 +23,26 @@ public func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError>
subscriber.putCompletion()
return
}
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
let filter = CIFilter.blendWithMask()
filter.inputImage = inputImage
filter.backgroundImage = CIImage(color: .clear)
filter.maskImage = CIImage(cvPixelBuffer: mask)
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
let image = UIImage(cgImage: cgImage)
subscriber.putNext(image)
subscriber.putCompletion()
return
if onlyCheck {
subscriber.putNext(UIImage())
subscriber.putCompletion()
} else {
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
let filter = CIFilter.blendWithMask()
filter.inputImage = inputImage
filter.backgroundImage = CIImage(color: .clear)
filter.maskImage = CIImage(cvPixelBuffer: mask)
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
let image = UIImage(cgImage: cgImage)
subscriber.putNext(image)
subscriber.putCompletion()
return
}
}
subscriber.putNext(nil)
subscriber.putCompletion()
}
subscriber.putNext(nil)
subscriber.putCompletion()
}
try? handler.perform([request])
return ActionDisposable {

View File

@ -94,6 +94,11 @@ public final class MediaEditor {
}
}
public enum Mode {
case `default`
case sticker
}
public enum Subject {
case image(UIImage, PixelDimensions)
case video(String, UIImage?, Bool, String?, PixelDimensions, Double)
@ -116,6 +121,7 @@ public final class MediaEditor {
}
private let context: AccountContext
private let mode: Mode
private let subject: Subject
private let clock = CMClockGetHostTimeClock()
@ -182,6 +188,9 @@ public final class MediaEditor {
}
}
public private(set) var canCutout: Bool = false
public var canCutoutUpdated: (Bool) -> Void = { _ in }
private var textureCache: CVMetalTextureCache!
public var hasPortraitMask: Bool {
@ -391,8 +400,9 @@ public final class MediaEditor {
}
}
public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
self.context = context
self.mode = mode
self.subject = subject
if let values {
self.values = values
@ -668,6 +678,19 @@ public final class MediaEditor {
} else {
textureSource.setMainInput(.image(image))
}
if case .sticker = self.mode {
let _ = (cutoutStickerImage(from: image, onlyCheck: true)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, result != nil else {
return
}
self.canCutout = true
self.canCutoutUpdated(true)
})
}
}
if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference {
textureSource.setMainInput(.video(playerItem))
@ -677,7 +700,12 @@ public final class MediaEditor {
}
self.renderer.textureSource = textureSource
self.setGradientColors(textureSourceResult.gradientColors)
switch self.mode {
case .default:
self.setGradientColors(textureSourceResult.gradientColors)
case .sticker:
self.setGradientColors(GradientColors(top: .clear, bottom: .clear))
}
if let _ = textureSourceResult.player {
self.updateRenderChain()
@ -1615,7 +1643,7 @@ public final class MediaEditor {
public func setGradientColors(_ gradientColors: GradientColors) {
self.gradientColorsPromise.set(.single(gradientColors))
self.updateValues(mode: .skipRendering) { values in
self.updateValues(mode: self.sourceIsVideo ? .skipRendering : .generic) { values in
return values.withUpdatedGradientColors(gradientColors: gradientColors.array)
}
}

View File

@ -178,7 +178,7 @@ final class MediaEditorComposer {
}
}
public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) {
public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, outputDimensions: CGSize? = nil, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])!
var drawingImage: CIImage?
@ -192,7 +192,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: entity.entity, colorSpace: colorSpace))
}
makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in
makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: outputDimensions ?? dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in
if let ciImage {
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
Queue.mainQueue().async {
@ -206,20 +206,19 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
}
private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, outputDimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, textScale: CGFloat = 1.0, completion: @escaping (CIImage?) -> Void) {
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
var isClear = false
if let gradientColor = values.gradientColors?.first, gradientColor.alpha.isZero {
isClear = true
}
var resultImage = CIImage(color: isClear ? .clear : .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
var initialScale: CGFloat
if mediaImage.extent.height > mediaImage.extent.width && values.isStory {
initialScale = max(dimensions.width / mediaImage.extent.width, dimensions.height / mediaImage.extent.height)
} else {
initialScale = dimensions.width / mediaImage.extent.width
}
if values.isStory {
resultImage = mediaImage.samplingLinear().composited(over: resultImage)
} else {
let initialScale = dimensions.width / mediaImage.extent.width
var horizontalScale = initialScale
if values.cropMirroring {
horizontalScale *= -1.0

View File

@ -634,6 +634,10 @@ public final class MediaEditorValues: Codable, Equatable {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
@ -716,6 +720,10 @@ public final class MediaEditorValues: Codable, Equatable {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: qualityPreset)
}
public var resultDimensions: PixelDimensions {
if self.videoIsFullHd {
return PixelDimensions(width: 1080, height: 1920)

View File

@ -165,6 +165,7 @@ final class OutputRenderPass: DefaultRenderPass {
renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
let drawableSize = renderTarget.drawableSize

View File

@ -128,15 +128,6 @@ final class UniversalTextureSource: TextureSource {
self.update()
}
}
//
// private func setupDisplayLink(frameRate: Int) {
// self.displayLink?.invalidate()
// self.displayLink = nil
//
// if self.playerItemOutput != nil {
// }
// }
}
private protocol InputContext {

View File

@ -233,7 +233,7 @@ final class VideoFinishPass: RenderPass {
}
private let canvasSize = CGSize(width: 1080.0, height: 1920.0)
private var gradientColors = GradientColors(topColor: simd_float3(0.0, 0.0, 0.0), bottomColor: simd_float3(0.0, 0.0, 0.0))
private var gradientColors = GradientColors(topColor: simd_float4(0.0, 0.0, 0.0, 0.0), bottomColor: simd_float4(0.0, 0.0, 0.0, 0.0))
func update(values: MediaEditorValues, videoDuration: Double?, additionalVideoDuration: Double?) {
let position = CGPoint(
x: canvasSize.width / 2.0 + values.cropOffset.x,
@ -241,6 +241,7 @@ final class VideoFinishPass: RenderPass {
)
self.isStory = values.isStory
self.isSticker = values.gradientColors?.first?.alpha == 0.0
self.mainPosition = VideoFinishPass.VideoPosition(position: position, size: self.mainPosition.size, scale: values.cropScale, rotation: values.cropRotation, baseScale: self.mainPosition.baseScale)
if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation {
@ -262,12 +263,12 @@ final class VideoFinishPass: RenderPass {
}
if let gradientColors = values.gradientColors, let top = gradientColors.first, let bottom = gradientColors.last {
let (topRed, topGreen, topBlue, _) = top.components
let (bottomRed, bottomGreen, bottomBlue, _) = bottom.components
let (topRed, topGreen, topBlue, topAlpha) = top.components
let (bottomRed, bottomGreen, bottomBlue, bottomAlpha) = bottom.components
self.gradientColors = GradientColors(
topColor: simd_float3(Float(topRed), Float(topGreen), Float(topBlue)),
bottomColor: simd_float3(Float(bottomRed), Float(bottomGreen), Float(bottomBlue))
topColor: simd_float4(Float(topRed), Float(topGreen), Float(topBlue), Float(topAlpha)),
bottomColor: simd_float4(Float(bottomRed), Float(bottomGreen), Float(bottomBlue), Float(bottomAlpha))
)
}
}
@ -289,6 +290,7 @@ final class VideoFinishPass: RenderPass {
)
private var isStory = true
private var isSticker = true
private var videoPositionChanges: [VideoPositionChange] = []
private var videoDuration: Double?
private var additionalVideoDuration: Double?
@ -482,10 +484,18 @@ final class VideoFinishPass: RenderPass {
}
let baseScale: CGFloat
if input.height > input.width {
baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height))
if !self.isSticker {
if input.height > input.width {
baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height))
} else {
baseScale = canvasSize.width / CGFloat(input.width)
}
} else {
baseScale = canvasSize.width / CGFloat(input.width)
if input.height > input.width {
baseScale = canvasSize.width / CGFloat(input.width)
} else {
baseScale = canvasSize.width / CGFloat(input.height)
}
}
self.mainPosition = self.mainPosition.with(size: CGSize(width: input.width, height: input.height), baseScale: baseScale)
@ -508,9 +518,13 @@ final class VideoFinishPass: RenderPass {
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
if self.gradientColors.topColor.w > 0.0 {
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
} else {
renderPassDescriptor.colorAttachments[0].loadAction = .clear
}
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return input
}
@ -521,12 +535,14 @@ final class VideoFinishPass: RenderPass {
znear: -1.0, zfar: 1.0)
)
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
self.encodeGradient(
using: renderCommandEncoder,
containerSize: containerSize,
device: device
)
if self.gradientColors.topColor.w > 0.0 {
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
self.encodeGradient(
using: renderCommandEncoder,
containerSize: containerSize,
device: device
)
}
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
@ -578,8 +594,8 @@ final class VideoFinishPass: RenderPass {
}
struct GradientColors {
var topColor: simd_float3
var bottomColor: simd_float3
var topColor: simd_float4
var bottomColor: simd_float4
}
func encodeGradient(

View File

@ -50,6 +50,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/Components/BlurredBackgroundComponent",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,363 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import DrawingUI
import MediaEditor
import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
private final class MediaCutoutScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mediaEditor: MediaEditor
init(
context: AccountContext,
mediaEditor: MediaEditor
) {
self.context = context
self.mediaEditor = mediaEditor
}
static func ==(lhs: MediaCutoutScreenComponent, rhs: MediaCutoutScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
public final class View: UIView {
private let buttonsContainerView = UIView()
private let buttonsBackgroundView = UIView()
private let cancelButton = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private var component: MediaCutoutScreenComponent?
private weak var state: State?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
self.buttonsContainerView.clipsToBounds = true
super.init(frame: frame)
self.backgroundColor = .clear
self.addSubview(self.buttonsContainerView)
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateInFromEditor() {
self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
private var animatingOut = false
func animateOutToEditor(completion: @escaping () -> Void) {
self.animatingOut = true
self.cancelButton.view?.isHidden = true
self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.state?.updated()
}
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.component = component
self.state = state
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor
let buttonSideInset: CGFloat
let buttonBottomInset: CGFloat = 8.0
var controlsBottomInset: CGFloat = 0.0
let previewSize: CGSize
var topInset: CGFloat = environment.statusBarHeight + 5.0
if isTablet {
let previewHeight = availableSize.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
buttonSideInset = 30.0
} else {
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
buttonSideInset = 10.0
if availableSize.height < previewSize.height + 30.0 {
topInset = 0.0
controlsBottomInset = -75.0
} else {
self.buttonsBackgroundView.backgroundColor = .clear
}
}
// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset))
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: {
guard let controller = environment.controller() as? MediaCutoutScreen else {
return
}
controller.requestDismiss(reset: true, animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: buttonSideInset, y: buttonBottomInset),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.buttonsContainerView.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: "Tap an object to cut it out", font: Font.regular(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0)
)
let labelFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: buttonBottomInset + 4.0),
size: labelSize
)
if let labelView = self.label.view {
if labelView.superview == nil {
self.buttonsContainerView.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class MediaCutoutScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: MediaCutoutScreen?
private let context: AccountContext
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: MediaCutoutScreen) {
self.controller = controller
self.context = controller.context
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.backgroundColor = .clear
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateInFromEditor() {
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateInFromEditor()
}
}
func animateOutToEditor(completion: @escaping () -> Void) {
if let mediaEditor = self.controller?.mediaEditor {
mediaEditor.play()
}
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateOutToEditor(completion: completion)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
if isTablet {
let previewHeight = layout.size.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
}
let bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: bottomInset,
right: layout.safeInsets.right
),
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
MediaCutoutScreenComponent(
context: self.context,
mediaEditor: controller.mediaEditor
)
),
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
componentView.clipsToBounds = true
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
if isFirstTime {
self.animateInFromEditor()
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
fileprivate let context: AccountContext
fileprivate let mediaEditor: MediaEditor
public var dismissed: () -> Void = {}
private var initialValues: MediaEditorValues
public init(context: AccountContext, mediaEditor: MediaEditor) {
self.context = context
self.mediaEditor = mediaEditor
self.initialValues = mediaEditor.values.makeCopy()
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
func requestDismiss(reset: Bool, animated: Bool) {
if reset {
self.mediaEditor.values = self.initialValues
}
self.dismissed()
self.node.animateOutToEditor(completion: {
self.dismiss()
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
}
}

View File

@ -139,6 +139,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -11,8 +11,9 @@ import TelegramCore
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> String {
private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay, offsetMinutes: Int) -> String {
var businessHoursText: String = ""
switch day {
case .open:
@ -26,6 +27,8 @@ private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> Strin
var resultText: String = ""
for range in intervals {
let range = TelegramBusinessHours.WorkingTimeInterval(startMinute: range.startMinute + offsetMinutes, endMinute: range.endMinute + offsetMinutes)
if !resultText.isEmpty {
resultText.append("\n")
}
@ -47,13 +50,13 @@ final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let businessHours: TelegramBusinessHours
let requestLayout: () -> Void
let requestLayout: (Bool) -> Void
init(
id: AnyHashable,
label: String,
businessHours: TelegramBusinessHours,
requestLayout: @escaping () -> Void
requestLayout: @escaping (Bool) -> Void
) {
self.id = id
self.label = label
@ -79,6 +82,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
private let labelNode: ImmediateTextNode
private let currentStatusText = ComponentView<Empty>()
private let currentDayText = ComponentView<Empty>()
private var timezoneSwitchButton: ComponentView<Empty>?
private var dayTitles: [ComponentView<Empty>] = []
private var dayValues: [ComponentView<Empty>] = []
private let arrowIcon = ComponentView<Empty>()
@ -90,6 +94,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
private var item: PeerInfoScreenBusinessHoursItem?
private var theme: PresentationTheme?
private var currentTimezone: TimeZone
private var displayLocalTimezone: Bool = false
private var cachedDays: [TelegramBusinessHours.WeekDay] = []
private var cachedWeekMinuteSet = IndexSet()
@ -115,6 +121,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
self.activateArea = AccessibilityAreaNode()
self.currentTimezone = TimeZone.current
super.init()
self.addSubnode(self.bottomSeparatorNode)
@ -179,7 +187,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
switch gesture {
case .tap, .longTap:
self.isExpanded = !self.isExpanded
self.item?.requestLayout()
self.item?.requestLayout(true)
default:
break
}
@ -255,11 +263,16 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
let currentHour = currentCalendar.component(.hour, from: currentDate)
let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute
var timezoneOffsetMinutes: Int = 0
if self.displayLocalTimezone {
timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute)
//TODO:localize
let openStatusText = isOpen ? "Open" : "Closed"
var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " "
var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex], offsetMinutes: timezoneOffsetMinutes) : " "
if !isOpen {
for range in self.cachedWeekMinuteSet.rangeView {
@ -325,6 +338,61 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
environment: {},
containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0)
)
var timezoneSwitchButtonSize: CGSize?
if item.businessHours.timezoneId != self.currentTimezone.identifier {
let timezoneSwitchButton: ComponentView<Empty>
if let current = self.timezoneSwitchButton {
timezoneSwitchButton = current
} else {
timezoneSwitchButton = ComponentView()
self.timezoneSwitchButton = timezoneSwitchButton
}
let timezoneSwitchTitle: String
//TODO:localize
if self.displayLocalTimezone {
timezoneSwitchTitle = "my time"
} else {
timezoneSwitchTitle = "local time"
}
timezoneSwitchButtonSize = timezoneSwitchButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: timezoneSwitchTitle, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor))
)),
background: AnyComponent(RoundedRectangle(
color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
cornerRadius: nil
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 2.0, right: 7.0),
action: { [weak self] in
guard let self else {
return
}
self.displayLocalTimezone = !self.displayLocalTimezone
self.item?.requestLayout(false)
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
if let timezoneSwitchButton = self.timezoneSwitchButton {
self.timezoneSwitchButton = nil
timezoneSwitchButton.view?.removeFromSuperview()
}
}
let timezoneSwitchButtonSpacing: CGFloat = 3.0
if timezoneSwitchButtonSize != nil {
topOffset -= 20.0
}
let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize)
if let currentDayTextView = self.currentDayText.view {
if currentDayTextView.superview == nil {
@ -337,6 +405,20 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
topOffset += max(currentStatusTextSize.height, currentDayTextSize.height)
if let timezoneSwitchButtonView = self.timezoneSwitchButton?.view, let timezoneSwitchButtonSize {
topOffset += timezoneSwitchButtonSpacing
var timezoneSwitchButtonTransition = transition
if timezoneSwitchButtonView.superview == nil {
timezoneSwitchButtonTransition = .immediate
self.contextSourceNode.contentNode.view.addSubview(timezoneSwitchButtonView)
}
let timezoneSwitchButtonFrame = CGRect(origin: CGPoint(x: width - dayRightInset - timezoneSwitchButtonSize.width, y: topOffset), size: timezoneSwitchButtonSize)
timezoneSwitchButtonTransition.updateFrame(view: timezoneSwitchButtonView, frame: timezoneSwitchButtonFrame)
topOffset += timezoneSwitchButtonSize.height
}
let daySpacing: CGFloat = 15.0
var dayHeights: CGFloat = 0.0
@ -383,7 +465,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(businessDays[i])
let businessHoursText = dayBusinessHoursText(businessDays[i], offsetMinutes: timezoneOffsetMinutes)
let dayTitleSize = dayTitle.update(
transition: .immediate,

View File

@ -1166,8 +1166,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if let businessHours = cachedData.businessHours {
//TODO:localize
items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: {
interaction.requestLayout(true)
items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: { animated in
interaction.requestLayout(animated)
}))
}
@ -3074,6 +3074,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -8795,7 +8796,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})
case .chatFolders:
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, dismissed: nil)
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil)
push(controller)
case .notificationsAndSounds:
if let settings = self.data?.globalSettings {

View File

@ -295,7 +295,23 @@ final class PeerInfoStoryGridScreenComponent: Component {
let _ = paneNode.scrollToTop()
}
func openCreateStory() {
guard let component = self.component else {
return
}
if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(customTarget: nil, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil })
coordinator?.animateIn()
}
}
private var isUpdating = false
func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
@ -313,7 +329,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
var bottomInset: CGFloat = environment.safeInsets.bottom
if self.selectedCount != 0 {
if self.selectedCount != 0 || (component.scope == .saved && self.paneNode?.isEmpty == false) {
let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = transition
if let current = self.selectionPanel {
@ -327,7 +343,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
let buttonText: String
switch component.scope {
case .saved:
buttonText = environment.strings.ChatList_Context_Archive
buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction
case .archive:
buttonText = environment.strings.StoryList_SaveToProfile
}
@ -344,7 +360,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
guard let paneNode = self.paneNode else {
return
}
@ -361,21 +377,25 @@ final class PeerInfoStoryGridScreenComponent: Component {
switch component.scope {
case .saved:
let selectedCount = paneNode.selectedItems.count
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
if selectedCount == 0 {
self.openCreateStory()
} else {
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
case .archive:
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: true).start()
@ -449,10 +469,28 @@ final class PeerInfoStoryGridScreenComponent: Component {
},
listContext: nil
)
paneNode.isEmptyUpdated = { [weak self] _ in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
self.paneNode = paneNode
self.addSubview(paneNode.view)
if let selectionPanelView = self.selectionPanel?.view {
self.insertSubview(paneNode.view, belowSubview: selectionPanelView)
} else {
self.addSubview(paneNode.view)
}
paneNode.emptyAction = { [weak self] in
guard let self else {
return
}
self.openCreateStory()
}
paneNode.additionalEmptyAction = { [weak self] in
guard let self, let component = self.component else {
return
}

View File

@ -948,6 +948,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
public var isEmptyUpdated: (Bool) -> Void = { _ in }
public private(set) var isSelectionModeActive: Bool
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
@ -985,6 +987,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)?
public var emptyAction: (() -> Void)?
public var additionalEmptyAction: (() -> Void)?
public var ensureRectVisible: ((UIView, CGRect) -> Void)?
@ -1729,6 +1732,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) {
self.items = items
self.isEmptyUpdated(self.isEmpty)
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
var gridSnapshot: UIView?
@ -2027,14 +2031,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
context: self.context,
theme: presentationData.theme,
animationName: "StoryListEmpty",
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyState_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyState_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction,
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
},
additionalActionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
additionalAction: { [weak self] in
guard let self else {
return
}
self.additionalEmptyAction?()
}
)),
environment: {},

View File

@ -46,6 +46,7 @@ swift_library(
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/AttachmentUI",
],
visibility = [
"//visibility:public",

View File

@ -46,13 +46,16 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: AutomaticBusinessMessageSetupScreen.InitialData
let mode: AutomaticBusinessMessageSetupScreen.Mode
init(
context: AccountContext,
initialData: AutomaticBusinessMessageSetupScreen.InitialData,
mode: AutomaticBusinessMessageSetupScreen.Mode
) {
self.context = context
self.initialData = initialData
self.mode = mode
}
@ -130,7 +133,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
private var isOn: Bool = false
private var accountPeer: EnginePeer?
private var messages: [EngineMessage] = []
private var currentShortcut: ShortcutMessageList.Item?
private var currentShortcutDisposable: Disposable?
private var schedule: Schedule = .always
private var customScheduleStart: Date?
@ -144,8 +148,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
private var replyToMessages: Bool = true
private var messagesDisposable: Disposable?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -172,7 +174,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
deinit {
self.messagesDisposable?.dispose()
self.currentShortcutDisposable?.dispose()
}
func scrollToTop() {
@ -350,10 +352,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
guard let component = self.component else {
return
}
let shortcutName: String
switch component.mode {
case .greeting:
shortcutName = "hello"
case .away:
shortcutName = "away"
}
let contents = AutomaticBusinessMessageSetupChatContents(
context: component.context,
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput,
shortcutId: nil
kind: .quickReplyMessageInput(shortcut: shortcutName),
shortcutId: self.currentShortcut?.id
)
let chatController = component.context.sharedContext.makeChatController(
context: component.context,
@ -364,7 +375,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
)
chatController.navigationPresentation = .modal
self.environment?.controller()?.push(chatController)
self.messagesDisposable?.dispose()
}
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
@ -459,14 +469,27 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
if self.component == nil {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
self.accountPeer = component.initialData.accountPeer
let shortcutName: String
switch component.mode {
case .greeting:
shortcutName = "hello"
case .away:
shortcutName = "away"
}
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList()
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
guard let self else {
return
}
self.accountPeer = peer
let shortcut = shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
if shortcut != self.currentShortcut {
self.currentShortcut = shortcut
self.state?.updated(transition: .immediate)
}
})
}
@ -632,15 +655,15 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
//TODO:localize
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
if let topMessage = self.messages.first {
if let currentShortcut = self.currentShortcut {
if let accountPeer = self.accountPeer {
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
accountPeer: accountPeer,
message: topMessage,
count: self.messages.count,
message: currentShortcut.topMessage,
count: currentShortcut.totalCount,
action: { [weak self] in
guard let self else {
return
@ -681,7 +704,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? (self.messages.count > 1 ? "GREETING MESSAGES" : "GREETING MESSAGE") : (self.messages.count > 1 ? "AWAY MESSAGES" : "AWAY MESSAGE"),
string: component.mode == .greeting ? "GREETING MESSAGE" : "AWAY MESSAGE",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
@ -1244,6 +1267,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentContainer {
public final class InitialData: AutomaticBusinessMessageSetupScreenInitialData {
let accountPeer: EnginePeer?
let shortcutMessageList: ShortcutMessageList
init(
accountPeer: EnginePeer?,
shortcutMessageList: ShortcutMessageList
) {
self.accountPeer = accountPeer
self.shortcutMessageList = shortcutMessageList
}
}
public enum Mode {
case greeting
case away
@ -1251,11 +1287,12 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
private let context: AccountContext
public init(context: AccountContext, mode: Mode) {
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
self.context = context
super.init(context: context, component: AutomaticBusinessMessageSetupScreenComponent(
context: context,
initialData: initialData,
mode: mode
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
@ -1293,4 +1330,20 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public static func initialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
),
context.engine.accountData.shortcutMessageList()
|> take(1)
)
|> map { accountPeer, shortcutMessageList -> AutomaticBusinessMessageSetupScreenInitialData in
return InitialData(
accountPeer: accountPeer,
shortcutMessageList: shortcutMessageList
)
}
}
}

View File

@ -21,19 +21,23 @@ import QuickReplyNameAlertController
import ChatListHeaderComponent
import PlainButtonComponent
import MultilineTextComponent
import AttachmentUI
final class QuickReplySetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: QuickReplySetupScreen.InitialData
let mode: QuickReplySetupScreen.Mode
init(
context: AccountContext,
initialData: QuickReplySetupScreen.InitialData
initialData: QuickReplySetupScreen.InitialData,
mode: QuickReplySetupScreen.Mode
) {
self.context = context
self.initialData = initialData
self.mode = mode
}
static func ==(lhs: QuickReplySetupScreenComponent, rhs: QuickReplySetupScreenComponent) -> Bool {
@ -516,6 +520,13 @@ final class QuickReplySetupScreenComponent: Component {
return
}
if case let .select(completion) = component.mode {
if let shortcutId {
completion(shortcutId)
}
return
}
if let shortcut {
let contents = AutomaticBusinessMessageSetupChatContents(
context: component.context,
@ -635,7 +646,7 @@ final class QuickReplySetupScreenComponent: Component {
var items: [ActionSheetItem] = []
//TODO:localize
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Shortcut" : "Delete Shortcuts", color: .destructive, action: { [weak self, weak actionSheet] in
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Quick Reply" : "Delete Quick Replies", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
@ -667,6 +678,7 @@ final class QuickReplySetupScreenComponent: Component {
size: CGSize,
insets: UIEdgeInsets,
statusBarHeight: CGFloat,
isModal: Bool,
transition: Transition,
deferScrollApplication: Bool
) -> CGFloat {
@ -706,14 +718,31 @@ final class QuickReplySetupScreenComponent: Component {
titleText = "Quick Replies"
}
let closeTitle: String
switch component.mode {
case .manage:
closeTitle = strings.Common_Close
case .select:
closeTitle = strings.Common_Cancel
}
let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content(
title: titleText,
navigationBackTitle: nil,
titleComponent: nil,
chatListTitle: nil,
leftButton: nil,
leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent(
content: .text(title: closeTitle, isBold: false),
pressed: { [weak self] _ in
guard let self else {
return
}
if self.attemptNavigation(complete: {}) {
self.environment?.controller()?.dismiss()
}
}
))) : nil,
rightButtons: rightButtons,
backTitle: "Back",
backTitle: isModal ? nil : "Back",
backPressed: { [weak self] in
guard let self else {
return
@ -896,6 +925,19 @@ final class QuickReplySetupScreenComponent: Component {
}
}
var isModal = false
if let controller = environment.controller(), controller.navigationPresentation == .modal {
isModal = true
}
if case .select = component.mode {
isModal = true
}
var statusBarHeight = environment.statusBarHeight
if isModal {
statusBarHeight = max(statusBarHeight, 1.0)
}
var listBottomInset = environment.safeInsets.bottom
let navigationHeight = self.updateNavigationBar(
component: component,
@ -903,7 +945,8 @@ final class QuickReplySetupScreenComponent: Component {
strings: environment.strings,
size: availableSize,
insets: environment.safeInsets,
statusBarHeight: environment.statusBarHeight,
statusBarHeight: statusBarHeight,
isModal: isModal,
transition: transition,
deferScrollApplication: true
)
@ -1013,7 +1056,12 @@ final class QuickReplySetupScreenComponent: Component {
var entries: [ContentEntry] = []
if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer {
entries.append(.add)
switch component.mode {
case .manage:
entries.append(.add)
case .select:
break
}
for item in shortcutMessageList.items {
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id)))
}
@ -1046,7 +1094,7 @@ final class QuickReplySetupScreenComponent: Component {
}
}
public final class QuickReplySetupScreen: ViewControllerComponentContainer {
public final class QuickReplySetupScreen: ViewControllerComponentContainer, AttachmentContainable {
public final class InitialData: QuickReplySetupScreenInitialData {
let accountPeer: EnginePeer?
let shortcutMessageList: ShortcutMessageList
@ -1060,14 +1108,36 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
}
}
public enum Mode {
case manage
case select(completion: (Int32) -> Void)
}
private let context: AccountContext
public init(context: AccountContext, initialData: InitialData) {
public var requestAttachmentMenuExpansion: () -> Void = {
}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in
}
public var cancelPanGesture: () -> Void = {
}
public var isContainerPanning: () -> Bool = {
return false
}
public var isContainerExpanded: () -> Bool = {
return false
}
public var mediaPickerContext: AttachmentMediaPickerContext?
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
self.context = context
super.init(context: context, component: QuickReplySetupScreenComponent(
context: context,
initialData: initialData
initialData: initialData,
mode: mode
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.scrollToTop = { [weak self] in
@ -1116,4 +1186,21 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
)
}
}
public func isContainerPanningUpdated(_ panning: Bool) {
}
public func resetForReuse() {
}
public func prepareForReuse() {
}
public func requestDismiss(completion: @escaping () -> Void) {
completion()
}
public func shouldDismissImmediately() -> Bool {
return true
}
}

View File

@ -81,6 +81,7 @@ final class BusinessDaySetupScreenComponent: Component {
private(set) var isOpen: Bool = false
private(set) var ranges: [BusinessHoursSetupScreenComponent.WorkingHourRange] = []
private var intersectingRanges = Set<Int>()
private var nextRangeId: Int = 0
override init(frame: CGRect) {
@ -116,7 +117,25 @@ final class BusinessDaySetupScreenComponent: Component {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
guard let component = self.component else {
return true
}
if self.intersectingRanges.isEmpty {
return true
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .defaultAction, title: "Reset", action: {
complete()
})
]), in: .window(.root))
return false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -191,6 +210,26 @@ final class BusinessDaySetupScreenComponent: Component {
private func validateRanges() {
self.ranges.sort(by: { $0.startMinute < $1.startMinute })
self.intersectingRanges.removeAll()
for i in 0 ..< self.ranges.count {
var minuteSet = IndexSet()
inner: for j in 0 ..< self.ranges.count {
if i == j {
continue inner
}
let range = self.ranges[j]
let rangeMinutes = range.startMinute ..< range.endMinute
minuteSet.insert(integersIn: rangeMinutes)
}
let range = self.ranges[i]
let rangeMinutes = range.startMinute ..< range.endMinute
if minuteSet.intersects(integersIn: rangeMinutes) {
self.intersectingRanges.insert(range.id)
}
}
}
func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
@ -260,7 +299,7 @@ final class BusinessDaySetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let sectionSpacing: CGFloat = 24.0
let _ = bottomContentInset
let _ = sectionSpacing
@ -335,80 +374,47 @@ final class BusinessDaySetupScreenComponent: Component {
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
var rangeSectionItems: [AnyComponentWithIdentity<Empty>] = []
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Opening time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
for i in 0 ..< 2 {
let isOpenTime = i == 0
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: isOpenTime ? "Opening time" : "Closing Time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: isOpenTime ? startText : endText, font: Font.regular(17.0), textColor: self.intersectingRanges.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemPrimaryTextColor))
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: startText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime)
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
}
))))
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Closing time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: endText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
}
))))
))))
}
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
@ -519,13 +525,13 @@ final class BusinessDaySetupScreenComponent: Component {
text: .plain(NSAttributedString(
string: "Add a Set of Hours",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
name: "Item List/AddTimeIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
@ -619,7 +625,7 @@ final class BusinessDaySetupScreenComponent: Component {
final class BusinessDaySetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void
fileprivate let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void
init(context: AccountContext, dayIndex: Int, day: BusinessHoursSetupScreenComponent.Day, updateDay: @escaping (BusinessHoursSetupScreenComponent.Day) -> Void) {
self.context = context
@ -647,9 +653,12 @@ final class BusinessDaySetupScreen: ViewControllerComponentContainer {
return true
}
self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil))
return componentView.attemptNavigation(complete: complete)
if componentView.attemptNavigation(complete: complete) {
self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil))
return true
} else {
return false
}
}
}

View File

@ -22,6 +22,32 @@ import LocationUI
import TelegramStringFormatting
import TimezoneSelectionScreen
private func wrappedMinuteRange(range: Range<Int>, dayIndexOffset: Int = 0) -> IndexSet {
let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60)
var result = IndexSet()
if mappedRange.upperBound > 7 * 24 * 60 {
result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60)
result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60))
} else {
result.insert(integersIn: mappedRange)
}
return result
}
private func getDayRanges(days: [BusinessHoursSetupScreenComponent.Day], index: Int) -> [BusinessHoursSetupScreenComponent.WorkingHourRange] {
let day = days[index]
if let ranges = day.ranges {
if ranges.isEmpty {
return [BusinessHoursSetupScreenComponent.WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)]
} else {
return ranges
}
} else {
return []
}
}
final class BusinessHoursSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -68,6 +94,16 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
struct DayRangeIndex: Hashable {
var day: Int
var id: Int
init(day: Int, id: Int) {
self.day = day
self.id = id
}
}
struct Day: Equatable {
var ranges: [WorkingHourRange]?
@ -83,7 +119,7 @@ final class BusinessHoursSetupScreenComponent: Component {
var timezoneId: String
private(set) var days: [Day]
private(set) var intersectingDays = Set<Int>()
private(set) var intersectingRanges = Set<DayRangeIndex>()
init(timezoneId: String, days: [Day]) {
self.timezoneId = timezoneId
@ -111,13 +147,45 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
if let value = try? self.asBusinessHours() {
if value != businessHours {
assertionFailure("Inconsistent representation")
}
}
self.validate()
}
mutating func validate() {
self.intersectingDays.removeAll()
self.intersectingRanges.removeAll()
for dayIndex in 0 ..< self.days.count {
var otherDaysMinutes = IndexSet()
inner: for otherDayIndex in 0 ..< self.days.count {
if dayIndex == otherDayIndex {
continue inner
}
for range in getDayRanges(days: self.days, index: otherDayIndex) {
otherDaysMinutes.formUnion(wrappedMinuteRange(range: range.startMinute ..< range.endMinute, dayIndexOffset: otherDayIndex))
}
}
let dayRanges = getDayRanges(days: self.days, index: dayIndex)
for i in 0 ..< dayRanges.count {
var currentDayOtherMinutes = IndexSet()
inner: for j in 0 ..< dayRanges.count {
if i == j {
continue inner
}
currentDayOtherMinutes.formUnion(wrappedMinuteRange(range: dayRanges[j].startMinute ..< dayRanges[j].endMinute, dayIndexOffset: dayIndex))
}
let currentDayIndices = wrappedMinuteRange(range: dayRanges[i].startMinute ..< dayRanges[i].endMinute, dayIndexOffset: dayIndex)
if !otherDaysMinutes.intersection(currentDayIndices).isEmpty || !currentDayOtherMinutes.intersection(currentDayIndices).isEmpty {
self.intersectingRanges.insert(DayRangeIndex(day: dayIndex, id: dayRanges[i].id))
}
}
}
}
mutating func update(days: [Day]) {
@ -140,13 +208,7 @@ final class BusinessHoursSetupScreenComponent: Component {
for range in effectiveRanges {
let minuteRange: Range<Int> = (dayStartMinute + range.startMinute) ..< (dayStartMinute + range.endMinute)
var wrappedMinutes = IndexSet()
if minuteRange.upperBound > 7 * 24 * 60 {
wrappedMinutes.insert(integersIn: minuteRange.lowerBound ..< 7 * 24 * 60)
wrappedMinutes.insert(integersIn: 0 ..< (7 * 24 * 60 - minuteRange.upperBound))
} else {
wrappedMinutes.insert(integersIn: minuteRange)
}
let wrappedMinutes = wrappedMinuteRange(range: minuteRange)
if !filledMinutes.intersection(wrappedMinutes).isEmpty {
throw ValidationError.intersectingRanges
@ -156,7 +218,20 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mappedIntervals)
var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = []
for interval in mappedIntervals {
if mergedIntervals.isEmpty {
mergedIntervals.append(interval)
} else {
if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute {
mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute)
} else {
mergedIntervals.append(interval)
}
}
}
return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mergedIntervals)
}
}
@ -231,7 +306,6 @@ final class BusinessHoursSetupScreenComponent: Component {
return true
} catch let error {
let _ = error
//TODO:localize
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
@ -359,7 +433,7 @@ final class BusinessHoursSetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let sectionSpacing: CGFloat = 30.0
let _ = bottomContentInset
let _ = sectionSpacing
@ -370,14 +444,14 @@ final class BusinessHoursSetupScreenComponent: Component {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "BusinessHoursEmoji"),
loop: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 10.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
@ -386,7 +460,7 @@ final class BusinessHoursSetupScreenComponent: Component {
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
contentHeight += 126.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Turn this on to show your opening hours schedule to your customers.", attributes: MarkdownAttributes(
@ -506,28 +580,42 @@ final class BusinessHoursSetupScreenComponent: Component {
title = " "
}
let subtitle: String
let subtitle = NSMutableAttributedString()
var invalidIndices: [Int] = []
let effectiveDayRanges = getDayRanges(days: self.daysState.days, index: dayIndex)
for range in effectiveDayRanges {
if self.daysState.intersectingRanges.contains(DayRangeIndex(day: dayIndex, id: range.id)) {
invalidIndices.append(range.id)
}
}
let subtitleFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0))
if let ranges = self.daysState.days[dayIndex].ranges {
if ranges.isEmpty {
subtitle = "Open 24 Hours"
subtitle.append(NSAttributedString(string: "Open 24 Hours", font: subtitleFont, textColor: invalidIndices.contains(0) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor))
} else {
var resultText: String = ""
for range in ranges {
if !resultText.isEmpty {
resultText.append(", ")
}
for i in 0 ..< ranges.count {
let range = ranges[i]
let startHours = clipMinutes(range.startMinute) / 60
let startMinutes = clipMinutes(range.startMinute) % 60
let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat())
let endHours = clipMinutes(range.endMinute) / 60
let endMinutes = clipMinutes(range.endMinute) % 60
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
resultText.append("\(startText)\u{00a0}- \(endText)")
var rangeString = "\(startText)\u{00a0}- \(endText)"
if i != ranges.count - 1 {
rangeString.append(", ")
}
subtitle.append(NSAttributedString(string: rangeString, font: subtitleFont, textColor: invalidIndices.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor))
}
subtitle = resultText
}
} else {
subtitle = "Closed"
subtitle.append(NSAttributedString(string: "Closed", font: subtitleFont, textColor: environment.theme.list.itemAccentColor))
}
daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent(
@ -542,14 +630,11 @@ final class BusinessHoursSetupScreenComponent: Component {
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: subtitle,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 5
text: .plain(subtitle),
maximumNumberOfLines: 20
)))
], alignment: .left, spacing: 2.0)),
], alignment: .left, spacing: 3.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 10.0, right: 0.0),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in
guard let self else {
return

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/LocationUI",
"//submodules/AppBundle",
"//submodules/Geocoding",
],
visibility = [
"//visibility:public",

View File

@ -20,6 +20,8 @@ import BundleIconComponent
import LottieComponent
import Markdown
import LocationUI
import CoreLocation
import Geocoding
final class BusinessLocationSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -74,7 +76,11 @@ final class BusinessLocationSetupScreenComponent: Component {
private let textFieldTag = NSObject()
private var resetAddressText: String?
private var isLoadingGeocodedAddress: Bool = false
private var geocodeAddressState: (address: String, disposable: Disposable)?
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
private var mapCoordinatesManuallySet: Bool = false
override init(frame: CGRect) {
self.scrollView = ScrollView()
@ -102,6 +108,7 @@ final class BusinessLocationSetupScreenComponent: Component {
}
deinit {
self.geocodeAddressState?.disposable.dispose()
}
func scrollToTop() {
@ -109,20 +116,38 @@ final class BusinessLocationSetupScreenComponent: Component {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
if let component = self.component {
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
guard let component = self.component else {
return true
}
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
if businessLocation != nil && address.isEmpty {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Address can't be empty.", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .destructiveAction, title: "Delete", action: {
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone()
complete()
})
]), in: .window(.root))
return false
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
return true
}
@ -165,12 +190,18 @@ final class BusinessLocationSetupScreenComponent: Component {
return
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, completion: { [weak self] location, _, _, address, _ in
var initialLocation: CLLocationCoordinate2D?
if let mapCoordinates = self.mapCoordinates {
initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude)
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in
guard let self else {
return
}
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.latitude, longitude: location.longitude)
self.mapCoordinatesManuallySet = true
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.isEmpty {
self.resetAddressText = address
}
@ -180,6 +211,43 @@ final class BusinessLocationSetupScreenComponent: Component {
self.environment?.controller()?.push(controller)
}
private func updateGeocodedAddress(string: String) {
let addressValue: String?
if self.mapCoordinates != nil && self.mapCoordinatesManuallySet {
addressValue = nil
} else if string.count < 3 {
addressValue = nil
} else {
addressValue = string
}
if let current = self.geocodeAddressState, current.address == addressValue {
} else {
self.geocodeAddressState?.disposable.dispose()
self.geocodeAddressState = nil
if let addressValue {
let disposable = MetaDisposable()
self.geocodeAddressState = (string, disposable)
disposable.set((
geocodeLocation(address: addressValue, locale: Locale.current)
|> delay(0.4, queue: .mainQueue())
|> deliverOnMainQueue
).start(next: { [weak self] result in
guard let self else {
return
}
if let location = result?.first?.location, !self.mapCoordinatesManuallySet {
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
self.state?.updated(transition: .immediate)
}
}))
}
}
}
func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
@ -189,6 +257,9 @@ final class BusinessLocationSetupScreenComponent: Component {
if self.component == nil {
if let initialValue = component.initialValue {
self.mapCoordinates = initialValue.coordinates
if self.mapCoordinates != nil {
self.mapCoordinatesManuallySet = true
}
self.resetAddressText = initialValue.address
}
}
@ -228,10 +299,7 @@ final class BusinessLocationSetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let _ = bottomContentInset
let _ = sectionSpacing
let sectionSpacing: CGFloat = 24.0
var contentHeight: CGFloat = 0.0
@ -246,7 +314,7 @@ final class BusinessLocationSetupScreenComponent: Component {
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
@ -304,6 +372,7 @@ final class BusinessLocationSetupScreenComponent: Component {
contentHeight += subtitleSize.height
contentHeight += 27.0
//TODO:localize
var addressSectionItems: [AnyComponentWithIdentity<Empty>] = []
addressSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
context: component.context,
@ -317,12 +386,7 @@ final class BusinessLocationSetupScreenComponent: Component {
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 64,
updated: { [weak self] value in
guard let self else {
return
}
let _ = self
let _ = value
updated: { _ in
},
tag: self.textFieldTag
))))
@ -374,6 +438,7 @@ final class BusinessLocationSetupScreenComponent: Component {
self.openLocationPicker()
} else {
self.mapCoordinates = nil
self.mapCoordinatesManuallySet = false
self.state?.updated(transition: .spring(duration: 0.4))
}
}

View File

@ -34,7 +34,22 @@ private struct TimezoneListEntry: Comparable, Identifiable {
}
func item(presentationData: PresentationData, searchMode: Bool, action: @escaping (String) -> Void) -> ListViewItem {
return ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: self.title, kind: .neutral, alignment: .natural, sectionId: 0, style: .plain, action: {
let hours = abs(self.offset / (60 * 60))
let minutes = abs(self.offset % (60 * 60)) / 60
let offsetString: String
if minutes == 0 {
offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours)"
} else {
let minutesString: String
if minutes < 10 {
minutesString = "0\(minutes)"
} else {
minutesString = "\(minutes)"
}
offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours):\(minutesString)"
}
return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: self.title, label: offsetString, sectionId: 0, style: .plain, disclosureStyle: .none, action: {
action(self.id)
})
}

View File

@ -134,6 +134,9 @@ final class StoryAuthorInfoComponent: Component {
if timeString.count < 6 {
combinedString.append(NSAttributedString(string: "\(timeString)", font: Font.regular(11.0), textColor: subtitleColor))
}
if component.isEdited {
combinedString.append(NSAttributedString(string: "\(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor))
}
subtitle = combinedString
subtitleTruncationType = .middle
} else {

View File

@ -990,7 +990,7 @@ public final class StoryContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
@ -1642,7 +1642,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let alternativeMedia = item.alternativeMedia, !preferHighQualityStories {
if let alternativeMedia = item.alternativeMedia, (!preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
@ -2880,7 +2880,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media

View File

@ -593,7 +593,7 @@ final class StoryItemContentComponent: Component {
let selectedMedia: EngineMedia
var messageMedia: EngineMedia?
if !component.preferHighQuality, let alternativeMedia = component.item.alternativeMedia {
if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia {
selectedMedia = alternativeMedia
switch alternativeMedia {

View File

@ -5420,6 +5420,7 @@ public final class StoryItemSetContainerComponent: Component {
var updateProgressImpl: ((Float) -> Void)?
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
isEditing: !repost,
forwardSource: repost ? (component.slice.peer, item) : nil,
@ -6857,7 +6858,7 @@ public final class StoryItemSetContainerComponent: Component {
})))
}
if case let .file(file) = component.slice.item.storyItem.media, file.isVideo {
if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo {
let isHq = component.slice.additionalPeerData.preferHighQualityStories
items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in
if isHq {

View File

@ -179,6 +179,7 @@ final class StoryItemSetContainerSendMessage {
self.inputPanelExternalState?.deleteBackward()
}
},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self {
self.view?.component?.controller()?.present(c, in: .window(.root), with: a)

View File

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

View File

@ -0,0 +1,117 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.334961 4.334961 cm
0.000000 0.000000 0.000000 scn
1.330000 10.665078 m
1.330000 15.820656 5.509422 20.000078 10.665000 20.000078 c
15.820579 20.000078 20.000000 15.820656 20.000000 10.665078 c
20.000000 5.509500 15.820579 1.330078 10.665000 1.330078 c
10.024749 1.330078 9.400214 1.394436 8.797290 1.516823 c
8.437361 1.589886 8.086353 1.357334 8.013291 0.997406 c
7.940229 0.637478 8.172781 0.286469 8.532710 0.213406 c
9.222226 0.073441 9.935388 0.000076 10.665000 0.000076 c
16.555117 0.000076 21.330002 4.774961 21.330002 10.665078 c
21.330002 16.555195 16.555117 21.330078 10.665000 21.330078 c
4.774883 21.330078 0.000000 16.555195 0.000000 10.665078 c
0.000000 9.935467 0.073363 9.222304 0.213327 8.532788 c
0.286389 8.172859 0.637397 7.940308 0.997326 8.013370 c
1.357255 8.086432 1.589807 8.437439 1.516745 8.797368 c
1.394358 9.400291 1.330000 10.024826 1.330000 10.665078 c
h
11.330000 17.165077 m
11.330000 17.532347 11.032269 17.830078 10.665000 17.830078 c
10.297730 17.830078 10.000000 17.532347 10.000000 17.165077 c
10.000000 11.114144 l
10.000000 10.638557 10.203376 10.185671 10.558834 9.869707 c
14.723198 6.168051 l
14.997698 5.924050 15.418027 5.948775 15.662027 6.223276 c
15.906028 6.497777 15.881302 6.918105 15.606802 7.162106 c
11.442438 10.863762 l
11.370919 10.927334 11.330000 11.018456 11.330000 11.114144 c
11.330000 17.165077 l
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 3.339844 3.334961 cm
0.000000 0.000000 0.000000 scn
5.330000 8.665077 m
5.330000 9.032347 5.032269 9.330078 4.665000 9.330078 c
4.297730 9.330078 4.000000 9.032347 4.000000 8.665077 c
4.000000 5.330077 l
0.665000 5.330077 l
0.297731 5.330077 0.000000 5.032347 0.000000 4.665077 c
0.000000 4.297808 0.297731 4.000077 0.665000 4.000077 c
4.000000 4.000077 l
4.000000 0.665077 l
4.000000 0.297808 4.297730 0.000077 4.665000 0.000077 c
5.032269 0.000077 5.330000 0.297808 5.330000 0.665077 c
5.330000 4.000077 l
8.665000 4.000077 l
9.032269 4.000077 9.330000 4.297808 9.330000 4.665077 c
9.330000 5.032347 9.032269 5.330077 8.665000 5.330077 c
5.330000 5.330077 l
5.330000 8.665077 l
h
f*
n
Q
endstream
endobj
3 0 obj
2167
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002257 00000 n
0000002280 00000 n
0000002453 00000 n
0000002527 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2586
%%EOF

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "check.pdf",
"filename" : "arrowhead_30.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 10.000000 9.771912 cm
1.000000 1.000000 1.000000 scn
0.742462 6.970551 m
0.332412 7.380601 -0.332412 7.380601 -0.742462 6.970551 c
-1.152513 6.560500 -1.152513 5.895677 -0.742462 5.485626 c
0.742462 6.970551 l
h
4.000000 2.228088 m
3.257538 1.485626 l
3.462021 1.281143 3.741785 1.170047 4.030842 1.178541 c
4.319900 1.187036 4.592658 1.314369 4.784780 1.530506 c
4.000000 2.228088 l
h
12.784780 10.530506 m
13.170044 10.963928 13.131004 11.627604 12.697582 12.012868 c
12.264160 12.398132 11.600484 12.359093 11.215220 11.925671 c
12.784780 10.530506 l
h
-0.742462 5.485626 m
3.257538 1.485626 l
4.742462 2.970551 l
0.742462 6.970551 l
-0.742462 5.485626 l
h
4.784780 1.530506 m
12.784780 10.530506 l
11.215220 11.925671 l
3.215220 2.925671 l
4.784780 1.530506 l
h
f
n
Q
endstream
endobj
3 0 obj
839
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 33.000000 33.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000929 00000 n
0000000951 00000 n
0000001124 00000 n
0000001198 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1257
%%EOF

View File

@ -1,150 +0,0 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm
0.000000 0.000000 0.000000 scn
16.557842 15.662290 m
17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c
8.443170 0.821279 l
8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c
7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c
0.389548 6.608668 l
-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c
0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c
7.112782 3.647242 l
14.716830 15.276961 l
15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c
h
f*
n
Q
endstream
endobj
2 0 obj
584
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 20.000000 m
0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c
20.000000 40.000000 l
31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c
40.000000 20.000000 l
40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c
20.000000 0.000000 l
8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c
0.000000 20.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
472
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000000842 00000 n
0000000864 00000 n
0000001584 00000 n
0000001606 00000 n
0000001904 00000 n
0000002006 00000 n
0000002027 00000 n
0000002200 00000 n
0000002274 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
2334
%%EOF

View File

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

View File

@ -0,0 +1,149 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.694702 2.078613 cm
0.000000 0.000000 0.000000 scn
9.817018 17.062620 m
9.747100 17.099379 9.663571 17.099379 9.593654 17.062620 c
8.489970 16.482380 l
8.313900 16.389814 8.108116 16.539326 8.141742 16.735382 c
8.352527 17.964352 l
8.365880 18.042206 8.340069 18.121647 8.283504 18.176785 c
7.390605 19.047144 l
7.248161 19.185993 7.326764 19.427906 7.523617 19.456511 c
8.757572 19.635815 l
8.835742 19.647175 8.903318 19.696270 8.938277 19.767105 c
9.490119 20.885258 l
9.578154 21.063637 9.832517 21.063639 9.920552 20.885260 c
10.472394 19.767105 l
10.507353 19.696270 10.574928 19.647175 10.653099 19.635815 c
11.887054 19.456511 l
12.083907 19.427906 12.162510 19.185993 12.020066 19.047144 c
11.127168 18.176785 l
11.070602 18.121647 11.044791 18.042206 11.058144 17.964352 c
11.268929 16.735382 l
11.302555 16.539326 11.096771 16.389814 10.920701 16.482380 c
9.817018 17.062620 l
h
1.785109 15.879396 m
1.735167 15.905652 1.675504 15.905652 1.625563 15.879396 c
0.837217 15.464938 l
0.711453 15.398820 0.564465 15.505613 0.588483 15.645655 c
0.739044 16.523491 l
0.748582 16.579102 0.730145 16.635843 0.689742 16.675226 c
0.051957 17.296913 l
-0.049789 17.396091 0.006356 17.568886 0.146965 17.589317 c
1.028362 17.717392 l
1.084198 17.725506 1.132466 17.760574 1.157437 17.811171 c
1.551610 18.609852 l
1.614492 18.737267 1.796180 18.737267 1.859062 18.609852 c
2.253235 17.811171 l
2.278205 17.760574 2.326474 17.725506 2.382310 17.717392 c
3.263707 17.589317 l
3.404316 17.568886 3.460460 17.396091 3.358715 17.296913 c
2.720930 16.675226 l
2.680526 16.635843 2.662090 16.579100 2.671628 16.523491 c
2.822188 15.645655 l
2.846207 15.505613 2.699219 15.398820 2.573454 15.464939 c
1.785109 15.879396 l
h
3.225563 7.079397 m
3.275504 7.105653 3.335167 7.105653 3.385108 7.079397 c
4.173454 6.664939 l
4.299219 6.598821 4.446208 6.705614 4.422188 6.845654 c
4.271628 7.723491 l
4.262090 7.779101 4.280527 7.835844 4.320930 7.875227 c
4.958715 8.496914 l
5.060461 8.596091 5.004316 8.768887 4.863707 8.789319 c
3.982310 8.917393 l
3.926474 8.925507 3.878206 8.960575 3.853235 9.011171 c
3.459062 9.809853 l
3.396180 9.937266 3.214492 9.937266 3.151610 9.809853 c
2.757437 9.011171 l
2.732466 8.960575 2.684198 8.925507 2.628362 8.917393 c
1.746965 8.789319 l
1.606356 8.768887 1.550211 8.596092 1.651957 8.496914 c
2.289742 7.875228 l
2.330145 7.835844 2.348582 7.779101 2.339044 7.723491 c
2.188483 6.845655 l
2.164464 6.705614 2.311453 6.598821 2.437217 6.664939 c
3.225563 7.079397 l
h
5.069424 14.825876 m
5.641542 15.397993 6.569129 15.397994 7.141247 14.825876 c
18.809877 3.157246 l
19.381996 2.585127 19.381994 1.657541 18.809877 1.085423 c
18.341248 0.616795 l
17.769131 0.044676 16.841543 0.044676 16.269424 0.616795 c
4.600795 12.285424 l
4.028677 12.857543 4.028677 13.785130 4.600795 14.357246 c
5.069424 14.825876 l
h
8.835109 11.251109 m
8.175561 10.591561 l
17.209877 1.557245 l
17.262598 1.504526 17.348076 1.504526 17.400797 1.557245 c
17.869425 2.025875 l
17.922146 2.078596 17.922146 2.164074 17.869425 2.216793 c
8.835109 11.251109 l
h
f*
n
Q
endstream
endobj
3 0 obj
3139
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003229 00000 n
0000003252 00000 n
0000003425 00000 n
0000003499 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3558
%%EOF

View File

@ -1125,7 +1125,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatFilterTag = value
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
var standalone = false
if case .customChatContents = strongSelf.chatLocation {
standalone = true
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
}, present: { c, a in
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
@ -4566,6 +4571,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
)
self.push(boostController)
})
}, openStickerEditor: { [weak self] in
guard let self else {
return
}
self.openStickerEditor()
}, requestMessageUpdate: { [weak self] id, scroll in
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
@ -9519,7 +9529,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.push(self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData))
let controller = self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData)
controller.navigationPresentation = .modal
self.push(controller)
})
}, sendBotStart: { [weak self] payload in
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
@ -9542,9 +9554,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
strongSelf.dismissAllTooltips()
@ -9554,32 +9563,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
var bannedMediaInput = false
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
}
}
@ -16835,6 +16846,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
transitionIn: nil,
transitionOut: { _, _ in

View File

@ -29,6 +29,8 @@ import ChatEntityKeyboardInputNode
import PremiumUI
import PremiumGiftAttachmentScreen
import TelegramCallsUI
import AutomaticBusinessMessageSetupScreen
import MediaEditorScreen
extension ChatControllerImpl {
enum AttachMenuSubject {
@ -131,8 +133,11 @@ extension ChatControllerImpl {
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
buttons = self.context.engine.messages.attachMenuBots()
|> map { attachMenuBots in
buttons = combineLatest(
self.context.engine.messages.attachMenuBots(),
self.context.engine.accountData.shortcutMessageList() |> take(1)
)
|> map { attachMenuBots, shortcutMessageList in
var buttons = availableButtons
var allButtons = availableButtons
var initialButton: AttachmentButtonType?
@ -166,6 +171,19 @@ extension ChatControllerImpl {
allButtons.insert(button, at: 1)
}
if let user = peer as? TelegramUser, user.botInfo == nil {
if let index = buttons.firstIndex(where: { $0 == .location }) {
buttons.insert(.quickReply, at: index + 1)
} else {
buttons.append(.quickReply)
}
if let index = allButtons.firstIndex(where: { $0 == .location }) {
allButtons.insert(.quickReply, at: index + 1)
} else {
allButtons.append(.quickReply)
}
}
return (buttons, allButtons, initialButton)
}
} else {
@ -602,6 +620,24 @@ extension ChatControllerImpl {
strongSelf.present(alertController, in: .window(.root))
}
}
case .quickReply:
let _ = (strongSelf.context.sharedContext.makeQuickReplySetupScreenInitialData(context: strongSelf.context)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak strongSelf] initialData in
guard let strongSelf else {
return
}
let controller = QuickReplySetupScreen(context: strongSelf.context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .select(completion: { [weak strongSelf] shortcutId in
guard let strongSelf else {
return
}
strongSelf.attachmentController?.dismiss(animated: true)
strongSelf.interfaceInteraction?.sendShortcut(shortcutId)
}))
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
})
default:
break
}
@ -1663,4 +1699,71 @@ extension ChatControllerImpl {
})
})
}
func openStickerEditor() {
let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil
})
// controller.forceSourceRect = true
// controller.getSourceRect = getSourceRect
mainController.requestController = { [weak self, weak mainController] _, present in
guard let self else {
return
}
let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker))
mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in
guard let self else {
return
}
if let result = result as? PHAsset {
controller.updateHiddenMediaId(result.localIdentifier)
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
let editorController = MediaEditorScreen(
context: self.context,
mode: .stickerEditor,
subject: .single(.asset(result)),
transitionIn: .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceImage: controller.transitionImage(for: result.localIdentifier)
)
),
transitionOut: { finished, isNew in
if !finished {
return MediaEditorScreen.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: 0.0
)
}
return nil
}, completion: { [weak self, weak mainController] result, commit in
mainController?.dismiss()
Queue.mainQueue().after(0.1) {
commit({})
if let mediaResult = result.media, case let .image(image, _) = mediaResult {
self?.enqueueStickerImage(image, isMemoji: false)
}
}
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.dismissed = { [weak controller] in
controller?.updateHiddenMediaId(nil)
}
self.push(editorController)
// completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in
// controller?.updateHiddenMediaId(nil)
// })
}
}
}
present(mediaPickerController, mediaPickerController.mediaPickerContext)
}
mainController.navigationPresentation = .flatModal
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.push(mainController)
}
}

View File

@ -68,6 +68,8 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
} else {
hasEditRights = true
}
} else if message.id.namespace == Namespaces.Message.QuickReplyCloud {
hasEditRights = true
} else if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud {
hasEditRights = false
} else if let author = message.author, author.id == accountPeerId, let peer = message.peers[message.id.peerId] {
@ -601,68 +603,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return .single(ContextController.Items(content: .list(actions)))
}
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
var actions: [ContextMenuItem] = []
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
f(.default)
})))
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak customChatContents] _, f in
f(.dismissWithoutContent)
guard let customChatContents else {
return
}
customChatContents.deleteMessages(ids: messages.map(\.id))
})))
}
return .single(ContextController.Items(content: .list(actions)))
}
var loadStickerSaveStatus: MediaId?
var loadCopyMediaResource: MediaResource?
var isAction = false
@ -1140,8 +1080,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
})))
}
var messageText: String = ""
for message in messages {
if !message.text.isEmpty {
@ -1164,6 +1102,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
}
for attribute in message.attributes {
if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute {
if !messageText.isEmpty {
messageText.append("\n")
}
messageText.append(attribute.text)
break
}
}
var isPoll = false
if messageText.isEmpty {
for media in message.media {
@ -1937,6 +1885,74 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.removeFirst()
}
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
actions.removeAll()
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
if !messageText.isEmpty || (resourceAvailable && isImage) || diceEmoji != nil {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
f(.default)
})))
}
if message.id.namespace == Namespaces.Message.QuickReplyCloud {
if data.canEdit {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
}
if !actions.isEmpty {
actions.append(.separator)
}
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak customChatContents] _, f in
f(.dismissWithoutContent)
guard let customChatContents else {
return
}
customChatContents.deleteMessages(ids: messages.map(\.id))
})))
}
}
}
return ContextController.Items(content: .list(actions), tip: nil)
}
}

View File

@ -405,8 +405,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.LowLatency)
options.insert(.PreferSynchronousResourceLoading)
if firstTime {
self.contentOffsetChangeTransition = .spring(duration: 0.4)
self.contentOffsetChangeTransition = .immediate
self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))
} else {
@ -419,7 +420,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
}
var insets = UIEdgeInsets()
insets.top = topInsetForLayout(size: validLayout.0, hasShortcuts: transition.hasShortcuts)
insets.top = topInsetForLayout(size: validLayout.0)
insets.left = validLayout.1
insets.right = validLayout.2
@ -437,12 +438,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
if let topItemOffset = topItemOffset {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
let position = strongSelf.listView.layer.position
strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset))
transition.animateView {
strongSelf.listView.position = position
}
//transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
transition.animatePositionAdditive(layer: strongSelf.listView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
}
}
})
@ -451,10 +448,31 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
}
}
private func topInsetForLayout(size: CGSize, hasShortcuts: Bool) -> CGFloat {
var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
if hasShortcuts {
minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
private func topInsetForLayout(size: CGSize) -> CGFloat {
var minimumItemHeights: CGFloat = 0.0
if let currentEntries = self.currentEntries, !currentEntries.isEmpty {
let indexLimit = min(4, currentEntries.count - 1)
for i in 0 ... indexLimit {
var itemHeight: CGFloat
switch currentEntries[i].content {
case .editShortcuts:
itemHeight = VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
case let .command(command):
switch command.command {
case .command:
itemHeight = MentionChatInputPanelItemNode.itemHeight
case .shortcut:
itemHeight = 58.0
}
}
if indexLimit >= 4 && i == indexLimit {
minimumItemHeights += floor(itemHeight * 0.5)
} else {
minimumItemHeights += itemHeight
}
}
} else {
minimumItemHeights = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
}
return max(size.height - minimumItemHeights, 0.0)
@ -465,16 +483,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
self.validLayout = (size, leftInset, rightInset, bottomInset)
var insets = UIEdgeInsets()
var hasShortcuts = false
if let currentEntries = self.currentEntries {
hasShortcuts = currentEntries.contains(where: { entry in
if case .editShortcuts = entry.content {
return true
}
return false
})
}
insets.top = self.topInsetForLayout(size: size, hasShortcuts: hasShortcuts)
insets.top = self.topInsetForLayout(size: size)
insets.left = leftInset
insets.right = rightInset

Some files were not shown because too many files have changed in this diff Show More