Various improvements

This commit is contained in:
Ilya Laktyushin 2022-12-17 15:35:00 +04:00
parent aeafae62df
commit 77df1cf45a
182 changed files with 6119 additions and 5449 deletions

View file

@ -8478,11 +8478,13 @@ Sorry for the inconvenience.";
"Conversation.SuggestedPhotoText" = "%@ suggests you to use this photo for your Telegram account.";
"Conversation.SuggestedPhotoTextYou" = "You suggested %@ to use this photo for their Telegram account.";
"Conversation.SuggestedPhotoView" = "View";
"Conversation.SuggestedPhotoSuccess" = "You have updated your Telegram account photo.";
"Conversation.SuggestedVideoTitle" = "Suggested Video";
"Conversation.SuggestedVideoText" = "%@ suggests you to use this video for your Telegram account.";
"Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account.";
"Conversation.SuggestedVideoView" = "View";
"Conversation.SuggestedVideoSuccess" = "You have updated your Telegram account video.";
"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception";
"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions";
@ -8493,9 +8495,6 @@ Sorry for the inconvenience.";
"Notification.SuggestedProfilePhoto" = "Suggested Profile Photo";
"Notification.SuggestedProfileVideo" = "Suggested Profile Video";
"PhotoEditor.SetProfilePhoto" = "Set Profile Photo";
"PhotoEditor.SetProfileVideo" = "Set Profile Video";
"PhotoEditor.SetAsMyPhoto" = "Set as My Photo";
"PhotoEditor.SetAsMyVideo" = "Set as My Video";
@ -8504,8 +8503,13 @@ Sorry for the inconvenience.";
"Privacy.ProfilePhoto.SetPublicPhoto" = "Set Public Photo";
"Privacy.ProfilePhoto.UpdatePublicPhoto" = "Update Public Photo";
"Privacy.ProfilePhoto.RemovePublicPhoto" = "Remove Public Photo";
"Privacy.ProfilePhoto.RemovePublicVideo" = "Remove Public Video";
"Privacy.ProfilePhoto.PublicPhotoInfo" = "You can upload a public photo for those who are restricted from viewing your real profile photo.";
"Privacy.ProfilePhoto.PublicPhotoSuccess" = "This photo is now set for those who are restricted from viewing your main photo.";
"Privacy.ProfilePhoto.PublicVideoSuccess" = "This video is now set for those who are restricted from viewing your main photo.";
"WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages";
"Common.Paste" = "Paste";
"PhotoEditor.SelectCoverFrameSuggestion" = "Choose a cover for profile video";

View file

@ -706,6 +706,13 @@ public enum ChatListSearchFilter: Equatable {
}
}
public enum InstalledStickerPacksControllerMode {
case general
case modal
case masks
case emoji
}
public let defaultContactLabel: String = "_$!<Mobile>!$_"
public enum CreateGroupMode {
@ -812,6 +819,8 @@ public protocol SharedAccountContext: AnyObject {
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode) -> ViewController
func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get }

View file

@ -316,8 +316,8 @@ public final class AppLockContextImpl: AppLockContext {
public var invalidAttempts: Signal<AccessChallengeAttempts?, NoError> {
return self.currentState.get()
|> map { state in
return state.unlockAttemts.flatMap { unlockAttemts in
return AccessChallengeAttempts(count: unlockAttemts.count, bootTimestamp: unlockAttemts.timestamp.bootTimestamp, uptime: unlockAttemts.timestamp.uptime)
return state.unlockAttempts.flatMap { unlockAttempts in
return AccessChallengeAttempts(count: unlockAttempts.count, bootTimestamp: unlockAttempts.timestamp.bootTimestamp, uptime: unlockAttempts.timestamp.uptime)
}
}
}
@ -346,7 +346,7 @@ public final class AppLockContextImpl: AppLockContext {
self.updateLockState { state in
var state = state
state.unlockAttemts = nil
state.unlockAttempts = nil
state.isManuallyLocked = false
@ -362,16 +362,16 @@ public final class AppLockContextImpl: AppLockContext {
public func failedUnlockAttempt() {
self.updateLockState { state in
var state = state
var unlockAttemts = state.unlockAttemts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0))
var unlockAttempts = state.unlockAttempts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0))
unlockAttemts.count += 1
unlockAttempts.count += 1
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
unlockAttemts.timestamp = timestamp
state.unlockAttemts = unlockAttemts
unlockAttempts.timestamp = timestamp
state.unlockAttempts = unlockAttempts
return state
}
}

View file

@ -24,13 +24,13 @@ public struct UnlockAttempts: Codable, Equatable {
public struct LockState: Codable, Equatable {
public var isManuallyLocked: Bool
public var autolockTimeout: Int32?
public var unlockAttemts: UnlockAttempts?
public var unlockAttempts: UnlockAttempts?
public var applicationActivityTimestamp: MonotonicTimestamp?
public init(isManuallyLocked: Bool = false, autolockTimeout: Int32? = nil, unlockAttemts: UnlockAttempts? = nil, applicationActivityTimestamp: MonotonicTimestamp? = nil) {
self.isManuallyLocked = isManuallyLocked
self.autolockTimeout = autolockTimeout
self.unlockAttemts = unlockAttemts
self.unlockAttempts = unlockAttemts
self.applicationActivityTimestamp = applicationActivityTimestamp
}
}

View file

@ -781,7 +781,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
)), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
}
case let .addContact(phoneNumber, theme, strings):
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
interaction.addContact(phoneNumber)
})
}

View file

@ -19,6 +19,7 @@ swift_library(
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/StickerPeekUI:StickerPeekUI",
],
visibility = [
"//visibility:public",

View file

@ -0,0 +1,61 @@
import Foundation
import Postbox
import StickerPeekUI
import TelegramUIPreferences
public struct ChatInterfaceStickerSettings: Equatable {
public let loopAnimatedStickers: Bool
public init(loopAnimatedStickers: Bool) {
self.loopAnimatedStickers = loopAnimatedStickers
}
public init(stickerSettings: StickerSettings) {
self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers
}
public static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool {
return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers
}
}
public enum ChatMediaInputGifMode: Equatable {
case recent
case trending
case emojiSearch(String)
}
public final class ChatMediaInputNodeInteraction {
public let navigateToCollectionId: (ItemCollectionId) -> Void
public let navigateBackToStickers: () -> Void
public let setGifMode: (ChatMediaInputGifMode) -> Void
public let openSettings: () -> Void
public let openTrending: (ItemCollectionId?) -> Void
public let dismissTrendingPacks: ([ItemCollectionId]) -> Void
public let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
public let openPeerSpecificSettings: () -> Void
public let dismissPeerSpecificSettings: () -> Void
public let clearRecentlyUsedStickers: () -> Void
public var stickerSettings: ChatInterfaceStickerSettings?
public var highlightedStickerItemCollectionId: ItemCollectionId?
public var highlightedItemCollectionId: ItemCollectionId?
public var highlightedGifMode: ChatMediaInputGifMode = .recent
public var previewedStickerPackItem: StickerPreviewPeekItem?
public var appearanceTransition: CGFloat = 1.0
public var displayStickerPlaceholder = true
public var displayStickerPackManageControls = true
public init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
self.navigateToCollectionId = navigateToCollectionId
self.navigateBackToStickers = navigateBackToStickers
self.setGifMode = setGifMode
self.openSettings = openSettings
self.openTrending = openTrending
self.dismissTrendingPacks = dismissTrendingPacks
self.toggleSearch = toggleSearch
self.openPeerSpecificSettings = openPeerSpecificSettings
self.dismissPeerSpecificSettings = dismissPeerSpecificSettings
self.clearRecentlyUsedStickers = clearRecentlyUsedStickers
}
}

View file

@ -181,6 +181,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
public let topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
public let bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
public let externalBottomPanelContainer: PagerExternalTopPanelContainer?
public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?
public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void
public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void
@ -198,6 +199,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
externalTopPanelContainer: PagerExternalTopPanelContainer?,
bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
externalBottomPanelContainer: PagerExternalTopPanelContainer?,
panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?,
isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void,
isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void,
@ -214,6 +216,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
self.topPanel = topPanel
self.externalTopPanelContainer = externalTopPanelContainer
self.bottomPanel = bottomPanel
self.externalBottomPanelContainer = externalBottomPanelContainer
self.panelStateUpdated = panelStateUpdated
self.isTopPanelExpandedUpdated = isTopPanelExpandedUpdated
self.isTopPanelHiddenUpdated = isTopPanelHiddenUpdated
@ -248,6 +251,9 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
if lhs.bottomPanel != rhs.bottomPanel {
return false
}
if lhs.externalBottomPanelContainer !== rhs.externalBottomPanelContainer {
return false
}
if lhs.panelHideBehavior != rhs.panelHideBehavior {
return false
}
@ -546,8 +552,13 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
bottomPanelTransition = .immediate
bottomPanelView = ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>()
self.bottomPanelView = bottomPanelView
self.addSubview(bottomPanelView)
}
let bottomPanelSuperview = component.externalBottomPanelContainer ?? self
if bottomPanelView.superview !== bottomPanelSuperview {
bottomPanelSuperview.addSubview(bottomPanelView)
}
let bottomPanelSize = bottomPanelView.update(
transition: bottomPanelTransition,
component: bottomPanel,

View file

@ -7,10 +7,12 @@ import TelegramCore
import TelegramPresentationData
import AppBundle
import PhoneNumberFormat
import AccountContext
private let titleFont = Font.regular(17.0)
public class ContactsAddItem: ListViewItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let phoneNumber: String
@ -18,7 +20,8 @@ public class ContactsAddItem: ListViewItem {
public let header: ListViewItemHeader?
public init(theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) {
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.phoneNumber = phoneNumber
@ -187,7 +190,7 @@ class ContactsAddItemNode: ListViewItemNode {
let leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 10.0 + params.rightInset
let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor)
let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(context: item.context, number: item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))

View file

@ -124,7 +124,7 @@ private enum ContactListSearchEntry: Comparable, Identifiable {
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?) -> ListViewItem {
switch self {
case let .addContact(theme, strings, phoneNumber):
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
addContact?(phoneNumber)
})
case let .peer(_, theme, strings, peer, presence, group, enabled):

View file

@ -86,6 +86,10 @@ swift_library(
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/StickerResources:StickerResources",
"//submodules/ImageBlur:ImageBlur",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
],
visibility = [
"//visibility:public",

View file

@ -2274,10 +2274,10 @@ private final class ColorPickerContent: CombinedComponent {
let swatch1Button = swatch1Button.update(
component: ColorSwatchComponent(
type: .pallete(false),
type: .pallete(state.selectedColor == DrawingColor(color: .black)),
color: DrawingColor(color: .black),
action: {
state.updateColor(DrawingColor(color: .black))
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
@ -2290,10 +2290,10 @@ private final class ColorPickerContent: CombinedComponent {
let swatch2Button = swatch2Button.update(
component: ColorSwatchComponent(
type: .pallete(false),
type: .pallete(state.selectedColor == DrawingColor(rgb: 0x0161fd)),
color: DrawingColor(rgb: 0x0161fd),
action: {
state.updateColor(DrawingColor(rgb: 0x0161fd))
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
@ -2306,9 +2306,10 @@ private final class ColorPickerContent: CombinedComponent {
let swatch3Button = swatch3Button.update(
component: ColorSwatchComponent(
type: .pallete(false),
type: .pallete(state.selectedColor == DrawingColor(rgb: 0x32c759)),
color: DrawingColor(rgb: 0x32c759),
action: {
state.updateColor(DrawingColor(rgb: 0x32c759))
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
@ -2321,9 +2322,10 @@ private final class ColorPickerContent: CombinedComponent {
let swatch4Button = swatch4Button.update(
component: ColorSwatchComponent(
type: .pallete(false),
type: .pallete(state.selectedColor == DrawingColor(rgb: 0xffcc02)),
color: DrawingColor(rgb: 0xffcc02),
action: {
state.updateColor(DrawingColor(rgb: 0xffcc02))
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
@ -2336,9 +2338,10 @@ private final class ColorPickerContent: CombinedComponent {
let swatch5Button = swatch5Button.update(
component: ColorSwatchComponent(
type: .pallete(false),
type: .pallete(state.selectedColor == DrawingColor(rgb: 0xff3a30)),
color: DrawingColor(rgb: 0xff3a30),
action: {
state.updateColor(DrawingColor(rgb: 0xff3a30))
}
),
availableSize: CGSize(width: 30.0, height: 30.0),

View file

@ -3,8 +3,20 @@ import UIKit
import Display
import AccountContext
final class DrawingBubbleEntity: DrawingEntity {
public enum DrawType {
final class DrawingBubbleEntity: DrawingEntity, Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case drawType
case color
case lineWidth
case referenceDrawingSize
case position
case size
case rotation
case tailPosition
}
public enum DrawType: Codable {
case fill
case stroke
}
@ -22,6 +34,10 @@ final class DrawingBubbleEntity: DrawingEntity {
var rotation: CGFloat
var tailPosition: CGPoint
var center: CGPoint {
return self.position
}
init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
self.isAnimated = false
@ -37,10 +53,33 @@ final class DrawingBubbleEntity: DrawingEntity {
self.tailPosition = CGPoint(x: 0.16, y: 0.18)
}
var center: CGPoint {
return self.position
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.isAnimated = false
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.position = try container.decode(CGPoint.self, forKey: .position)
self.size = try container.decode(CGSize.self, forKey: .size)
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.drawType, forKey: .drawType)
try container.encode(self.color, forKey: .color)
try container.encode(self.lineWidth, forKey: .lineWidth)
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)
try container.encode(self.size, forKey: .size)
try container.encode(self.rotation, forKey: .rotation)
try container.encode(self.tailPosition, forKey: .tailPosition)
}
func duplicate() -> DrawingEntity {
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
newEntity.referenceDrawingSize = self.referenceDrawingSize
@ -138,9 +177,6 @@ final class DrawingBubbleEntityView: DrawingEntityView {
return
}
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
// selectionView.scale = scale
selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
selectionView.setNeedsLayout()
}

View file

@ -17,10 +17,102 @@ protocol DrawingEntity: AnyObject {
func makeView(context: AccountContext) -> DrawingEntityView
}
enum CodableDrawingEntity {
case sticker(DrawingStickerEntity)
case text(DrawingTextEntity)
case simpleShape(DrawingSimpleShapeEntity)
case bubble(DrawingBubbleEntity)
case vector(DrawingVectorEntity)
init?(entity: DrawingEntity) {
if let entity = entity as? DrawingStickerEntity {
self = .sticker(entity)
} else if let entity = entity as? DrawingTextEntity {
self = .text(entity)
} else if let entity = entity as? DrawingSimpleShapeEntity {
self = .simpleShape(entity)
} else if let entity = entity as? DrawingBubbleEntity {
self = .bubble(entity)
} else if let entity = entity as? DrawingVectorEntity {
self = .vector(entity)
} else {
return nil
}
}
var entity: DrawingEntity {
switch self {
case let .sticker(entity):
return entity
case let .text(entity):
return entity
case let .simpleShape(entity):
return entity
case let .bubble(entity):
return entity
case let .vector(entity):
return entity
}
}
}
extension CodableDrawingEntity: Codable {
private enum CodingKeys: String, CodingKey {
case type
case entity
}
private enum EntityType: Int, Codable {
case sticker
case text
case simpleShape
case bubble
case vector
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(EntityType.self, forKey: .type)
switch type {
case .sticker:
self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity))
case .text:
self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity))
case .simpleShape:
self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity))
case .bubble:
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
case .vector:
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .sticker(payload):
try container.encode(EntityType.sticker, forKey: .type)
try container.encode(payload, forKey: .entity)
case let .text(payload):
try container.encode(EntityType.text, forKey: .type)
try container.encode(payload, forKey: .entity)
case let .simpleShape(payload):
try container.encode(EntityType.simpleShape, forKey: .type)
try container.encode(payload, forKey: .entity)
case let .bubble(payload):
try container.encode(EntityType.bubble, forKey: .type)
try container.encode(payload, forKey: .entity)
case let .vector(payload):
try container.encode(EntityType.vector, forKey: .type)
try container.encode(payload, forKey: .entity)
}
}
}
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
weak var selectionContainerView: DrawingSelectionContainerView?
private var tapGestureRecognizer: UITapGestureRecognizer!
@ -30,16 +122,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
init(context: AccountContext, size: CGSize, entities: [DrawingEntity] = []) {
public init(context: AccountContext, size: CGSize) {
self.context = context
self.size = size
super.init(frame: CGRect(origin: .zero, size: size))
for entity in entities {
self.add(entity)
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecognizer)
self.tapGestureRecognizer = tapGestureRecognizer
@ -48,7 +136,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var entities: [DrawingEntity] {
var entities: [DrawingEntity] = []
for case let view as DrawingEntityView in self.subviews {
@ -57,6 +145,26 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return entities
}
public func setup(withEntitiesData entitiesData: Data!) {
self.clear()
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
let entities = codableEntities.map { $0.entity }
for entity in entities {
self.add(entity)
}
}
}
var entitiesData: Data? {
let entities = self.entities.compactMap({ CodableDrawingEntity(entity: $0) })
if let data = try? JSONEncoder().encode(entities) {
return data
} else {
return nil
}
}
private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint {
let offsetLength = round(self.size.width * 0.1)
let offset = CGPoint(x: offsetLength, y: offsetLength)
@ -118,7 +226,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
sticker.position = center
if setup {
sticker.referenceDrawingSize = self.size
sticker.scale = 1.0
if !sticker.file.isVideoSticker && sticker.file.mimeType.hasSuffix("webm") {
sticker.scale = 4.0
} else {
sticker.scale = 1.0
}
}
} else if let bubble = entity as? DrawingBubbleEntity {
bubble.position = center
@ -171,11 +283,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
func removeAll() {
self.clear()
self.selectionChanged(nil)
self.hasSelectionChanged(false)
}
private func clear() {
for case let view as DrawingEntityView in self.subviews {
view.removeFromSuperview()
}
self.selectionChanged(nil)
self.hasSelectionChanged(false)
}
func bringToFront(uuid: UUID) {
@ -222,7 +338,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
view.updateVisibility(visibility)
}
}
@objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) {
let location = gestureRecognzier.location(in: self)
@ -256,7 +372,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
}
}
if let entity = entity, let entityView = self.getView(for: entity.uuid) {
self.selectedEntityView = entityView
@ -298,7 +414,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
return result
}
public func clearSelection() {
self.selectEntity(nil)
}
@ -382,7 +498,7 @@ public class DrawingEntityView: UIView {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center

View file

@ -969,7 +969,7 @@ struct Polyline {
self.azimuth = azimuth
self.touchPoint = touchPoint
}
init(touchPoint: TouchPath.Point) {
self.location = touchPoint.event.location
self.force = touchPoint.event.force
@ -989,6 +989,17 @@ struct Polyline {
touchPoint: self.touchPoint
)
}
func withLocation(_ point: CGPoint) -> Polyline.Point {
return Point(
location: point,
force: self.force,
altitudeAngle: self.altitudeAngle,
azimuth: self.azimuth,
velocity: self.velocity,
touchPoint: self.touchPoint
)
}
}
public internal(set) var isComplete: Bool
@ -1023,9 +1034,8 @@ struct Polyline {
}
init(points: [Point]) {
assert(!points.isEmpty)
self.isComplete = true
self.touchIdentifier = points.first!.event.touchIdentifier
self.touchIdentifier = points.first?.event.touchIdentifier ?? ""
self.points = points
}
@ -1063,7 +1073,6 @@ struct Polyline {
}
}
// Remove points from the end of the list toward the beginning
for index in indexesToRemove.reversed() {
guard index < points.count else {
print("Error: unknown polyline index \(index)")

View file

@ -2,9 +2,10 @@ import Foundation
import UIKit
import QuartzCore
import MetalKit
import Display
import AppBundle
public final class DrawingMetalView: MTKView {
final class DrawingMetalView: MTKView {
private let size: CGSize
private let commandQueue: MTLCommandQueue
@ -20,7 +21,7 @@ public final class DrawingMetalView: MTKView {
private var markerBrush: Brush?
private var pencilBrush: Brush?
public init?(size: CGSize) {
init?(size: CGSize) {
var size = size
if Int(size.width) % 16 != 0 {
size.width = round(size.width / 16.0) * 16.0
@ -74,12 +75,22 @@ public final class DrawingMetalView: MTKView {
}
func drawInContext(_ cgContext: CGContext) {
guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
// guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
// return
// }
// let context = CIContext(cgContext: cgContext)
// let rect = CGRect(origin: .zero, size: ciImage.extent.size)
// context.draw(ciImage, in: rect, from: rect)
guard let texture = self.drawable?.texture, let image = texture.createCGImage() else {
return
}
let context = CIContext(cgContext: cgContext)
let rect = CGRect(origin: .zero, size: ciImage.extent.size)
context.draw(ciImage, in: rect, from: rect)
let rect = CGRect(origin: .zero, size: CGSize(width: image.width, height: image.height))
cgContext.saveGState()
cgContext.translateBy(x: rect.midX, y: rect.midY)
cgContext.scaleBy(x: 1.0, y: -1.0)
cgContext.translateBy(x: -rect.midX, y: -rect.midY)
cgContext.draw(image, in: rect)
cgContext.restoreGState()
}
private func setup() {
@ -116,7 +127,7 @@ public final class DrawingMetalView: MTKView {
self.penBrush = Brush(texture: nil, target: self, rotation: .ahead)
if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) {
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(0.0))
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(-0.55))
}
if let url = getAppBundle().url(forResource: "pencil", withExtension: "png"), let data = try? Data(contentsOf: url) {
@ -124,10 +135,11 @@ public final class DrawingMetalView: MTKView {
}
}
override public func draw(_ rect: CGRect) {
var clearOnce = false
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let drawable = self.drawable, let texture = drawable.texture else {
guard let drawable = self.drawable, let texture = drawable.texture?.texture else {
return
}
@ -138,6 +150,10 @@ public final class DrawingMetalView: MTKView {
attachment?.loadAction = .clear
attachment?.storeAction = .store
guard let _ = attachment?.texture else {
return
}
let commandBuffer = self.commandQueue.makeCommandBuffer()
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
@ -160,10 +176,11 @@ public final class DrawingMetalView: MTKView {
return
}
self.clearOnce = true
drawable.updateBuffer(with: self.size)
drawable.clear()
drawable.commit()
drawable.commit(wait: true)
}
enum BrushType {
@ -182,10 +199,21 @@ public final class DrawingMetalView: MTKView {
self.pencilBrush?.updated(point, color: color, state: state, size: size)
}
}
func setup(_ points: [CGPoint], brush: BrushType, color: DrawingColor, size: CGFloat) {
switch brush {
case .pen:
self.penBrush?.setup(points, color: color, size: size)
case .marker:
self.markerBrush?.setup(points, color: color, size: size)
case .pencil:
self.pencilBrush?.setup(points, color: color, size: size)
}
}
}
private class Drawable {
public private(set) var texture: MTLTexture?
public private(set) var texture: Texture?
internal var pixelFormat: MTLPixelFormat = .bgra8Unorm
internal var size: CGSize
@ -199,12 +227,12 @@ private class Drawable {
self.size = size
self.pixelFormat = pixelFormat
self.device = device
self.texture = self.makeColorTexture(DrawingColor.clear)
self.texture = self.makeTexture()
self.commandQueue = device?.makeCommandQueue()
self.renderPassDescriptor = MTLRenderPassDescriptor()
let attachment = self.renderPassDescriptor?.colorAttachments[0]
attachment?.texture = self.texture
attachment?.texture = self.texture?.texture
attachment?.loadAction = .load
attachment?.storeAction = .store
@ -212,9 +240,8 @@ private class Drawable {
}
func clear() {
self.texture = self.makeColorTexture(DrawingColor.clear)
self.renderPassDescriptor?.colorAttachments[0].texture = self.texture
self.commit()
self.texture?.clear()
self.commit(wait: true)
}
internal func updateBuffer(with size: CGSize) {
@ -226,49 +253,40 @@ private class Drawable {
internal func prepareForDraw() {
if self.commandBuffer == nil {
self.commandBuffer = commandQueue?.makeCommandBuffer()
self.commandBuffer = self.commandQueue?.makeCommandBuffer()
}
}
internal func makeCommandEncoder() -> MTLRenderCommandEncoder? {
guard let commandBuffer = self.commandBuffer, let rpd = renderPassDescriptor else {
guard let commandBuffer = self.commandBuffer, let renderPassDescriptor = self.renderPassDescriptor else {
return nil
}
return commandBuffer.makeRenderCommandEncoder(descriptor: rpd)
return commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
}
internal func commit() {
internal func commit(wait: Bool = false) {
self.commandBuffer?.commit()
if wait {
self.commandBuffer?.waitUntilCompleted()
}
self.commandBuffer = nil
}
internal func makeColorTexture(_ color: DrawingColor) -> MTLTexture? {
guard self.size.width * self.size.height > 0 else {
internal func makeTexture() -> Texture? {
guard self.size.width * self.size.height > 0, let device = self.device else {
return nil
}
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: self.pixelFormat,
width: Int(self.size.width),
height: Int(self.size.height),
mipmapped: false
)
textureDescriptor.usage = [.renderTarget, .shaderRead]
guard let texture = device?.makeTexture(descriptor: textureDescriptor) else {
return nil
}
let region = MTLRegion(
origin: MTLOrigin(x: 0, y: 0, z: 0),
size: MTLSize(width: texture.width, height: texture.height, depth: 1)
)
let bytesPerRow = 4 * texture.width
let data = Data(capacity: Int(bytesPerRow * texture.height))
if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow)
}
return texture
return Texture(device: device, width: Int(self.size.width), height: Int(self.size.height))
}
}
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
private class Brush {
private(set) var texture: MTLTexture?
private(set) var pipelineState: MTLRenderPipelineState!
@ -356,7 +374,7 @@ private class Brush {
switch state {
case .began:
self.bezier.begin(with: point)
self.pushPoint(point, color: color, size: size, isEnd: false)
let _ = self.pushPoint(point, color: color, size: size, isEnd: false)
case .changed:
if self.bezier.points.count > 0 && point != lastRenderedPoint {
self.pushPoint(point, color: color, size: size, isEnd: false)
@ -370,6 +388,45 @@ private class Brush {
}
}
func setup(_ inputPoints: [CGPoint], color: DrawingColor, size: CGFloat) {
guard inputPoints.count >= 2 else {
return
}
var pointStep: CGFloat
if case .random = self.rotation {
pointStep = size * 0.1
} else {
pointStep = 2.0
}
var lines: [Line] = []
var previousPoint = inputPoints[0]
var points: [CGPoint] = []
self.bezier.begin(with: inputPoints.first!)
for point in inputPoints {
let smoothPoints = self.bezier.pushPoint(point)
points.append(contentsOf: smoothPoints)
}
self.bezier.finish()
for i in 1 ..< points.count {
let p = points[i]
if (i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) {
let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep)
lines.append(line)
previousPoint = p
}
}
if let drawable = self.target?.drawable {
let stroke = Stroke(color: color, lines: lines, target: drawable)
self.render(stroke: stroke, in: drawable)
drawable.commit(wait: true)
}
}
private var lastRenderedPoint: CGPoint?
func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) {
var pointStep: CGFloat
@ -396,9 +453,7 @@ private class Brush {
if let drawable = self.target?.drawable {
let stroke = Stroke(color: color, lines: lines, target: drawable)
self.render(stroke: stroke, in: drawable)
drawable.commit()
}
}
@ -489,8 +544,7 @@ class BezierGenerator {
return []
}
step += 1
let result = genericPathPoints()
return result
return self.generateSmoothPathPoints()
}
func finish() {
@ -501,7 +555,7 @@ class BezierGenerator {
var points: [CGPoint] = []
private var step = 0
private func genericPathPoints() -> [CGPoint] {
private func generateSmoothPathPoints() -> [CGPoint] {
var begin: CGPoint
var control: CGPoint
let end = CGPoint.middle(p1: points[step], p2: points[step + 1])
@ -552,3 +606,149 @@ private struct Line {
return self.end.angle(to: self.start)
}
}
final class Texture {
#if !targetEnvironment(simulator)
let buffer: MTLBuffer
#endif
let width: Int
let height: Int
let bytesPerRow: Int
let texture: MTLTexture
init?(device: MTLDevice, width: Int, height: Int) {
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
if #available(iOS 12.0, *) {
#if targetEnvironment(simulator)
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget, .shaderRead]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
#else
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
return nil
}
self.buffer = buffer
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = buffer.storageMode
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
return nil
}
#endif
self.texture = texture
} else {
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget, .shaderRead]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
self.texture = texture
}
self.clear()
// let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
// pixelFormat: self.pixelFormat,
// width: Int(self.size.width),
// height: Int(self.size.height),
// mipmapped: false
// )
// textureDescriptor.usage = [.renderTarget, .shaderRead]
// guard let texture = device?.makeTexture(descriptor: textureDescriptor) else {
// return nil
// }
// let region = MTLRegion(
// origin: MTLOrigin(x: 0, y: 0, z: 0),
// size: MTLSize(width: texture.width, height: texture.height, depth: 1)
// )
// let bytesPerRow = 4 * texture.width
// let data = Data(capacity: Int(bytesPerRow * texture.height))
// if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
// texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow)
// }
// return texture
}
func clear() {
let region = MTLRegion(
origin: MTLOrigin(x: 0, y: 0, z: 0),
size: MTLSize(width: self.width, height: self.height, depth: 1)
)
let data = Data(capacity: Int(self.bytesPerRow * self.height))
if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: self.bytesPerRow)
}
}
func createCGImage() -> CGImage? {
#if targetEnvironment(simulator)
guard let data = NSMutableData(capacity: self.bytesPerRow * self.height) else {
return nil
}
data.length = self.bytesPerRow * self.height
self.texture.getBytes(data.mutableBytes, bytesPerRow: self.bytesPerRow, bytesPerImage: self.bytesPerRow * self.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.width, height: self.height, depth: 1)), mipmapLevel: 0, slice: 0)
guard let dataProvider = CGDataProvider(data: data as CFData) else {
return nil
}
#else
let content = self.content
let pool = self.pool
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.buffer.contents(), count: self.buffer.length, deallocator: .custom { [weak pool] _, _ in
// guard let pool = pool else {
// return
// }
// pool.recycle(content: content)
}) as CFData) else {
return nil
}
#endif
guard let image = CGImage(
width: Int(self.width),
height: Int(self.height),
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return image
}
}

View file

@ -15,6 +15,7 @@ import ComponentDisplayAdapters
import LottieAnimationComponent
import ViewControllerComponent
import ContextUI
import ChatEntityKeyboardInputNode
enum DrawingToolState: Equatable {
enum Key: CaseIterable {
@ -279,10 +280,10 @@ struct DrawingState: Equatable {
return DrawingState(
selectedTool: .pen,
tools: [
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.25, mode: .round)),
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.5, mode: .round)),
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.5, mode: .round)),
.pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.5, mode: .round)),
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.2, mode: .round)),
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.75, mode: .round)),
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.4, mode: .round)),
.pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.3, mode: .round)),
.lasso,
.eraser(DrawingToolState.EraserState(size: 0.5, mode: .bitmap))
]
@ -308,6 +309,7 @@ enum DrawingScreenTransition {
}
private let undoButtonTag = GenericComponentViewTag()
private let redoButtonTag = GenericComponentViewTag()
private let clearAllButtonTag = GenericComponentViewTag()
private let colorButtonTag = GenericComponentViewTag()
private let addButtonTag = GenericComponentViewTag()
@ -699,7 +701,7 @@ private final class DrawingScreenComponent: CombinedComponent {
}
func addTextEntity() {
let textEntity = DrawingTextEntity(text: "", style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: self.currentColor)
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
self.insertEntity.invoke(textEntity)
}
@ -811,6 +813,7 @@ private final class DrawingScreenComponent: CombinedComponent {
style: DrawingTextStyle(style: textEntity.style),
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
font: DrawingTextFont(font: textEntity.font),
isEmojiKeyboard: false,
toggleStyle: { [weak state, weak textEntity] in
guard let textEntity = textEntity else {
return
@ -860,7 +863,8 @@ private final class DrawingScreenComponent: CombinedComponent {
entityView.update()
}
state?.updated(transition: .easeInOut(duration: 0.2))
}
},
toggleKeyboard: nil
),
availableSize: CGSize(width: context.availableSize.width - 84.0, height: 44.0),
transition: context.transition
@ -1200,7 +1204,7 @@ private final class DrawingScreenComponent: CombinedComponent {
action: {
performAction.invoke(.redo)
}
).minSize(CGSize(width: 44.0, height: 44.0)),
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
transition: context.transition
)
@ -1368,37 +1372,40 @@ private final class DrawingScreenComponent: CombinedComponent {
image = nil
}
let brushModeButton = brushModeButton.update(
component: Button(
content: AnyComponent(
BrushButtonContent(
title: title,
image: image ?? UIImage()
)
),
action: { [weak state] in
guard let controller = controller() as? DrawingScreen else {
return
}
if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View {
if isEraser {
state?.presentEraserModePicker(buttonView)
} else {
state?.presentBrushModePicker(buttonView)
if [.pen, .eraser].contains(state.drawingState.selectedTool) {
let brushModeButton = brushModeButton.update(
component: Button(
content: AnyComponent(
BrushButtonContent(
title: title,
image: image ?? UIImage()
)
),
action: { [weak state] in
guard let controller = controller() as? DrawingScreen else {
return
}
if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View {
if isEraser {
state?.presentEraserModePicker(buttonView)
} else {
state?.presentBrushModePicker(buttonView)
}
}
}
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag),
availableSize: CGSize(width: 75.0, height: 33.0),
transition: .immediate
)
context.add(brushModeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
modeRightInset += 35.0
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag),
availableSize: CGSize(width: 75.0, height: 33.0),
transition: .immediate
)
context.add(brushModeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
modeRightInset += 35.0
} else {
modeRightInset = 16.0
}
} else {
var isFilled = false
if let entity = state.selectedEntity as? DrawingSimpleShapeEntity, case .fill = entity.drawType {
@ -1662,7 +1669,7 @@ private final class DrawingScreenComponent: CombinedComponent {
}
public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
fileprivate final class Node: ViewControllerTracingNode, FPSCounterDelegate {
fileprivate final class Node: ViewControllerTracingNode {
private weak var controller: DrawingScreen?
private let context: AccountContext
private let updateState: ActionSlot<DrawingView.NavigationState>
@ -1685,10 +1692,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private var presentationData: PresentationData
private let hapticFeedback = HapticFeedback()
private var validLayout: ContainerViewLayout?
private let fpsCounter = FPSCounter()
private var fpsLabel: UILabel?
private var _drawingView: DrawingView?
var drawingView: DrawingView {
if self._drawingView == nil, let controller = self.controller {
@ -1741,7 +1745,26 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
}
self.performAction.connect { [weak self] action in
if let strongSelf = self {
strongSelf._drawingView?.performAction(action)
if action == .clear {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?._drawingView?.performAction(action)
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
} else {
strongSelf._drawingView?.performAction(action)
}
}
}
self.updateToolState.connect { [weak self] state in
@ -1762,7 +1785,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private var _entitiesView: DrawingEntitiesView?
var entitiesView: DrawingEntitiesView {
if self._entitiesView == nil, let controller = self.controller {
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size, entities: [])
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
self._drawingView?.entitiesView = self._entitiesView
let entitiesLayer = self.entitiesView.layer
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
@ -1917,10 +1940,33 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
super.init()
self.apply.connect { [weak self] _ in
self?.controller?.requestApply()
if let strongSelf = self {
strongSelf.controller?.requestApply()
}
}
self.dismiss.connect { [weak self] _ in
self?.controller?.requestDismiss()
if let strongSelf = self {
if !strongSelf.drawingView.isEmpty {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?.controller?.requestDismiss()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
} else {
strongSelf.controller?.requestDismiss()
}
}
}
}
@ -1929,23 +1975,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
self.view.disablesInteractiveKeyboardGestureRecognizer = true
self.view.disablesInteractiveTransitionGestureRecognizer = true
if self.fpsLabel == nil {
let fpsLabel = UILabel(frame: CGRect(origin: CGPoint(x: 30.0, y: 10.0), size: CGSize(width: 120.0, height: 44.0)))
fpsLabel.alpha = 0.1
fpsLabel.textColor = .white
// self.view.addSubview(fpsLabel)
self.fpsLabel = fpsLabel
}
self.fpsCounter.delegate = self
self.fpsCounter.startTracking()
}
}
func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int) {
self.fpsLabel?.text = "\(fps)"
}
func presentEyedropper(dismissed: @escaping () -> Void) {
guard let controller = self.controller else {
return
@ -2064,6 +2095,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
@ -2190,6 +2226,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
style: DrawingTextStyle(style: textEntity.style),
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
font: DrawingTextFont(font: textEntity.font),
isEmojiKeyboard: entityView.textView.inputView != nil,
presentColorPicker: { [weak self] in
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
return
@ -2262,6 +2299,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
}
},
toggleKeyboard: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleInputMode()
}
)
),
@ -2280,6 +2323,58 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
}
}
}
private func toggleInputMode() {
guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else {
return
}
let textView = entityView.textView
var shouldHaveInputView = false
if textView.isFirstResponder {
if textView.inputView == nil {
shouldHaveInputView = true
}
} else {
shouldHaveInputView = true
}
if shouldHaveInputView {
let inputView = EntityInputView(context: self.context, isDark: true, areCustomEmojiEnabled: true)
inputView.insertText = { [weak entityView] text in
entityView?.insertText(text)
}
inputView.deleteBackwards = { [weak textView] in
textView?.deleteBackward()
}
inputView.switchToKeyboard = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.toggleInputMode()
}
// inputView?.presentController = { [weak self] c in
// guard let strongSelf = self else {
// return
// }
// strongSelf.presentController(c)
// }
textView.inputView = inputView
} else {
textView.inputView = nil
}
if textView.isFirstResponder {
textView.reloadInputViews()
} else {
textView.becomeFirstResponder()
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate)
}
}
}
fileprivate var node: Node {
@ -2288,14 +2383,16 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
private let context: AccountContext
private let size: CGSize
private let originalSize: CGSize
public var requestDismiss: (() -> Void)!
public var requestApply: (() -> Void)!
public var getCurrentImage: (() -> UIImage?)!
public init(context: AccountContext, size: CGSize) {
public init(context: AccountContext, size: CGSize, originalSize: CGSize) {
self.context = context
self.size = size
self.originalSize = originalSize
super.init(navigationBarPresentationData: nil)
@ -2342,36 +2439,36 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
}, opaque: false, scale: 1.0)
var hasAnimatedEntities = false
var legacyEntities: [TGPhotoPaintEntity] = []
for entity in self.entitiesView.entities {
if entity.isAnimated {
hasAnimatedEntities = true
break
}
if let entity = entity as? DrawingStickerEntity {
let coder = PostboxEncoder()
coder.encodeRootObject(entity.file)
let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38)
if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) {
stickerEntity.position = entity.position
stickerEntity.scale = entity.scale
stickerEntity.angle = entity.rotation
legacyEntities.append(stickerEntity)
}
} else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView {
let textEntity = TGPhotoPaintStaticEntity()
textEntity.position = entity.position
textEntity.angle = entity.rotation
textEntity.renderImage = view.getRenderImage()
legacyEntities.append(textEntity)
} else if let _ = entity as? DrawingSimpleShapeEntity {
} else if let _ = entity as? DrawingBubbleEntity {
} else if let _ = entity as? DrawingVectorEntity {
}
// if let entity = entity as? DrawingStickerEntity {
// let coder = PostboxEncoder()
// coder.encodeRootObject(entity.file)
//
// let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38)
// if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) {
// stickerEntity.position = entity.position
// stickerEntity.scale = entity.scale
// stickerEntity.angle = entity.rotation
// legacyEntities.append(stickerEntity)
// }
// } else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView {
// let textEntity = TGPhotoPaintStaticEntity()
// textEntity.position = entity.position
// textEntity.angle = entity.rotation
// textEntity.renderImage = view.getRenderImage()
// legacyEntities.append(textEntity)
// } else if let _ = entity as? DrawingSimpleShapeEntity {
//
// } else if let _ = entity as? DrawingBubbleEntity {
//
// } else if let _ = entity as? DrawingVectorEntity {
//
// }
}
let finalImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in
@ -2396,7 +2493,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
image = finalImage
}
return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil)
return TGPaintingData(drawing: nil, entitiesData: self.entitiesView.entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities)
// return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil)
}
public func resultImage() -> UIImage! {

View file

@ -3,14 +3,26 @@ import UIKit
import Display
import AccountContext
final class DrawingSimpleShapeEntity: DrawingEntity {
public enum ShapeType {
final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case shapeType
case drawType
case color
case lineWidth
case referenceDrawingSize
case position
case size
case rotation
}
public enum ShapeType: Codable {
case rectangle
case ellipse
case star
}
public enum DrawType {
public enum DrawType: Codable {
case fill
case stroke
}
@ -28,6 +40,10 @@ final class DrawingSimpleShapeEntity: DrawingEntity {
var size: CGSize
var rotation: CGFloat
var center: CGPoint {
return self.position
}
init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
self.isAnimated = false
@ -43,10 +59,33 @@ final class DrawingSimpleShapeEntity: DrawingEntity {
self.rotation = 0.0
}
var center: CGPoint {
return self.position
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.isAnimated = false
self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType)
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.position = try container.decode(CGPoint.self, forKey: .position)
self.size = try container.decode(CGSize.self, forKey: .size)
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.shapeType, forKey: .shapeType)
try container.encode(self.drawType, forKey: .drawType)
try container.encode(self.color, forKey: .color)
try container.encode(self.lineWidth, forKey: .lineWidth)
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)
try container.encode(self.size, forKey: .size)
try container.encode(self.rotation, forKey: .rotation)
}
func duplicate() -> DrawingEntity {
let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
newEntity.referenceDrawingSize = self.referenceDrawingSize

View file

@ -8,7 +8,18 @@ import TelegramAnimatedStickerNode
import StickerResources
import AccountContext
final class DrawingStickerEntity: DrawingEntity {
final class DrawingStickerEntity: DrawingEntity, Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case isAnimated
case file
case referenceDrawingSize
case position
case scale
case rotation
case mirrored
}
let uuid: UUID
let isAnimated: Bool
let file: TelegramMediaFile
@ -22,6 +33,10 @@ final class DrawingStickerEntity: DrawingEntity {
var color: DrawingColor = DrawingColor.clear
var lineWidth: CGFloat = 0.0
var center: CGPoint {
return self.position
}
init(file: TelegramMediaFile) {
self.uuid = UUID()
self.isAnimated = file.isAnimatedSticker
@ -35,10 +50,30 @@ final class DrawingStickerEntity: DrawingEntity {
self.mirrored = false
}
var center: CGPoint {
return self.position
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.isAnimated = try container.decode(Bool.self, forKey: .isAnimated)
self.file = try container.decode(TelegramMediaFile.self, forKey: .file)
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.position = try container.decode(CGPoint.self, forKey: .position)
self.scale = try container.decode(CGFloat.self, forKey: .scale)
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.isAnimated, forKey: .isAnimated)
try container.encode(self.file, forKey: .file)
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)
try container.encode(self.scale, forKey: .scale)
try container.encode(self.rotation, forKey: .rotation)
try container.encode(self.mirrored, forKey: .mirrored)
}
func duplicate() -> DrawingEntity {
let newEntity = DrawingStickerEntity(file: self.file)
newEntity.referenceDrawingSize = self.referenceDrawingSize
@ -102,7 +137,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
private func setup() {
if let dimensions = self.file.dimensions {
if self.file.isAnimatedSticker || self.file.isVideoSticker {
if self.file.isAnimatedSticker || self.file.isVideoSticker || self.file.mimeType == "video/webm" {
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = false
@ -176,7 +211,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
self.didSetUpAnimationNode = true
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker)
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker || self.file.mimeType == "video/webm")
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)

View file

@ -1,10 +1,60 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AccountContext
import TextFormat
import EmojiTextAttachmentView
final class DrawingTextEntity: DrawingEntity {
enum Style {
final class DrawingTextEntity: DrawingEntity, Codable {
final class CustomEmojiAttribute: Codable {
private enum CodingKeys: String, CodingKey {
case attribute
case rangeOrigin
case rangeLength
}
let attribute: ChatTextInputTextCustomEmojiAttribute
let range: NSRange
init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) {
self.attribute = attribute
self.range = range
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute)
let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin)
let rangeLength = try container.decode(Int.self, forKey: .rangeLength)
self.range = NSMakeRange(rangeOrigin, rangeLength)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.attribute, forKey: .attribute)
try container.encode(self.range.location, forKey: .rangeOrigin)
try container.encode(self.range.length, forKey: .rangeLength)
}
}
private enum CodingKeys: String, CodingKey {
case uuid
case text
case textAttributes
case style
case font
case alignment
case fontSize
case color
case referenceDrawingSize
case position
case width
case scale
case rotation
}
enum Style: Codable {
case regular
case filled
case semi
@ -24,7 +74,7 @@ final class DrawingTextEntity: DrawingEntity {
}
}
enum Font {
enum Font: Codable {
case sanFrancisco
case newYork
case monospaced
@ -44,7 +94,7 @@ final class DrawingTextEntity: DrawingEntity {
}
}
enum Alignment {
enum Alignment: Codable {
case left
case center
case right
@ -73,9 +123,17 @@ final class DrawingTextEntity: DrawingEntity {
}
var uuid: UUID
let isAnimated: Bool
var isAnimated: Bool {
var isAnimated = false
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
isAnimated = true
}
})
return isAnimated
}
var text: String
var text: NSAttributedString
var style: Style
var font: Font
var alignment: Alignment
@ -86,11 +144,15 @@ final class DrawingTextEntity: DrawingEntity {
var referenceDrawingSize: CGSize
var position: CGPoint
var width: CGFloat
var scale: CGFloat
var rotation: CGFloat
init(text: String, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
var center: CGPoint {
return self.position
}
init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
self.uuid = UUID()
self.isAnimated = false
self.text = text
self.style = style
@ -102,18 +164,65 @@ final class DrawingTextEntity: DrawingEntity {
self.referenceDrawingSize = .zero
self.position = .zero
self.width = 100.0
self.scale = 1.0
self.rotation = 0.0
}
var center: CGPoint {
return self.position
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
let text = try container.decode(String.self, forKey: .text)
let attributedString = NSMutableAttributedString(string: text)
let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes)
for attribute in textAttributes {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range)
}
self.text = attributedString
self.style = try container.decode(Style.self, forKey: .style)
self.font = try container.decode(Font.self, forKey: .font)
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.position = try container.decode(CGPoint.self, forKey: .position)
self.width = try container.decode(CGFloat.self, forKey: .width)
self.scale = try container.decode(CGFloat.self, forKey: .scale)
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.text.string, forKey: .text)
var textAttributes: [CustomEmojiAttribute] = []
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
textAttributes.append(CustomEmojiAttribute(attribute: value, range: range))
}
})
try container.encode(textAttributes, forKey: .textAttributes)
try container.encode(self.style, forKey: .style)
try container.encode(self.font, forKey: .font)
try container.encode(self.alignment, forKey: .alignment)
try container.encode(self.fontSize, forKey: .fontSize)
try container.encode(self.color, forKey: .color)
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)
try container.encode(self.width, forKey: .width)
try container.encode(self.scale, forKey: .scale)
try container.encode(self.rotation, forKey: .rotation)
}
func duplicate() -> DrawingEntity {
let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
newEntity.referenceDrawingSize = self.referenceDrawingSize
newEntity.position = self.position
newEntity.width = self.width
newEntity.scale = self.scale
newEntity.rotation = self.rotation
return newEntity
}
@ -131,14 +240,15 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
return self.entity as! DrawingTextEntity
}
private let textView: DrawingTextView
let textView: DrawingTextView
var customEmojiContainerView: CustomEmojiContainerView?
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
init(context: AccountContext, entity: DrawingTextEntity) {
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
self.textView.backgroundColor = .clear
self.textView.text = entity.text
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.contentInset = .zero
@ -159,6 +269,15 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.addSubview(self.textView)
self.update(animated: false)
self.emojiViewProvider = { [weak self] emoji in
guard let strongSelf = self else {
return UIView()
}
let pointSize: CGFloat = 128.0
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
}
}
required init?(coder: NSCoder) {
@ -178,9 +297,62 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.endEditing()
}
func updateEntities() {
self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer)
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
if let attributedText = self.textView.attributedText {
let beginning = self.textView.beginningOfDocument
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) {
let rect = self.textView.firstRect(for: textRange)
customEmojiRects.append((rect, value))
}
}
})
}
let color = self.textEntity.color.toUIColor()
let textColor: UIColor
switch self.textEntity.style {
case .regular:
textColor = color
case .filled:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .semi:
textColor = color
case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
}
if !customEmojiRects.isEmpty {
let customEmojiContainerView: CustomEmojiContainerView
if let current = self.customEmojiContainerView {
customEmojiContainerView = current
} else {
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
return nil
}
return emojiViewProvider(emoji)
})
customEmojiContainerView.isUserInteractionEnabled = false
self.textView.addSubview(customEmojiContainerView)
self.customEmojiContainerView = customEmojiContainerView
}
customEmojiContainerView.update(fontSize: self.displayFontSize, textColor: textColor, emojiRects: customEmojiRects)
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
}
}
func beginEditing(accessoryView: UIView?) {
self._isEditing = true
if !self.textEntity.text.isEmpty {
if !self.textEntity.text.string.isEmpty {
let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity
previousEntity?.uuid = self.textEntity.uuid
self.previousEntity = previousEntity
@ -214,7 +386,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
if let selectionView = self.selectionView as? DrawingTextEntititySelectionView {
selectionView.alpha = 0.0
if !self.textEntity.text.isEmpty {
if !self.textEntity.text.string.isEmpty {
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
@ -223,6 +395,8 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
func endEditing(reset: Bool = false) {
self._isEditing = false
self.textView.resignFirstResponder()
self.textView.inputView = nil
self.textView.inputAccessoryView = nil
self.textView.isEditable = false
self.textView.isSelectable = false
@ -240,14 +414,12 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.containerView?.remove(uuid: self.textEntity.uuid)
}
} else {
self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
if self.textEntity.text.isEmpty {
// self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
if self.textEntity.text.string.isEmpty {
self.containerView?.remove(uuid: self.textEntity.uuid)
}
}
self.textView.text = self.textEntity.text
if let fadeView = self.fadeView {
self.fadeView = nil
fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fadeView] _ in
@ -288,8 +460,33 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
}
func textViewDidChange(_ textView: UITextView) {
guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else {
return
}
let range = NSMakeRange(0, updatedText.length)
updatedText.removeAttribute(.font, range: range)
updatedText.removeAttribute(.paragraphStyle, range: range)
updatedText.removeAttribute(.foregroundColor, range: range)
self.textEntity.text = updatedText
self.sizeToFit()
self.textView.setNeedsDisplay()
self.update()
}
func insertText(_ text: NSAttributedString) {
guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else {
return
}
let range = NSMakeRange(0, updatedText.length)
updatedText.removeAttribute(.font, range: range)
updatedText.removeAttribute(.paragraphStyle, range: range)
updatedText.removeAttribute(.foregroundColor, range: range)
updatedText.replaceCharacters(in: self.textView.selectedRange, with: text)
self.textEntity.text = updatedText
self.update()
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
@ -321,27 +518,65 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.textView.frame = rect
}
override func update(animated: Bool = false) {
if !self.isEditing {
self.center = self.textEntity.position
self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
}
private var displayFontSize: CGFloat {
let minFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.05)
let maxFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.45)
let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize
return fontSize
}
private func updateText() {
guard let text = self.textEntity.text.mutableCopy() as? NSMutableAttributedString else {
return
}
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
let font: UIFont
switch self.textEntity.font {
case .sanFrancisco:
self.textView.font = Font.with(size: fontSize, design: .regular, weight: .semibold)
font = Font.with(size: fontSize, design: .regular, weight: .semibold)
case .newYork:
self.textView.font = Font.with(size: fontSize, design: .serif, weight: .semibold)
font = Font.with(size: fontSize, design: .serif, weight: .semibold)
case .monospaced:
self.textView.font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
case .round:
self.textView.font = Font.with(size: fontSize, design: .round, weight: .semibold)
font = Font.with(size: fontSize, design: .round, weight: .semibold)
}
text.addAttribute(.font, value: font, range: range)
let color = self.textEntity.color.toUIColor()
let textColor: UIColor
switch self.textEntity.style {
case .regular:
textColor = color
case .filled:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .semi:
textColor = color
case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
text.enumerateAttributes(in: range) { attributes, subrange, _ in
if let _ = attributes[ChatTextInputAttributes.customEmoji] {
text.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange)
}
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = self.textEntity.alignment.alignment
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
self.textView.attributedText = text
}
override func update(animated: Bool = false) {
if !self.isEditing {
self.center = self.textEntity.position
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.textEntity.rotation), self.textEntity.scale, self.textEntity.scale)
}
self.textView.textAlignment = self.textEntity.alignment.alignment
let color = self.textEntity.color.toUIColor()
switch self.textEntity.style {
@ -358,7 +593,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.textView.strokeColor = nil
self.textView.frameColor = UIColor(rgb: 0xffffff, alpha: 0.75)
case .stroke:
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
self.textView.strokeColor = color
self.textView.frameColor = nil
}
@ -375,22 +610,35 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
self.textView.layer.shadowRadius = 0.0
}
self.updateText()
self.sizeToFit()
Queue.mainQueue().after(0.001) {
self.updateEntities()
}
super.update(animated: animated)
}
override func updateSelectionView() {
super.updateSelectionView()
guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
// selectionView.scale = scale
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
selectionView.center = self.convert(center, to: selectionView.superview)
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0))
selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
self.popIdentityTransformForMeasurement()
}
override func makeSelectionView() -> DrawingEntitySelectionView {
@ -492,7 +740,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview)
var updatedFontSize = entity.fontSize
var updatedScale = entity.scale
var updatedPosition = entity.position
var updatedRotation = entity.rotation
@ -502,7 +750,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
deltaX *= -1.0
}
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
updatedFontSize = max(0.0, min(1.0, updatedFontSize * scaleDelta))
updatedScale = max(0.01, updatedScale * scaleDelta)
let deltaAngle: CGFloat
if self.currentHandle === self.leftHandle {
@ -516,7 +764,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
updatedPosition.y += delta.y
}
entity.fontSize = updatedFontSize
entity.scale = updatedScale
entity.position = updatedPosition
entity.rotation = updatedRotation
entityView.update()
@ -537,7 +785,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
switch gestureRecognizer.state {
case .began, .changed:
let scale = gestureRecognizer.scale
entity.fontSize = max(0.0, min(1.0, entity.fontSize * scale))
entity.fontSize = max(0.1, entity.scale * scale)
entityView.update()
gestureRecognizer.scale = 1.0
@ -823,8 +1071,8 @@ private class DrawingTextStorage: NSTextStorage {
}
}
private class DrawingTextView: UITextView {
var drawingLayoutManager: DrawingTextLayoutManager {
class DrawingTextView: UITextView {
fileprivate var drawingLayoutManager: DrawingTextLayoutManager {
return self.layoutManager as! DrawingTextLayoutManager
}
@ -912,14 +1160,14 @@ private class DrawingTextView: UITextView {
super.insertText(text)
self.fixTypingAttributes()
}
override func paste(_ sender: Any?) {
self.fixTypingAttributes()
super.paste(sender)
self.fixTypingAttributes()
}
private func fixTypingAttributes() {
fileprivate func fixTypingAttributes() {
var attributes: [NSAttributedString.Key: Any] = [:]
if let font = self.font {
attributes[NSAttributedString.Key.font] = font

View file

@ -6,468 +6,6 @@ protocol DrawingRenderLayer: CALayer {
}
//final class PenTool: DrawingElement {
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
// var lineWidth: CGFloat = 0.0
//
// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, strokeWidth: CGFloat, shadowRadius: CGFloat) {
// self.contentsScale = 1.0
// self.lineWidth = lineWidth
//
//
// }
// }
//
// let uuid = UUID()
//
// let drawingSize: CGSize
// let color: DrawingColor
// let lineWidth: CGFloat
// let arrow: Bool
//
// var path: BezierPath?
// var boundingBox: CGRect?
//
// var renderPath: CGPath?
// let renderStrokeWidth: CGFloat
// let renderShadowRadius: CGFloat
// let renderLineWidth: CGFloat
//
// var translation = CGPoint()
//
// private var currentRenderLayer: DrawingRenderLayer?
//
// var bounds: CGRect {
// return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
// }
//
// var points: [Polyline.Point] {
// guard let linePath = self.path else {
// return []
// }
// var points: [Polyline.Point] = []
// for element in linePath.elements {
// if case .moveTo = element.type {
// points.append(element.startPoint.offsetBy(self.translation))
// } else {
// points.append(element.endPoint.offsetBy(self.translation))
// }
// }
// return points
// }
//
// func containsPoint(_ point: CGPoint) -> Bool {
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
// }
//
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
// if let linePath = self.path {
// let pathBoundingBox = path.bounds
// if self.bounds.intersects(pathBoundingBox) {
// for element in linePath.elements {
// if case .moveTo = element.type {
// if path.contains(element.startPoint.location.offsetBy(self.translation)) {
// return true
// }
// } else {
// if path.contains(element.startPoint.location.offsetBy(self.translation)) {
// return true
// }
// if path.contains(element.endPoint.location.offsetBy(self.translation)) {
// return true
// }
// if case .cubicCurve = element.type {
// if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
// return true
// }
// if path.contains(element.controlPoints[1].offsetBy(self.translation)) {
// return true
// }
// } else if case .quadCurve = element.type {
// if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
// return true
// }
// }
// }
// }
// }
// }
// return false
// }
//
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
// self.drawingSize = drawingSize
// self.color = color
// self.lineWidth = lineWidth
// self.arrow = arrow
//
// let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008
// let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03
//
// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003)
// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09)
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
//
// self.renderStrokeWidth = strokeWidth
// self.renderShadowRadius = shadowRadius
// self.renderLineWidth = lineWidth
// }
//
// func setupRenderLayer() -> DrawingRenderLayer? {
// let layer = RenderLayer()
// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, strokeWidth: self.renderStrokeWidth, shadowRadius: self.renderShadowRadius)
// self.currentRenderLayer = layer
// return layer
// }
//
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
// guard case let .smoothCurve(bezierPath) = path else {
// return
// }
//
// self.path = bezierPath
//
//// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
//// let lastPoint = lastPosition
//// var secondPoint = polyline.points[polyline.points.count - 2]
//// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
//// secondPoint = polyline.points[polyline.points.count - 3]
//// }
//// let angle = lastPoint.angle(to: secondPoint.location)
//// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
//// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
////
//// let arrowPath = UIBezierPath()
//// arrowPath.move(to: point2)
//// arrowPath.addLine(to: lastPoint)
//// arrowPath.addLine(to: point1)
//// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
////
//// combinedPath.usesEvenOddFillRule = false
//// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
//// }
//
//
// let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
// self.renderPath = cgPath
//
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// currentRenderLayer.updatePath(cgPath)
// }
// }
//
// func draw(in context: CGContext, size: CGSize) {
// guard let path = self.renderPath else {
// return
// }
// context.saveGState()
//
// context.translateBy(x: self.translation.x, y: self.translation.y)
//
// context.setShouldAntialias(true)
//
// context.setBlendMode(.normal)
//
// context.addPath(path)
// context.setFillColor(UIColor.white.cgColor)
// context.setStrokeColor(UIColor.white.cgColor)
// context.setLineWidth(self.renderStrokeWidth * 0.5)
// context.setShadow(offset: .zero, blur: self.renderShadowRadius * 3.0, color: self.color.toCGColor())
// context.drawPath(using: .fillStroke)
//
// context.addPath(path)
// context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor)
// context.setLineCap(.round)
// context.setLineWidth(self.renderStrokeWidth)
// context.setStrokeColor(UIColor.white.mixedWith(self.color.toUIColor(), alpha: 0.25).cgColor)
// context.strokePath()
//
// context.addPath(path)
// context.setFillColor(UIColor.white.cgColor)
//
// context.fillPath()
//
// context.restoreGState()
// }
//}
//final class PenTool: DrawingElement {
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
// var lineWidth: CGFloat = 0.0
//
// let fillLayer = SimpleShapeLayer()
//
// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat) {
// self.contentsScale = 1.0
//
// let minLineWidth = max(1.0, min(size.width, size.height) * 0.003)
// let maxLineWidth = max(10.0, min(size.width, size.height) * 0.055)
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
//
// self.lineWidth = lineWidth
//
// let bounds = CGRect(origin: .zero, size: size)
// self.frame = bounds
//
// self.fillLayer.frame = bounds
// self.fillLayer.contentsScale = 1.0
// self.fillLayer.fillColor = color.toCGColor()
// self.fillLayer.strokeColor = color.toCGColor()
// self.fillLayer.lineWidth = 1.0
//
// self.addSublayer(self.fillLayer)
// }
//
// func updatePath(_ path: CGPath) {
// self.fillLayer.path = path
// }
// }
//
// let uuid = UUID()
//
// let drawingSize: CGSize
// let color: DrawingColor
// let lineWidth: CGFloat
// let arrow: Bool
//
// var polyline: Polyline?
//// var path = BezierPath()
// let renderArrowLength: CGFloat
// let renderArrowLineWidth: CGFloat
// var renderLineWidth: CGFloat = 0.0
//
// var translation = CGPoint()
//
// private var currentRenderLayer: DrawingRenderLayer?
//
// var bounds: CGRect {
// return .zero
// }
//
// var points: [CGPoint] {
// guard let polyline = self.polyline else {
// return []
// }
// var points: [CGPoint] = []
// for point in polyline.points {
// points.append(point.location)
// }
// return points
// }
//
// func containsPoint(_ point: CGPoint) -> Bool {
// return false
// }
//
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
// if let polyline = self.polyline {
// let pathBoundingBox = path.bounds
// if self.bounds.intersects(pathBoundingBox) {
// for point in polyline.points {
// if path.contains(point.location) {
// return true
// }
// }
// }
// }
// return false
// }
//
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
// self.drawingSize = drawingSize
// self.color = color
// self.lineWidth = lineWidth
// self.arrow = arrow
//
// self.renderArrowLength = min(drawingSize.width, drawingSize.height) * 0.04
// self.renderArrowLineWidth = min(drawingSize.width, drawingSize.height) * 0.01
// }
//
// func setupRenderLayer() -> DrawingRenderLayer? {
// let layer = RenderLayer()
// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.lineWidth)
// self.currentRenderLayer = layer
// return layer
// }
//
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
// guard case let .polyline(polyline) = path else {
// return
// }
//
// self.polyline = polyline
//
// struct LineSegment {
// var firstPoint: CGPoint
// var secondPoint: CGPoint
// }
//
// func lineSegmentPerpendicularTo(_ pp: LineSegment, fraction: CGFloat) -> LineSegment {
// let x0: CGFloat = pp.firstPoint.x
// let y0: CGFloat = pp.firstPoint.y
//
// let x1: CGFloat = pp.secondPoint.x
// let y1: CGFloat = pp.secondPoint.y
//
// let dx = x1 - x0
// let dy = y1 - y0
//
// var xa: CGFloat
// var ya: CGFloat
// var xb: CGFloat
// var yb: CGFloat
//
// xa = x1 + fraction * 0.5 * dy
// ya = y1 - fraction * 0.5 * dx
// xb = x1 - fraction * 0.5 * dy
// yb = y1 + fraction * 0.5 * dx
//
// return LineSegment(firstPoint: CGPoint(x: xa, y: ya), secondPoint: CGPoint(x: xb, y: yb))
// }
//
//
// func len_sq(_ p1: CGPoint, p2: CGPoint) -> CGFloat {
// let dx: CGFloat = p2.x - p1.x
// let dy: CGFloat = p2.y - p1.y
// return dx * dx + dy * dy
// }
//
// func clamp<T: Comparable>(_ value: T, min: T, max: T) -> T {
// if (value < min) {
// return min
// }
// if (value > max) {
// return max
// }
// return value
// }
//
// var ls: [LineSegment] = []
// var isFirst = true
//
// let combinedPath = UIBezierPath()
// var segmentPath = UIBezierPath()
//
// var pts: [CGPoint] = []
// var ctr: Int = 0
//
// let ff: CGFloat = 0.2
// let lower: CGFloat = 0.01
// let upper: CGFloat = 1.0
//
// var lastUsedIndex = 0
// var index = 0
// for point in polyline.points {
// pts.insert(point.location, at: ctr)
// ctr += 1
//
// if ctr == 5 {
// pts[3] = CGPoint(x: (pts[2].x + pts[4].x) / 2.0, y: (pts[2].y + pts[4].y) / 2.0)
//
// if isFirst {
// isFirst = false
// let segment = LineSegment(firstPoint: pts[0], secondPoint: pts[0])
// ls.append(segment)
// segmentPath.move(to: pts[0])
// }
//
// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper)
// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: pts[2]), min: lower, max: upper)
// let frac3: CGFloat = ff/clamp(len_sq(pts[2], p2: pts[3]), min: lower, max: upper)
//
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1)
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: pts[2]), fraction: frac2), at: 2)
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[2], secondPoint: pts[3]), fraction: frac3), at: 3)
//
// segmentPath.move(to: ls[0].firstPoint)
// segmentPath.addCurve(to: ls[3].firstPoint, controlPoint1: ls[1].firstPoint, controlPoint2: ls[2].firstPoint)
// segmentPath.addLine(to: ls[3].secondPoint)
// segmentPath.addCurve(to: ls[0].secondPoint, controlPoint1: ls[2].secondPoint, controlPoint2: ls[1].secondPoint)
// segmentPath.close()
// combinedPath.append(segmentPath)
//
// let last = ls[3]
// ls.removeAll()
// ls.append(last)
//
// pts[0] = pts[3]
// pts[1] = pts[4]
// ctr = 2
//
// combinedPath.append(segmentPath)
// segmentPath = UIBezierPath()
//
// lastUsedIndex = index
// }
// index += 1
// }
//
// var lastPosition = polyline.points.last?.location ?? CGPoint()
// if let lastPoint = polyline.points.last, ls.count > 0 {
// if lastUsedIndex < polyline.points.count - 1 {
// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper)
// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: lastPoint.location), min: lower, max: upper)
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1)
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: lastPoint.location), fraction: frac2), at: 2)
//
// segmentPath.move(to: ls[0].firstPoint)
// segmentPath.addQuadCurve(to: ls[2].firstPoint, controlPoint: ls[1].firstPoint)
// segmentPath.addLine(to: ls[2].secondPoint)
// segmentPath.addQuadCurve(to: ls[0].secondPoint, controlPoint: ls[1].secondPoint)
// segmentPath.addLine(to: ls[0].secondPoint)
// segmentPath.close()
// combinedPath.append(segmentPath)
//
// let diameter = ls[2].firstPoint.distance(to: ls[2].secondPoint)
// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: lastPoint.x - diameter * 0.5, y: lastPoint.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter))))
//
// lastPosition = lastPoint.location
// } else {
// let diameter = ls[0].firstPoint.distance(to: ls[0].secondPoint)
// let center = ls[0].firstPoint.point(to: ls[0].secondPoint, t: 0.5)
// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: center.x - diameter * 0.5, y: center.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter))))
//
// lastPosition = center
// }
// }
//
// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
// let lastPoint = lastPosition
// var secondPoint = polyline.points[polyline.points.count - 2]
// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
// secondPoint = polyline.points[polyline.points.count - 3]
// }
// let angle = lastPoint.angle(to: secondPoint.location)
// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
//
// let arrowPath = UIBezierPath()
// arrowPath.move(to: point2)
// arrowPath.addLine(to: lastPoint)
// arrowPath.addLine(to: point1)
// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
//
// combinedPath.usesEvenOddFillRule = false
// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
// }
//
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// currentRenderLayer.updatePath(combinedPath.cgPath)
// }
// }
//
// func draw(in context: CGContext, size: CGSize) {
// let renderLayer: DrawingRenderLayer?
// if let currentRenderLayer = self.currentRenderLayer {
// renderLayer = currentRenderLayer
// } else {
// renderLayer = self.setupRenderLayer()
//// (renderLayer as? RenderLayer)?.updatePath(self.path.path.cgPath)
// }
// renderLayer?.render(in: context)
// }
//}
final class MarkerTool: DrawingElement {
let uuid = UUID()
@ -483,22 +21,25 @@ final class MarkerTool: DrawingElement {
var translation = CGPoint()
var bounds: CGRect {
return self.renderPath.bounds
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
}
var points: [Polyline.Point] = []
var _points: [Polyline.Point] = []
var points: [Polyline.Point] {
return self._points.map { $0.offsetBy(self.translation) }
}
weak var metalView: DrawingMetalView?
func containsPoint(_ point: CGPoint) -> Bool {
return self.renderPath.contains(point)
return self.renderPath.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y)))
}
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
let pathBoundingBox = path.bounds
if self.bounds.intersects(pathBoundingBox) {
for point in self.points {
if path.contains(point.location) {
for point in self._points {
if path.contains(point.location.offsetBy(self.translation)) {
return true
}
}
@ -523,27 +64,43 @@ final class MarkerTool: DrawingElement {
return nil
}
private var hot = false
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
guard case let .location(point) = path else {
return
}
if self.points.isEmpty {
if self._points.isEmpty {
self.renderPath.move(to: point.location)
} else {
self.renderPath.addLine(to: point.location)
}
self.points.append(point)
self._points.append(point)
self.hot = true
self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth)
}
func draw(in context: CGContext, size: CGSize) {
guard !self.points.isEmpty else {
guard !self._points.isEmpty else {
return
}
context.saveGState()
context.translateBy(x: self.translation.x, y: self.translation.y)
let hot = self.hot
if hot {
self.hot = false
} else {
self.metalView?.setup(self._points.map { $0.location }, brush: .marker, color: self.color, size: self.renderLineWidth)
}
self.metalView?.drawInContext(context)
if !hot {
self.metalView?.clear()
}
context.restoreGState()
}
}
@ -789,10 +346,13 @@ final class PencilTool: DrawingElement {
var renderAngle: CGFloat = 0.0
var bounds: CGRect {
return self.renderPath.bounds
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
}
var points: [Polyline.Point] = []
var _points: [Polyline.Point] = []
var points: [Polyline.Point] {
return self._points.map { $0.offsetBy(self.translation) }
}
weak var metalView: DrawingMetalView?
@ -803,7 +363,7 @@ final class PencilTool: DrawingElement {
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
let pathBoundingBox = path.bounds
if self.bounds.intersects(pathBoundingBox) {
for point in self.points {
for point in self._points {
if path.contains(point.location.offsetBy(self.translation)) {
return true
}
@ -829,27 +389,43 @@ final class PencilTool: DrawingElement {
return nil
}
private var hot = false
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
guard case let .location(point) = path else {
return
}
if self.points.isEmpty {
if self._points.isEmpty {
self.renderPath.move(to: point.location)
} else {
self.renderPath.addLine(to: point.location)
}
self.points.append(point)
self._points.append(point)
self.hot = true
self.metalView?.updated(point, state: state, brush: .pencil, color: self.color, size: self.renderLineWidth)
}
func draw(in context: CGContext, size: CGSize) {
guard !self.points.isEmpty else {
guard !self._points.isEmpty else {
return
}
context.saveGState()
context.translateBy(x: self.translation.x, y: self.translation.y)
let hot = self.hot
if hot {
self.hot = false
} else {
self.metalView?.setup(self._points.map { $0.location }, brush: .pencil, color: self.color, size: self.renderLineWidth)
}
self.metalView?.drawInContext(context)
if !hot {
self.metalView?.clear()
}
context.restoreGState()
}
}

View file

@ -3,7 +3,15 @@ import UIKit
import QuartzCore
import simd
struct DrawingColor: Equatable {
struct DrawingColor: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case red
case green
case blue
case alpha
case position
}
public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
public var red: CGFloat
@ -48,6 +56,24 @@ struct DrawingColor: Equatable {
public init(rgb: UInt32) {
self.init(color: UIColor(rgb: rgb))
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.red = try container.decode(CGFloat.self, forKey: .red)
self.green = try container.decode(CGFloat.self, forKey: .green)
self.blue = try container.decode(CGFloat.self, forKey: .blue)
self.alpha = try container.decode(CGFloat.self, forKey: .alpha)
self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.red, forKey: .red)
try container.encode(self.green, forKey: .green)
try container.encode(self.blue, forKey: .blue)
try container.encode(self.alpha, forKey: .alpha)
try container.encodeIfPresent(self.position, forKey: .position)
}
func withUpdatedRed(_ red: CGFloat) -> DrawingColor {
return DrawingColor(
@ -561,156 +587,6 @@ private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2:
return []
}
class FPSCounter: NSObject {
/// Helper class that relays display link updates to the FPSCounter
///
/// This is necessary because CADisplayLink retains its target. Thus
/// if the FPSCounter class would be the target of the display link
/// it would create a retain cycle. The delegate has a weak reference
/// to its parent FPSCounter, thus preventing this.
///
internal class DisplayLinkProxy: NSObject {
/// A weak ref to the parent FPSCounter instance.
@objc weak var parentCounter: FPSCounter?
/// Notify the parent FPSCounter of a CADisplayLink update.
///
/// This method is automatically called by the CADisplayLink.
///
/// - Parameters:
/// - displayLink: The display link that updated
///
@objc func updateFromDisplayLink(_ displayLink: CADisplayLink) {
parentCounter?.updateFromDisplayLink(displayLink)
}
}
// MARK: - Initialization
private let displayLink: CADisplayLink
private let displayLinkProxy: DisplayLinkProxy
/// Create a new FPSCounter.
///
/// To start receiving FPS updates you need to start tracking with the
/// `startTracking(inRunLoop:mode:)` method.
///
public override init() {
self.displayLinkProxy = DisplayLinkProxy()
self.displayLink = CADisplayLink(
target: self.displayLinkProxy,
selector: #selector(DisplayLinkProxy.updateFromDisplayLink(_:))
)
super.init()
self.displayLinkProxy.parentCounter = self
}
deinit {
self.displayLink.invalidate()
}
// MARK: - Configuration
/// The delegate that should receive FPS updates.
public weak var delegate: FPSCounterDelegate?
/// Delay between FPS updates. Longer delays mean more averaged FPS numbers.
@objc public var notificationDelay: TimeInterval = 1.0
// MARK: - Tracking
private var runloop: RunLoop?
private var mode: RunLoop.Mode?
/// Start tracking FPS updates.
///
/// You can specify wich runloop to use for tracking, as well as the runloop modes.
/// Usually you'll want the main runloop (default), and either the common run loop modes
/// (default), or the tracking mode (`RunLoop.Mode.tracking`).
///
/// When the counter is already tracking, it's stopped first.
///
/// - Parameters:
/// - runloop: The runloop to start tracking in
/// - mode: The mode(s) to track in the runloop
///
@objc public func startTracking(inRunLoop runloop: RunLoop = .main, mode: RunLoop.Mode = .common) {
self.stopTracking()
self.runloop = runloop
self.mode = mode
self.displayLink.add(to: runloop, forMode: mode)
}
/// Stop tracking FPS updates.
///
/// This method does nothing if the counter is not currently tracking.
///
@objc public func stopTracking() {
guard let runloop = self.runloop, let mode = self.mode else { return }
self.displayLink.remove(from: runloop, forMode: mode)
self.runloop = nil
self.mode = nil
}
// MARK: - Handling Frame Updates
private var lastNotificationTime: CFAbsoluteTime = 0.0
private var numberOfFrames = 0
private func updateFromDisplayLink(_ displayLink: CADisplayLink) {
if self.lastNotificationTime == 0.0 {
self.lastNotificationTime = CFAbsoluteTimeGetCurrent()
return
}
self.numberOfFrames += 1
let currentTime = CFAbsoluteTimeGetCurrent()
let elapsedTime = currentTime - self.lastNotificationTime
if elapsedTime >= self.notificationDelay {
self.notifyUpdateForElapsedTime(elapsedTime)
self.lastNotificationTime = 0.0
self.numberOfFrames = 0
}
}
private func notifyUpdateForElapsedTime(_ elapsedTime: CFAbsoluteTime) {
let fps = Int(round(Double(self.numberOfFrames) / elapsedTime))
self.delegate?.fpsCounter(self, didUpdateFramesPerSecond: fps)
}
}
/// The delegate protocol for the FPSCounter class.
///
/// Implement this protocol if you want to receive updates from a `FPSCounter`.
///
protocol FPSCounterDelegate: NSObjectProtocol {
/// Called in regular intervals while the counter is tracking FPS.
///
/// - Parameters:
/// - counter: The FPSCounter that sent the update
/// - fps: The current FPS of the application
///
func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int)
}
class BezierPath {
struct Element {
enum ElementType {

View file

@ -3,8 +3,20 @@ import UIKit
import Display
import AccountContext
final class DrawingVectorEntity: DrawingEntity {
public enum VectorType {
final class DrawingVectorEntity: DrawingEntity, Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case type
case color
case lineWidth
case drawingSize
case referenceDrawingSize
case start
case mid
case end
}
public enum VectorType: Codable {
case line
case oneSidedArrow
case twoSidedArrow
@ -34,6 +46,10 @@ final class DrawingVectorEntity: DrawingEntity {
}
}
var center: CGPoint {
return self.start
}
init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {
self.uuid = UUID()
self.isAnimated = false
@ -49,8 +65,34 @@ final class DrawingVectorEntity: DrawingEntity {
self.end = CGPoint()
}
var center: CGPoint {
return self.start
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.isAnimated = false
self.type = try container.decode(VectorType.self, forKey: .type)
self.color = try container.decode(DrawingColor.self, forKey: .color)
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.start = try container.decode(CGPoint.self, forKey: .start)
let mid = try container.decode(CGPoint.self, forKey: .mid)
self.mid = (mid.x, mid.y)
self.end = try container.decode(CGPoint.self, forKey: .end)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.type, forKey: .type)
try container.encode(self.color, forKey: .color)
try container.encode(self.lineWidth, forKey: .lineWidth)
try container.encode(self.drawingSize, forKey: .drawingSize)
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.start, forKey: .start)
try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid)
try container.encode(self.end, forKey: .end)
}
func duplicate() -> DrawingEntity {

View file

@ -90,7 +90,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
private var currentDrawingView: UIView
private var currentDrawingLayer: DrawingRenderLayer?
private var selectionImage: UIImage?
private var pannedSelectionView: UIView
var lassoView: DrawingLassoView
@ -155,6 +154,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
self.lassoView.isHidden = true
self.metalView = DrawingMetalView(size: size)!
self.metalView.isHidden = true
self.brushSizePreviewLayer = SimpleShapeLayer()
self.brushSizePreviewLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))
@ -258,6 +258,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
guard let newElement = strongSelf.prepareNewElement() else {
return
}
if newElement is MarkerTool || newElement is PencilTool {
self?.metalView.isHidden = false
}
if let renderLayer = newElement.setupRenderLayer() {
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
strongSelf.currentDrawingLayer = renderLayer
@ -366,7 +371,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
if let strongSelf = self {
strongSelf.skipDrawing = Set(elements)
strongSelf.commit(reset: true)
strongSelf.updateSelectionImage()
strongSelf.updateSelectionContent()
}
}
@ -386,12 +391,12 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
}
}
strongSelf.skipDrawing = Set()
strongSelf.commit(reset: true)
strongSelf.selectionImage = nil
strongSelf.pannedSelectionView.layer.contents = nil
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
strongSelf.lassoView.translate(offset)
strongSelf.commit(reset: true, completion: {
strongSelf.pannedSelectionView.layer.contents = nil
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
strongSelf.lassoView.translate(offset)
})
}
}
}
@ -468,46 +473,66 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
}
}
private let queue = Queue()
private var skipDrawing = Set<UUID>()
private func commit(reset: Bool = false) {
private func commit(reset: Bool = false, interactive: Bool = false, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
let currentImage = self.drawingImage
self.drawingImage = self.renderer.image { context in
if !reset {
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
if let image = currentImage {
image.draw(at: .zero)
}
if let uncommitedElement = self.uncommitedElement {
uncommitedElement.draw(in: context.cgContext, size: self.imageSize)
}
} else {
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
for element in self.elements {
if !self.skipDrawing.contains(element.uuid) {
element.draw(in: context.cgContext, size: self.imageSize)
let uncommitedElement = self.uncommitedElement
let imageSize = self.imageSize
let skipDrawing = self.skipDrawing
let action = {
let updatedImage = self.renderer.image { context in
if !reset {
context.cgContext.clear(CGRect(origin: .zero, size: imageSize))
if let image = currentImage {
image.draw(at: .zero)
}
if let uncommitedElement = uncommitedElement {
uncommitedElement.draw(in: context.cgContext, size: imageSize)
}
} else {
context.cgContext.clear(CGRect(origin: .zero, size: imageSize))
for element in self.elements {
if !skipDrawing.contains(element.uuid) {
element.draw(in: context.cgContext, size: imageSize)
}
}
}
}
Queue.mainQueue().async {
self.drawingImage = updatedImage
self.layer.contents = updatedImage.cgImage
if let currentDrawingLayer = self.currentDrawingLayer {
self.currentDrawingLayer = nil
currentDrawingLayer.removeFromSuperlayer()
}
self.metalView.clear()
self.metalView.isHidden = true
completion()
}
}
self.layer.contents = self.drawingImage?.cgImage
if let currentDrawingLayer = self.currentDrawingLayer {
self.currentDrawingLayer = nil
currentDrawingLayer.removeFromSuperlayer()
if synchronous {
action()
} else {
self.queue.async {
action()
}
}
self.metalView.clear()
}
private func updateSelectionImage() {
self.selectionImage = self.renderer.image { context in
private func updateSelectionContent() {
let selectionImage = self.renderer.image { context in
for element in self.elements {
if self.skipDrawing.contains(element.uuid) {
element.draw(in: context.cgContext, size: self.imageSize)
}
}
}
self.pannedSelectionView.layer.contents = self.selectionImage?.cgImage
self.pannedSelectionView.layer.contents = selectionImage.cgImage
}
fileprivate func cancelDrawing() {
@ -520,15 +545,24 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
}
fileprivate func finishDrawing() {
self.commit()
self.redoElements.removeAll()
if let uncommitedElement = self.uncommitedElement {
self.elements.append(uncommitedElement)
self.uncommitedElement = nil
let complete: (Bool) -> Void = { synchronous in
self.commit(interactive: true, synchronous: synchronous)
self.redoElements.removeAll()
if let uncommitedElement = self.uncommitedElement {
self.elements.append(uncommitedElement)
self.uncommitedElement = nil
}
self.updateInternalState()
}
if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.arrow {
uncommitedElement.finishArrow({
complete(true)
})
} else {
complete(false)
}
self.updateInternalState()
}
weak var entitiesView: DrawingEntitiesView?

View file

@ -91,6 +91,32 @@ final class PenTool: DrawingElement {
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
}
func animateArrowPaths(leftArrowPath: UIBezierPath, rightArrowPath: UIBezierPath, lineWidth: CGFloat, completion: @escaping () -> Void) {
let leftArrowShape = CAShapeLayer()
leftArrowShape.path = leftArrowPath.cgPath
leftArrowShape.lineWidth = lineWidth
leftArrowShape.strokeColor = self.color?.cgColor
leftArrowShape.lineCap = .round
leftArrowShape.frame = self.bounds
self.addSublayer(leftArrowShape)
let rightArrowShape = CAShapeLayer()
rightArrowShape.path = rightArrowPath.cgPath
rightArrowShape.lineWidth = lineWidth
rightArrowShape.strokeColor = self.color?.cgColor
rightArrowShape.lineCap = .round
rightArrowShape.frame = self.bounds
self.addSublayer(rightArrowShape)
leftArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
rightArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, completion: { [weak leftArrowShape, weak rightArrowShape] _ in
completion()
leftArrowShape?.removeFromSuperlayer()
rightArrowShape?.removeFromSuperlayer()
})
}
override func draw(in ctx: CGContext) {
guard let color = self.color else {
return
@ -122,10 +148,17 @@ final class PenTool: DrawingElement {
var didSetupArrow = false
let renderLineWidth: CGFloat
let renderArrowLength: CGFloat
let renderArrowLineWidth: CGFloat
var bezierPaths: [UIBezierPath] = []
var tempBezierPath: UIBezierPath?
var arrowLeftPath: UIBezierPath?
var arrowLeftPoint: CGPoint?
var arrowRightPath: UIBezierPath?
var arrowRightPoint: CGPoint?
var translation = CGPoint()
private var currentRenderLayer: DrawingRenderLayer?
@ -138,8 +171,16 @@ final class PenTool: DrawingElement {
var points: [Polyline.Point] {
var points: [Polyline.Point] = []
var lastPoint: Polyline.Point?
for point in self._points {
points.append(point.offsetBy(self.translation))
lastPoint = point
}
if let arrowLeftPoint, let lastPoint {
points.append(lastPoint.withLocation(arrowLeftPoint.offsetBy(self.translation)))
}
if let arrowRightPoint, let lastPoint {
points.append(lastPoint.withLocation(arrowRightPoint.offsetBy(self.translation)))
}
return points
}
@ -148,9 +189,15 @@ final class PenTool: DrawingElement {
private var nextPointIndex: Int = 0
private var drawPoints = [PointWeighted](repeating: PointWeighted.zero, count: 4)
private var arrowParams: (CGPoint, CGFloat)?
func containsPoint(_ point: CGPoint) -> Bool {
for path in self.bezierPaths {
if path.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) {
return true
}
}
return false
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
}
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
@ -178,12 +225,20 @@ final class PenTool: DrawingElement {
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
self.renderLineWidth = lineWidth
self.renderArrowLength = lineWidth * 7.0
self.renderArrowLineWidth = lineWidth * 2.0
// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth)
// if arrow {
// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
// }
self.path = Polyline(points: [])
}
func finishArrow(_ completion: @escaping () -> Void) {
if let arrowLeftPath, let arrowRightPath {
(self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: {
completion()
})
} else {
completion()
}
}
func setupRenderLayer() -> DrawingRenderLayer? {
@ -211,6 +266,7 @@ final class PenTool: DrawingElement {
}
self._points.append(point)
self.path?.points.append(point)
switch state {
case .began:
@ -222,6 +278,41 @@ final class PenTool: DrawingElement {
}
case .ended:
self.updateWithLocation(point.location, ended: true)
if self.arrow {
let points = self.path?.points ?? []
var direction: CGFloat?
let p2 = points[points.count - 1].location
for i in 1 ..< min(points.count - 2, 12) {
let p1 = points[points.count - 1 - i].location
if p1.distance(to: p2) > renderArrowLength * 0.5 {
direction = p2.angle(to: p1)
break
}
}
if let point = points.last?.location, let direction {
self.arrowParams = (point, direction)
let arrowLeftPath = UIBezierPath()
arrowLeftPath.move(to: point)
let leftPoint = point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)
arrowLeftPath.addLine(to: leftPoint)
let arrowRightPath = UIBezierPath()
arrowRightPath.move(to: point)
let rightPoint = point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)
arrowRightPath.addLine(to: rightPoint)
self.arrowLeftPath = arrowLeftPath
self.arrowLeftPoint = leftPoint
self.arrowRightPath = arrowRightPath
self.arrowRightPoint = rightPoint
}
}
case .cancelled:
break
}
@ -229,41 +320,6 @@ final class PenTool: DrawingElement {
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
currentRenderLayer.draw(paths: self.bezierPaths, tempPath: self.tempBezierPath, color: self.color.toUIColor(), rect: CGRect(origin: .zero, size: self.drawingSize))
}
// self.path = line
//
// let rect = self.renderLine.draw(at: point)
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// currentRenderLayer.draw(line: self.renderLine, rect: rect)
// }
// self.path = bezierPath
// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
// let lastPoint = lastPosition
// var secondPoint = polyline.points[polyline.points.count - 2]
// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
// secondPoint = polyline.points[polyline.points.count - 3]
// }
// let angle = lastPoint.angle(to: secondPoint.location)
// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
//
// let arrowPath = UIBezierPath()
// arrowPath.move(to: point2)
// arrowPath.addLine(to: lastPoint)
// arrowPath.addLine(to: point1)
// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
//
// combinedPath.usesEvenOddFillRule = false
// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
// }
// let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
// self.renderPath = cgPath
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// currentRenderLayer.updatePath(cgPath)
// }
}
private let minDistance: CGFloat = 2
@ -289,7 +345,6 @@ final class PenTool: DrawingElement {
self.tempBezierPath = newBezier
}
private var isFirstPoint: Bool {
return nextPointIndex == 0
}
@ -401,6 +456,18 @@ final class PenTool: DrawingElement {
context.fillPath()
}
if let arrowLeftPath, let arrowRightPath {
context.setStrokeColor(self.color.toCGColor())
context.setLineWidth(self.renderArrowLineWidth)
context.setLineCap(.round)
context.addPath(arrowLeftPath.cgPath)
context.strokePath()
context.addPath(arrowRightPath.cgPath)
context.strokePath()
}
context.restoreGState()
}
}
@ -503,431 +570,3 @@ extension UIBezierPath {
return (line.normalLine(from: pointA), line.normalLine(from: pointB))
}
}
//
//final class PenTool: DrawingElement {
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
// func setup(size: CGSize) {
// self.shouldRasterize = true
// self.contentsScale = 1.0
//
// let bounds = CGRect(origin: .zero, size: size)
// self.frame = bounds
// }
//
// private var line: StrokeLine?
// fileprivate func draw(line: StrokeLine, rect: CGRect) {
// self.line = line
// self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
// }
//
// override func draw(in ctx: CGContext) {
// self.line?.drawInContext(ctx)
// }
// }
//
// let uuid = UUID()
//
// let drawingSize: CGSize
// let color: DrawingColor
// let lineWidth: CGFloat
// let arrow: Bool
//
// var path: Polyline?
// var boundingBox: CGRect?
//
// private var renderLine: StrokeLine
// var didSetupArrow = false
// private var renderLineArrow1: StrokeLine?
// private var renderLineArrow2: StrokeLine?
// let renderLineWidth: CGFloat
//
// var translation = CGPoint()
//
// private var currentRenderLayer: DrawingRenderLayer?
//
// var bounds: CGRect {
// return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
// }
//
// var points: [Polyline.Point] {
// guard let linePath = self.path else {
// return []
// }
// var points: [Polyline.Point] = []
// for point in linePath.points {
// points.append(point.offsetBy(self.translation))
// }
// return points
// }
//
// func containsPoint(_ point: CGPoint) -> Bool {
// return false
// // return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
// }
//
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
// if let linePath = self.path {
// let pathBoundingBox = path.bounds
// if self.bounds.intersects(pathBoundingBox) {
// for point in linePath.points {
// if path.contains(point.location.offsetBy(self.translation)) {
// return true
// }
// }
// }
// }
// return false
// }
//
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
// self.drawingSize = drawingSize
// self.color = color
// self.lineWidth = lineWidth
// self.arrow = arrow
//
// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003)
// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09)
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
//
// self.renderLineWidth = lineWidth
//
// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth)
// if arrow {
// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
// }
// }
//
// func setupRenderLayer() -> DrawingRenderLayer? {
// let layer = RenderLayer()
// layer.setup(size: self.drawingSize)
// self.currentRenderLayer = layer
// return layer
// }
//
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
// guard case let .polyline(line) = path, let point = line.points.last else {
// return
// }
// self.path = line
//
// let rect = self.renderLine.draw(at: point)
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// currentRenderLayer.draw(line: self.renderLine, rect: rect)
// }
// // self.path = bezierPath
//
// // if self.arrow && polyline.isComplete, polyline.points.count > 2 {
// // let lastPoint = lastPosition
// // var secondPoint = polyline.points[polyline.points.count - 2]
// // if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
// // secondPoint = polyline.points[polyline.points.count - 3]
// // }
// // let angle = lastPoint.angle(to: secondPoint.location)
// // let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
// // let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
// //
// // let arrowPath = UIBezierPath()
// // arrowPath.move(to: point2)
// // arrowPath.addLine(to: lastPoint)
// // arrowPath.addLine(to: point1)
// // let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
// //
// // combinedPath.usesEvenOddFillRule = false
// // combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
// // }
//
// // let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
// // self.renderPath = cgPath
//
// // if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
// // currentRenderLayer.updatePath(cgPath)
// // }
// }
//
// func draw(in context: CGContext, size: CGSize) {
// context.saveGState()
//
// context.translateBy(x: self.translation.x, y: self.translation.y)
//
// context.setShouldAntialias(true)
//
// if self.arrow, let path = self.path, let lastPoint = path.points.last {
// var lastPointWithVelocity: Polyline.Point?
// for point in path.points.reversed() {
// if point.velocity > 0.0 {
// lastPointWithVelocity = point
// break
// }
// }
// if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity {
// let w = self.renderLineWidth
// var dist: CGFloat = 18.0 * sqrt(w)
// let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w))
//
// let suffix = path.points.suffix(100).reversed()
//
// var p0 = suffix.first!
//
// var p2 = suffix.last!
// var d: CGFloat = 0
// for p in suffix {
// d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
// if d >= dist {
// p2 = p
// break
// }
// p0 = p
// }
//
// p0 = suffix.first!
// dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y))
//
// var i = 0
// for spread in [-spread, spread] {
// var points: [CGPoint] = []
// points.append(lastPoint.location)
//
// p0 = suffix.first!
// var prev = p0.location
// d = 0
// for p in suffix {
// let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
// d += d1
// if d >= dist {
// break
// }
// let d2 = d1 / cos(spread)
// let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x)
// let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread))
//
// points.append(
// cur
// )
//
// p0 = p
// prev = cur
// }
//
// for point in points {
// if i == 0 {
// let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
// } else if i == 1 {
// let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
// }
// }
// i += 1
// }
// self.didSetupArrow = true
// }
// self.renderLineArrow1?.drawInContext(context)
// self.renderLineArrow2?.drawInContext(context)
// }
//
// self.renderLine.drawInContext(context)
//
// context.restoreGState()
// }
//}
//
//private class StrokeLine {
// struct Segment {
// let a: CGPoint
// let b: CGPoint
// let c: CGPoint
// let d: CGPoint
// let abWidth: CGFloat
// let cdWidth: CGFloat
// }
//
// struct Point {
// let position: CGPoint
// let width: CGFloat
//
// init(position: CGPoint, width: CGFloat) {
// self.position = position
// self.width = width
// }
// }
//
// private(set) var points: [Point] = []
// private var smoothPoints: [Point] = []
// private var segments: [Segment] = []
// private var lastWidth: CGFloat?
//
// private let minLineWidth: CGFloat
// let lineWidth: CGFloat
//
// let color: UIColor
//
// init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) {
// self.color = color
// self.minLineWidth = minLineWidth
// self.lineWidth = lineWidth
// }
//
// func draw(at point: Polyline.Point) -> CGRect {
// let width = extractLineWidth(from: point.velocity)
// self.lastWidth = width
//
// let point = Point(position: point.location, width: width)
// return appendPoint(point)
// }
//
// func drawInContext(_ context: CGContext) {
// self.drawSegments(self.segments, inContext: context)
// }
//
// func extractLineWidth(from velocity: CGFloat) -> CGFloat {
// let minValue = self.minLineWidth
// let maxValue = self.lineWidth
//
// var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue))
// if let lastWidth = self.lastWidth {
// size = size * 0.2 + lastWidth * 0.8
// }
// return size
// }
//
// func appendPoint(_ point: Point) -> CGRect {
// self.points.append(point)
//
// guard self.points.count > 2 else { return .null }
//
// let index = self.points.count - 1
// let point0 = self.points[index - 2]
// let point1 = self.points[index - 1]
// let point2 = self.points[index]
//
// let newSmoothPoints = smoothPoints(
// fromPoint0: point0,
// point1: point1,
// point2: point2
// )
//
// let lastOldSmoothPoint = smoothPoints.last
// smoothPoints.append(contentsOf: newSmoothPoints)
//
// guard smoothPoints.count > 1 else { return .null }
//
// let newSegments: ([Segment], CGRect) = {
// guard let lastOldSmoothPoint = lastOldSmoothPoint else {
// return segments(fromSmoothPoints: newSmoothPoints)
// }
// return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints)
// }()
// segments.append(contentsOf: newSegments.0)
//
// return newSegments.1
// }
//
// func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] {
// var smoothPoints = [Point]()
//
// let midPoint1 = (point0.position + point1.position) * 0.5
// let midPoint2 = (point1.position + point2.position) * 0.5
//
// let segmentDistance = 2.0
// let distance = midPoint1.distance(to: midPoint2)
// let numberOfSegments = min(128, max(floor(distance/segmentDistance), 32))
//
// let step = 1.0 / numberOfSegments
// for t in stride(from: 0, to: 1, by: step) {
// let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t
// let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5)
// let point = Point(position: position, width: size)
// smoothPoints.append(point)
// }
//
// let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5)
// smoothPoints.append(finalPoint)
//
// return smoothPoints
// }
//
// func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) {
// var segments = [Segment]()
// var updateRect = CGRect.null
// for i in 1 ..< smoothPoints.count {
// let previousPoint = smoothPoints[i - 1].position
// let previousWidth = smoothPoints[i - 1].width
// let currentPoint = smoothPoints[i].position
// let currentWidth = smoothPoints[i].width
// let direction = currentPoint - previousPoint
//
// guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else {
// continue
// }
//
// var perpendicular = CGPoint(x: -direction.y, y: direction.x)
// let length = perpendicular.length
// if length > 0.0 {
// perpendicular = perpendicular / length
// }
//
// let a = previousPoint + perpendicular * previousWidth / 2
// let b = previousPoint - perpendicular * previousWidth / 2
// let c = currentPoint + perpendicular * currentWidth / 2
// let d = currentPoint - perpendicular * currentWidth / 2
//
// let ab: CGPoint = {
// let center = (b + a)/2
// let radius = center - b
// return .init(x: center.x - radius.y, y: center.y + radius.x)
// }()
// let cd: CGPoint = {
// let center = (c + d)/2
// let radius = center - c
// return .init(x: center.x + radius.y, y: center.y - radius.x)
// }()
//
// let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x)
// let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y)
// let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
// let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
//
// updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY))
//
// segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth))
// }
// return (segments, updateRect)
// }
//
// func drawSegments(_ segments: [Segment], inContext context: CGContext) {
// for segment in segments {
// context.beginPath()
//
// context.setStrokeColor(color.cgColor)
// context.setFillColor(color.cgColor)
//
// context.move(to: segment.b)
//
// let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x)
// context.addArc(
// center: (segment.a + segment.b)/2,
// radius: segment.abWidth/2,
// startAngle: abStartAngle,
// endAngle: abStartAngle + .pi,
// clockwise: true
// )
// context.addLine(to: segment.c)
//
// let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x)
// context.addArc(
// center: (segment.c + segment.d)/2,
// radius: segment.cdWidth/2,
// startAngle: cdStartAngle,
// endAngle: cdStartAngle + .pi,
// clockwise: true
// )
// context.closePath()
//
// context.fillPath()
// context.strokePath()
// }
// }
//}
//

View file

@ -11,29 +11,49 @@ import ComponentFlow
import ViewControllerComponent
import EntityKeyboard
import PagerComponent
import FeaturedStickersScreen
struct InputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent?
var masks: EmojiPagerContentComponent?
init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent?,
masks: EmojiPagerContentComponent?
) {
self.emoji = emoji
self.stickers = stickers
self.masks = masks
}
}
private final class StickerSelectionComponent: Component {
public typealias EnvironmentType = Empty
typealias EnvironmentType = Empty
public let theme: PresentationTheme
public let strings: PresentationStrings
public let deviceMetrics: DeviceMetrics
public let stickerContent: EmojiPagerContentComponent
public let backgroundColor: UIColor
public let separatorColor: UIColor
let theme: PresentationTheme
let strings: PresentationStrings
let deviceMetrics: DeviceMetrics
let bottomInset: CGFloat
let content: InputData
let backgroundColor: UIColor
let separatorColor: UIColor
public init(
init(
theme: PresentationTheme,
strings: PresentationStrings,
deviceMetrics: DeviceMetrics,
stickerContent: EmojiPagerContentComponent,
bottomInset: CGFloat,
content: InputData,
backgroundColor: UIColor,
separatorColor: UIColor
) {
self.theme = theme
self.strings = strings
self.deviceMetrics = deviceMetrics
self.stickerContent = stickerContent
self.bottomInset = bottomInset
self.content = content
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
}
@ -48,7 +68,10 @@ private final class StickerSelectionComponent: Component {
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.stickerContent != rhs.stickerContent {
if lhs.bottomInset != rhs.bottomInset {
return false
}
if lhs.content != rhs.content {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
@ -96,7 +119,7 @@ private final class StickerSelectionComponent: Component {
self.backgroundColor = component.backgroundColor
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
self.panelSeparatorView.backgroundColor = component.separatorColor
self.panelSeparatorView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
self.component = component
self.state = state
@ -109,15 +132,18 @@ private final class StickerSelectionComponent: Component {
theme: component.theme,
strings: component.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0),
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: component.bottomInset, right: 0.0),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: nil,
stickerContent: component.stickerContent,
emojiContent: component.content.emoji,
stickerContent: component.content.stickers,
maskContent: component.content.masks,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: false,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: true,
topPanelExtensionUpdated: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { _, _ in },
@ -128,7 +154,7 @@ private final class StickerSelectionComponent: Component {
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
displayBottomPanel: true,
isExpanded: true
)),
environment: {},
@ -182,8 +208,8 @@ public class StickerPickerScreen: ViewController {
let scrollView: UIScrollView
let hostView: ComponentHostView<Empty>
private var stickerContent: EmojiPagerContentComponent?
private let stickerContentDisposable = MetaDisposable()
private var content: InputData?
private let contentDisposable = MetaDisposable()
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
private(set) var isExpanded = false
@ -224,31 +250,63 @@ public class StickerPickerScreen: ViewController {
self.containerView.addSubview(self.scrollView)
self.scrollView.addSubview(self.hostView)
self.stickerContentDisposable.set((
EmojiPagerContentComponent.stickerInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId
)
|> deliverOnMainQueue).start(next: { [weak self] content in
if let strongSelf = self {
strongSelf.updateContent(content)
}
})
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
isStatusSelection: false,
isReactionSelection: false,
isEmojiSelection: true,
topReactionItems: [],
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
hasSearch: false
)
let stickerItems = EmojiPagerContentComponent.stickerInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId,
hasSearch: false,
hasTrending: true
)
let maskItems = EmojiPagerContentComponent.stickerInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
stickerOrderedItemListCollectionIds: [],
chatPeerId: context.account.peerId,
hasSearch: false,
hasTrending: false
)
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
maskItems
)
self.contentDisposable.set(signal.start(next: { [weak self] emoji, stickers, masks in
if let strongSelf = self {
strongSelf.updateContent(InputData(emoji: emoji, stickers: stickers, masks: masks))
}
}))
}
deinit {
self.stickerContentDisposable.dispose()
self.contentDisposable.dispose()
}
func updateContent(_ content: EmojiPagerContentComponent) {
self.stickerContent = content
content.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
func updateContent(_ content: InputData) {
self.content = content
content.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let strongSelf = self, let file = item.itemFile else {
return
@ -256,30 +314,229 @@ public class StickerPickerScreen: ViewController {
strongSelf.controller?.completion(file)
strongSelf.controller?.dismiss(animated: true)
},
deleteBackwards: {},
openStickerSettings: {
// guard let controllerInteraction = controllerInteraction else {
// return
// }
// let controller = installedStickerPacksController(context: context, mode: .modal)
// controller.navigationPresentation = .modal
// controllerInteraction.navigationController()?.pushViewController(controller)
},
deleteBackwards: nil,
openStickerSettings: nil,
openFeatured: {
// guard let controllerInteraction = controllerInteraction else {
// return
// }
//
// controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
// context: context,
// highlightedPackId: nil,
// sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
// guard let controllerInteraction = controllerInteraction else {
// return false
// }
// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
// }
// ))
},
openSearch: {},
addGroupAction: { [weak self] groupId, isPremiumLocked in
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
return
}
let context = controller.context
if isPremiumLocked {
// let controller = PremiumIntroScreen(context: context, source: .stickers)
// controllerInteraction.navigationController()?.pushViewController(controller)
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.info.id == collectionId {
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(completed: {
})
break
}
}
})
},
clearGroup: { [weak self] groupId in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
if groupId == AnyHashable("popular") {
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
let context = controller.context
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
}
},
pushController: { c in },
presentController: { c in },
presentGlobalOverlayController: { c in },
navigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
},
requestUpdate: { _ in },
updateSearchQuery: { _, _ in },
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
useOpaqueTheme: false
)
content.masks?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let strongSelf = self, let file = item.itemFile else {
return
}
strongSelf.controller?.completion(file)
strongSelf.controller?.dismiss(animated: true)
},
deleteBackwards: nil,
openStickerSettings: nil,
openFeatured: {
},
openSearch: {},
addGroupAction: { [weak self] groupId, isPremiumLocked in
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
return
}
let context = controller.context
if isPremiumLocked {
// let controller = PremiumIntroScreen(context: context, source: .stickers)
// controllerInteraction.navigationController()?.pushViewController(controller)
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.info.id == collectionId {
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(completed: {
})
break
}
}
})
},
clearGroup: { [weak self] groupId in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
if groupId == AnyHashable("popular") {
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
let context = controller.context
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
}
},
pushController: { c in },
presentController: { c in },
presentGlobalOverlayController: { c in },
navigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
},
requestUpdate: { _ in },
updateSearchQuery: { _, _ in },
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
useOpaqueTheme: false
)
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let strongSelf = self, let file = item.itemFile else {
return
}
strongSelf.controller?.completion(file)
strongSelf.controller?.dismiss(animated: true)
},
deleteBackwards: nil,
openStickerSettings: nil,
openFeatured: { [weak self] in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
strongSelf.controller?.push(
FeaturedStickersScreen(
context: controller.context,
highlightedPackId: nil,
sendSticker: { _, _, _ in
// guard let controllerInteraction = controllerInteraction else {
// return false
// }
return false
// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
}
)
)
},
openSearch: {},
addGroupAction: { [weak self] groupId, isPremiumLocked in
@ -477,6 +734,7 @@ public class StickerPickerScreen: ViewController {
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
let clipFrame: CGRect
let contentFrame: CGRect
if layout.metrics.widthClass == .compact {
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
if isLandscape {
@ -495,6 +753,7 @@ public class StickerPickerScreen: ViewController {
if isLandscape {
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
contentFrame = clipFrame
} else {
let coveredByModalTransition: CGFloat = 0.0
var containerTopInset: CGFloat = 10.0
@ -510,6 +769,7 @@ public class StickerPickerScreen: ViewController {
let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height)
contentFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height - topInset)
}
} else {
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
@ -521,12 +781,13 @@ public class StickerPickerScreen: ViewController {
let minSide = min(layout.size.width, layout.size.height)
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
contentFrame = clipFrame
}
transition.setFrame(view: self.containerView, frame: clipFrame)
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
if let stickerContent = self.stickerContent {
if let content = self.content {
var stickersTransition: Transition = transition
if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint {
self.scheduledEmojiContentAnimationHint = nil
@ -541,14 +802,15 @@ public class StickerPickerScreen: ViewController {
theme: self.theme,
strings: self.presentationData.strings,
deviceMetrics: layout.deviceMetrics,
stickerContent: stickerContent,
bottomInset: layout.intrinsicInsets.bottom,
content: content,
backgroundColor: self.theme.list.itemBlocksBackgroundColor,
separatorColor: self.theme.list.blocksBackgroundColor
)
),
environment: {},
forceUpdate: true,
containerSize: CGSize(width: clipFrame.size.width, height: clipFrame.height)
containerSize: CGSize(width: contentFrame.size.width, height: contentFrame.height)
)
contentSize.height = max(layout.size.height - navigationHeight, contentSize.height)
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil)
@ -816,10 +1078,6 @@ public class StickerPickerScreen: ViewController {
fatalError("init(coder:) has not been implemented")
}
@objc private func cancelPressed() {
self.dismiss(animated: true, completion: nil)
}
override open func loadDisplayNode() {
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
self.displayNodeDidLoad()

View file

@ -5,6 +5,7 @@ import ComponentFlow
import LegacyComponents
import TelegramCore
import Postbox
import LottieAnimationComponent
enum DrawingTextStyle: Equatable {
case regular
@ -175,21 +176,29 @@ final class TextAlignmentComponent: Component {
}
final class TextFontComponent: Component {
let styleButton: AnyComponent<Empty>
let alignmentButton: AnyComponent<Empty>
let values: [DrawingTextFont]
let selectedValue: DrawingTextFont
let updated: (DrawingTextFont) -> Void
init(values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) {
init(styleButton: AnyComponent<Empty>, alignmentButton: AnyComponent<Empty>, values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) {
self.styleButton = styleButton
self.alignmentButton = alignmentButton
self.values = values
self.selectedValue = selectedValue
self.updated = updated
}
static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool {
return lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue
return lhs.styleButton == rhs.styleButton && lhs.alignmentButton == rhs.alignmentButton && lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue
}
public final class View: UIView {
private let styleButtonHost: ComponentView<Empty>
private let alignmentButtonHost: ComponentView<Empty>
private var buttons: [DrawingTextFont: HighlightableButton] = [:]
private let scrollView = UIScrollView()
private let scrollMask = UIView()
@ -207,6 +216,9 @@ final class TextFontComponent: Component {
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.decelerationRate = .fast
self.styleButtonHost = ComponentView()
self.alignmentButtonHost = ComponentView()
super.init(frame: frame)
self.mask = self.scrollMask
@ -251,6 +263,36 @@ final class TextFontComponent: Component {
var contentWidth: CGFloat = 0.0
let styleSize = self.styleButtonHost.update(
transition: transition,
component: component.styleButton,
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.styleButtonHost.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: -7.0, y: -7.0), size: styleSize)
}
contentWidth += 44.0
let alignmentSize = self.alignmentButtonHost.update(
transition: transition,
component: component.alignmentButton,
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.alignmentButtonHost.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: contentWidth - 7.0, y: -7.0), size: alignmentSize)
}
contentWidth += 32.0
for value in component.values {
contentWidth += 12.0
let button: HighlightableButton
@ -319,6 +361,7 @@ final class TextSettingsComponent: CombinedComponent {
let style: DrawingTextStyle
let alignment: DrawingTextAlignment
let font: DrawingTextFont
let isEmojiKeyboard: Bool
let presentColorPicker: () -> Void
let presentFastColorPicker: (GenericComponentViewTag) -> Void
@ -327,24 +370,28 @@ final class TextSettingsComponent: CombinedComponent {
let toggleStyle: () -> Void
let toggleAlignment: () -> Void
let updateFont: (DrawingTextFont) -> Void
let toggleKeyboard: (() -> Void)?
init(
color: DrawingColor?,
style: DrawingTextStyle,
alignment: DrawingTextAlignment,
font: DrawingTextFont,
isEmojiKeyboard: Bool,
presentColorPicker: @escaping () -> Void = {},
presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in },
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
dismissFastColorPicker: @escaping () -> Void = {},
toggleStyle: @escaping () -> Void,
toggleAlignment: @escaping () -> Void,
updateFont: @escaping (DrawingTextFont) -> Void
updateFont: @escaping (DrawingTextFont) -> Void,
toggleKeyboard: (() -> Void)?
) {
self.color = color
self.style = style
self.alignment = alignment
self.font = font
self.isEmojiKeyboard = isEmojiKeyboard
self.presentColorPicker = presentColorPicker
self.presentFastColorPicker = presentFastColorPicker
self.updateFastColorPickerPan = updateFastColorPickerPan
@ -352,6 +399,7 @@ final class TextSettingsComponent: CombinedComponent {
self.toggleStyle = toggleStyle
self.toggleAlignment = toggleAlignment
self.updateFont = updateFont
self.toggleKeyboard = toggleKeyboard
}
static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool {
@ -367,6 +415,9 @@ final class TextSettingsComponent: CombinedComponent {
if lhs.font != rhs.font {
return false
}
if lhs.isEmojiKeyboard != rhs.isEmojiKeyboard {
return false
}
return true
}
@ -376,6 +427,8 @@ final class TextSettingsComponent: CombinedComponent {
case filled
case semi
case stroke
case keyboard
case emoji
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey) -> UIImage {
@ -392,6 +445,10 @@ final class TextSettingsComponent: CombinedComponent {
image = UIImage(bundleImageName: "Media Editor/TextSemi")!
case .stroke:
image = UIImage(bundleImageName: "Media Editor/TextStroke")!
case .keyboard:
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: .white)!
case .emoji:
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputEmojiIcon"), color: .white)!
}
cachedImages[key] = image
return image
@ -407,8 +464,9 @@ final class TextSettingsComponent: CombinedComponent {
let colorButton = Child(ColorSwatchComponent.self)
let colorButtonTag = GenericComponentViewTag()
let alignmentButton = Child(Button.self)
let styleButton = Child(Button.self)
// let styleButton = Child(Button.self)
// let alignmentButton = Child(Button.self)
let keyboardButton = Child(Button.self)
let font = Child(TextFontComponent.self)
return { context in
@ -465,69 +523,71 @@ final class TextSettingsComponent: CombinedComponent {
styleImage = state.image(.stroke)
}
let styleButton = styleButton.update(
component: Button(
content: AnyComponent(
Image(
image: styleImage
)
),
action: {
toggleStyle()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .easeInOut(duration: 0.2)
)
context.add(styleButton
.position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0))
.update(Transition.Update { _, view, transition in
if let snapshot = view.snapshotView(afterScreenUpdates: false) {
transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
snapshot?.removeFromSuperview()
})
snapshot.frame = view.frame
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
view.superview?.addSubview(snapshot)
}
})
)
offset += 44.0
let alignmentButton = alignmentButton.update(
component: Button(
content: AnyComponent(
TextAlignmentComponent(
alignment: component.alignment
)
),
action: {
toggleAlignment()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: context.availableSize,
transition: .easeInOut(duration: 0.2)
)
context.add(alignmentButton
.position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0))
)
offset += 44.0
var fontAvailableWidth: CGFloat = context.availableSize.width
if component.color != nil {
fontAvailableWidth -= 88.0
}
let font = font.update(
component: TextFontComponent(
styleButton: AnyComponent(
Button(
content: AnyComponent(
Image(
image: styleImage
)
),
action: {
toggleStyle()
}
).minSize(CGSize(width: 44.0, height: 44.0))
),
alignmentButton: AnyComponent(
Button(
content: AnyComponent(
TextAlignmentComponent(
alignment: component.alignment
)
),
action: {
toggleAlignment()
}
).minSize(CGSize(width: 44.0, height: 44.0))
),
values: DrawingTextFont.allCases,
selectedValue: component.font,
updated: { font in
updateFont(font)
}
),
availableSize: CGSize(width: context.availableSize.width - offset, height: 30.0),
availableSize: CGSize(width: fontAvailableWidth, height: 30.0),
transition: .easeInOut(duration: 0.2)
)
context.add(font
.position(CGPoint(x: offset + font.size.width / 2.0, y: context.availableSize.height / 2.0))
)
offset += 44.0
if let toggleKeyboard = component.toggleKeyboard {
let keyboardButton = keyboardButton.update(
component: Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(name: !component.isEmojiKeyboard ? "input_anim_smileToKey" : "input_anim_keyToSmile" , mode: .animateTransitionFromPrevious),
colors: ["__allcolors__": UIColor.white],
size: CGSize(width: 32.0, height: 32.0)
)
),
action: {
toggleKeyboard()
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 32.0, height: 32.0),
transition: .easeInOut(duration: 0.2)
)
context.add(keyboardButton
.position(CGPoint(x: context.availableSize.width - keyboardButton.size.width / 2.0, y: context.availableSize.height / 2.0))
)
}
return context.availableSize
}

View file

@ -0,0 +1,39 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "FeaturedStickersScreen",
module_name = "FeaturedStickersScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/MergeLists:MergeLists",
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/StickerPeekUI:StickerPeekUI",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/UndoUI:UndoUI",
"//submodules/ContextUI:ContextUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/StickerResources:StickerResources",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,19 @@
import Foundation
import AsyncDisplayKit
import Display
import ChatPresentationInterfaceState
import TelegramPresentationData
open class ChatMediaInputPane: ASDisplayNode {
var inputNodeInteraction: ChatMediaInputNodeInteraction?
var collectionListPanelOffset: CGFloat = 0.0
var isEmpty: Bool {
return false
}
open func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
}
open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
}
}

View file

@ -12,15 +12,16 @@ import AccountContext
import StickerPackPreviewUI
import PresentationDataUtils
import UndoUI
import ChatControllerInteraction
final class TrendingPaneInteraction {
let installPack: (ItemCollectionInfo) -> Void
let openPack: (ItemCollectionInfo) -> Void
let getItemIsPreviewed: (StickerPackItem) -> Bool
let openSearch: () -> Void
let itemContext = StickerPaneSearchGlobalItemContext()
public final class TrendingPaneInteraction {
public let installPack: (ItemCollectionInfo) -> Void
public let openPack: (ItemCollectionInfo) -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public let openSearch: () -> Void
public let itemContext = StickerPaneSearchGlobalItemContext()
init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) {
public init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) {
self.installPack = installPack
self.openPack = openPack
self.getItemIsPreviewed = getItemIsPreviewed
@ -28,17 +29,17 @@ final class TrendingPaneInteraction {
}
}
final class TrendingPanePackEntry: Identifiable, Comparable {
let index: Int
let info: StickerPackCollectionInfo
let theme: PresentationTheme
let strings: PresentationStrings
let topItems: [StickerPackItem]
let installed: Bool
let unread: Bool
let topSeparator: Bool
public final class TrendingPanePackEntry: Identifiable, Comparable {
public let index: Int
public let info: StickerPackCollectionInfo
public let theme: PresentationTheme
public let strings: PresentationStrings
public let topItems: [StickerPackItem]
public let installed: Bool
public let unread: Bool
public let topSeparator: Bool
init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) {
public init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) {
self.index = index
self.info = info
self.theme = theme
@ -49,11 +50,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable {
self.topSeparator = topSeparator
}
var stableId: ItemCollectionId {
public var stableId: ItemCollectionId {
return self.info.id
}
static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
public static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
@ -81,11 +82,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable {
return true
}
static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
public static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
public func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
let info = self.info
return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, listAppearance: false, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: false, installed: self.installed, unread: self.unread, open: {
interaction.openPack(info)
@ -190,13 +191,13 @@ private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], ins
return result
}
final class ChatMediaInputTrendingPane: ChatMediaInputPane {
public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
private let context: AccountContext
private let controllerInteraction: ChatControllerInteraction
private let getItemIsPreviewed: (StickerPackItem) -> Bool
private let isPane: Bool
let gridNode: GridNode
public let gridNode: GridNode
private var enqueuedTransitions: [TrendingPaneTransition] = []
private var validLayout: (CGSize, CGFloat)?
@ -206,15 +207,15 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
private let _ready = Promise<Void>()
private var didSetReady = false
var ready: Signal<Void, NoError> {
public var ready: Signal<Void, NoError> {
return self._ready.get()
}
var scrollingInitiated: (() -> Void)?
public var scrollingInitiated: (() -> Void)?
private let installDisposable = MetaDisposable()
init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
self.context = context
self.controllerInteraction = controllerInteraction
self.getItemIsPreviewed = getItemIsPreviewed
@ -236,7 +237,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
self.installDisposable.dispose()
}
func activate() {
public func activate() {
if self.isActivated {
return
}
@ -369,7 +370,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
})
}
override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
public override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.validLayout != nil
self.validLayout = (size, bottomInset)
@ -401,7 +402,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
}
}
override func willEnterHierarchy() {
public override func willEnterHierarchy() {
super.willEnterHierarchy()
self.activate()
@ -416,7 +417,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
}
}
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
let localPoint = self.view.convert(point, to: self.gridNode.view)
var resultNode: StickerPaneSearchGlobalItemNode?
self.gridNode.forEachItemNode { itemNode in
@ -430,7 +431,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
return nil
}
func updatePreviewing(animated: Bool) {
public func updatePreviewing(animated: Bool) {
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
itemNode.updatePreviewing(animated: animated)

View file

@ -17,6 +17,7 @@ import SearchBarNode
import UndoUI
import ContextUI
import PremiumUI
import ChatPresentationInterfaceState
private final class FeaturedInteraction {
let installPack: (ItemCollectionInfo, Bool) -> Void
@ -787,7 +788,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
}
}
final class FeaturedStickersScreen: ViewController {
public final class FeaturedStickersScreen: ViewController {
private let context: AccountContext
fileprivate let highlightedPackId: ItemCollectionId?
private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
@ -1492,3 +1493,17 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
}
}
}
public final class StickerPaneSearchInteraction {
public let open: (StickerPackCollectionInfo) -> Void
public let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void
public let sendSticker: (FileMediaReference, UIView, CGRect) -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
self.open = open
self.install = install
self.sendSticker = sendSticker
self.getItemIsPreviewed = getItemIsPreviewed
}
}

View file

@ -11,35 +11,35 @@ private func generateLoupeIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: templateLoupeIcon, color: color)
}
enum PaneSearchBarType {
public enum PaneSearchBarType {
case stickers
case gifs
}
final class PaneSearchBarPlaceholderItem: GridItem {
let theme: PresentationTheme
let strings: PresentationStrings
let type: PaneSearchBarType
let activate: () -> Void
public final class PaneSearchBarPlaceholderItem: GridItem {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let type: PaneSearchBarType
public let activate: () -> Void
let section: GridSection? = nil
let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true)
public let section: GridSection? = nil
public let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true)
init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
public init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.type = type
self.activate = activate
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = PaneSearchBarPlaceholderNode()
node.activate = self.activate
node.setup(theme: self.theme, strings: self.strings, type: self.type)
return node
}
func update(node: GridItemNode) {
public func update(node: GridItemNode) {
guard let node = node as? PaneSearchBarPlaceholderNode else {
assertionFailure()
return
@ -49,15 +49,15 @@ final class PaneSearchBarPlaceholderItem: GridItem {
}
}
final class PaneSearchBarPlaceholderNode: GridItemNode {
public final class PaneSearchBarPlaceholderNode: GridItemNode {
private var currentState: (PresentationTheme, PresentationStrings, PaneSearchBarType)?
var activate: (() -> Void)?
public var activate: (() -> Void)?
let backgroundNode: ASImageNode
let labelNode: ImmediateTextNode
let iconNode: ASImageNode
public let backgroundNode: ASImageNode
public let labelNode: ImmediateTextNode
public let iconNode: ASImageNode
override init() {
public override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
@ -82,13 +82,13 @@ final class PaneSearchBarPlaceholderNode: GridItemNode {
self.addSubnode(self.iconNode)
}
override func didLoad() {
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
public func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
if self.currentState?.0 !== theme || self.currentState?.1 !== strings || self.currentState?.2 != type {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
@ -105,7 +105,7 @@ final class PaneSearchBarPlaceholderNode: GridItemNode {
}
}
override func layout() {
public override func layout() {
super.layout()
let bounds = self.bounds

View file

@ -69,30 +69,34 @@ private final class StickerPaneSearchGlobalSectionNode: ASDisplayNode {
}
}
final class StickerPaneSearchGlobalItemContext {
var canPlayMedia: Bool = false
public final class StickerPaneSearchGlobalItemContext {
public var canPlayMedia: Bool
public init(canPlayMedia: Bool = false) {
self.canPlayMedia = canPlayMedia
}
}
final class StickerPaneSearchGlobalItem: GridItem {
let account: Account
let theme: PresentationTheme
let strings: PresentationStrings
let listAppearance: Bool
let fillsRow: Bool
let info: StickerPackCollectionInfo
let topItems: [StickerPackItem]
let topSeparator: Bool
let regularInsets: Bool
let installed: Bool
let installing: Bool
let unread: Bool
let open: () -> Void
let install: () -> Void
let getItemIsPreviewed: (StickerPackItem) -> Bool
let itemContext: StickerPaneSearchGlobalItemContext
public final class StickerPaneSearchGlobalItem: GridItem {
public let account: Account
public let theme: PresentationTheme
public let strings: PresentationStrings
public let listAppearance: Bool
public let fillsRow: Bool
public let info: StickerPackCollectionInfo
public let topItems: [StickerPackItem]
public let topSeparator: Bool
public let regularInsets: Bool
public let installed: Bool
public let installing: Bool
public let unread: Bool
public let open: () -> Void
public let install: () -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public let itemContext: StickerPaneSearchGlobalItemContext
let section: GridSection?
var fillsRowWithHeight: (CGFloat, Bool)? {
public let section: GridSection?
public var fillsRowWithHeight: (CGFloat, Bool)? {
var additionalHeight: CGFloat = 0.0
if self.regularInsets {
additionalHeight = 12.0 + 12.0
@ -106,7 +110,7 @@ final class StickerPaneSearchGlobalItem: GridItem {
return (128.0 + additionalHeight, self.fillsRow)
}
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
self.account = account
self.theme = theme
self.strings = strings
@ -126,13 +130,13 @@ final class StickerPaneSearchGlobalItem: GridItem {
self.section = StickerPaneSearchGlobalSection(title: sectionTitle, theme: theme)
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPaneSearchGlobalItemNode()
node.setup(item: self)
return node
}
func update(node: GridItemNode) {
public func update(node: GridItemNode) {
guard let node = node as? StickerPaneSearchGlobalItemNode else {
assertionFailure()
return
@ -145,7 +149,7 @@ private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.semibold(13.0)
class StickerPaneSearchGlobalItemNode: GridItemNode {
public class StickerPaneSearchGlobalItemNode: GridItemNode {
private let titleNode: TextNode
private let descriptionNode: TextNode
private let unreadNode: ASImageNode
@ -159,7 +163,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
private let topSeparatorNode: ASDisplayNode
private var highlightNode: ASDisplayNode?
var item: StickerPaneSearchGlobalItem?
public var item: StickerPaneSearchGlobalItem?
private var appliedItem: StickerPaneSearchGlobalItem?
private let preloadDisposable = MetaDisposable()
private let preloadedStickerPackThumbnailDisposable = MetaDisposable()
@ -175,7 +179,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
}
}
override var isVisibleInGrid: Bool {
public override var isVisibleInGrid: Bool {
didSet {
if oldValue != self.isVisibleInGrid {
self.updatePlayback()
@ -200,7 +204,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
}
}
override init() {
public override init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
@ -298,14 +302,14 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
self.preloadedStickerPackThumbnailDisposable.dispose()
}
override func didLoad() {
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
private var absoluteLocation: (CGRect, CGSize)?
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
public override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteLocation = (rect, containerSize)
for node in self.itemNodes {
@ -314,7 +318,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
}
}
func setup(item: StickerPaneSearchGlobalItem) {
public func setup(item: StickerPaneSearchGlobalItem) {
if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && self.item?.info.id != item.info.id {
self.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.account.network, postbox: item.account.postbox, id: item.info.id).start())
}
@ -325,7 +329,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
self.updatePreviewing(animated: false)
}
func updateCanPlayMedia() {
public func updateCanPlayMedia() {
guard let item = self.item else {
return
}
@ -333,7 +337,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
self.canPlayMedia = item.itemContext.canPlayMedia
}
func highlight() {
public func highlight() {
guard self.highlightNode == nil else {
return
}
@ -354,7 +358,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
}
}
override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
public override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = self.item else {
return
}
@ -521,7 +525,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
}
}
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
guard let item = self.item else {
return nil
}
@ -535,7 +539,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
return nil
}
func updatePreviewing(animated: Bool) {
public func updatePreviewing(animated: Bool) {
guard let item = self.item else {
return
}

View file

@ -10,6 +10,7 @@ import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ChatPresentationInterfaceState
final class StickerPaneSearchStickerSection: GridSection {
let code: String
@ -65,16 +66,16 @@ final class StickerPaneSearchStickerSectionNode: ASDisplayNode {
}
}
final class StickerPaneSearchStickerItem: GridItem {
let account: Account
let code: String?
let stickerItem: FoundStickerItem
let selected: (ASDisplayNode, CGRect) -> Void
let inputNodeInteraction: ChatMediaInputNodeInteraction
public final class StickerPaneSearchStickerItem: GridItem {
public let account: Account
public let code: String?
public let stickerItem: FoundStickerItem
public let selected: (ASDisplayNode, CGRect) -> Void
public let inputNodeInteraction: ChatMediaInputNodeInteraction
let section: GridSection?
public let section: GridSection?
init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) {
public init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) {
self.account = account
self.stickerItem = stickerItem
self.inputNodeInteraction = inputNodeInteraction
@ -83,7 +84,7 @@ final class StickerPaneSearchStickerItem: GridItem {
self.section = nil
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPaneSearchStickerItemNode()
node.inputNodeInteraction = self.inputNodeInteraction
node.setup(account: self.account, stickerItem: self.stickerItem, code: self.code)
@ -91,7 +92,7 @@ final class StickerPaneSearchStickerItem: GridItem {
return node
}
func update(node: GridItemNode) {
public func update(node: GridItemNode) {
guard let node = node as? StickerPaneSearchStickerItemNode else {
assertionFailure()
return
@ -104,17 +105,17 @@ final class StickerPaneSearchStickerItem: GridItem {
private let textFont = Font.regular(20.0)
final class StickerPaneSearchStickerItemNode: GridItemNode {
public final class StickerPaneSearchStickerItemNode: GridItemNode {
private var currentState: (Account, FoundStickerItem, CGSize)?
let imageNode: TransformImageNode
private(set) var animationNode: AnimatedStickerNode?
public let imageNode: TransformImageNode
public private(set) var animationNode: AnimatedStickerNode?
private let textNode: ASTextNode
private let stickerFetchedDisposable = MetaDisposable()
var currentIsPreviewing = false
public var currentIsPreviewing = false
override var isVisibleInGrid: Bool {
public override var isVisibleInGrid: Bool {
didSet {
self.updateVisibility()
}
@ -122,14 +123,14 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
private var isPlaying = false
var inputNodeInteraction: ChatMediaInputNodeInteraction?
var selected: ((ASDisplayNode, CGRect) -> Void)?
public var inputNodeInteraction: ChatMediaInputNodeInteraction?
public var selected: ((ASDisplayNode, CGRect) -> Void)?
var stickerItem: FoundStickerItem? {
public var stickerItem: FoundStickerItem? {
return self.currentState?.1
}
override init() {
public override init() {
self.imageNode = TransformImageNode()
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
@ -145,7 +146,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
self.stickerFetchedDisposable.dispose()
}
override func didLoad() {
public override func didLoad() {
super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
@ -184,7 +185,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
}
}
override func layout() {
public override func layout() {
super.layout()
let bounds = self.bounds
@ -211,11 +212,11 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
self.selected?(self, self.bounds)
}
func transitionNode() -> ASDisplayNode? {
public func transitionNode() -> ASDisplayNode? {
return self.imageNode
}
func updateVisibility() {
public func updateVisibility() {
let isPlaying = self.isVisibleInGrid
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
@ -223,7 +224,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
}
}
func updatePreviewing(animated: Bool) {
public func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
isPreviewing = interaction.previewedStickerPackItem == .found(item)

View file

@ -116,6 +116,10 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
switch action.action {
case let .photoUpdated(image), let .suggestedProfilePhoto(image):
if let peer = messageMainPeer(EngineMessage(message)), let image = image {
var isSuggested = false
if case .suggestedProfilePhoto = action.action {
isSuggested = true
}
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false)])
let sourceCorners: AvatarGalleryController.SourceCorners
@ -124,7 +128,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
} else {
sourceCorners = .round
}
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
})
return .chatAvatars(galleryController, image)

View file

@ -60,7 +60,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable {
}
}
public func composedDisplayTitle(strings: PresentationStrings) -> String {
public func composedDisplayTitle(context: AccountContext, strings: PresentationStrings) -> String {
switch self {
case let .personName(firstName, lastName, phone):
if !firstName.isEmpty {
@ -72,7 +72,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable {
} else if !lastName.isEmpty {
return lastName
} else if !phone.isEmpty {
return formatPhoneNumber("+\(phone)")
return formatPhoneNumber(context: context, number: "+\(phone)")
} else {
return strings.User_DeletedAccount
}
@ -395,7 +395,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
nameMaximumNumberOfLines = 2
}
let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(context: item.accountContext, strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var statusText: String = ""
let statusColor: UIColor
@ -404,7 +404,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
switch item.mode {
case .settings:
if let phone = peer.phone, !phone.isEmpty {
statusText += formatPhoneNumber(phone)
statusText += formatPhoneNumber(context: item.accountContext, number: phone)
}
if let username = peer.addressName, !username.isEmpty {
if !statusText.isEmpty {

View file

@ -22,6 +22,7 @@ public enum ItemListPeerActionItemColor {
public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let icon: UIImage?
let iconSignal: Signal<UIImage?, NoError>?
let title: String
public let alwaysPlain: Bool
let hasSeparator: Bool
@ -31,9 +32,10 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.iconSignal = iconSignal
self.title = title
self.alwaysPlain = alwaysPlain
self.hasSeparator = hasSeparator
@ -114,6 +116,8 @@ class ItemListPeerActionItemNode: ListViewItemNode {
private var item: ItemListPeerActionItem?
private let iconDisposable = MetaDisposable()
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
@ -149,6 +153,10 @@ class ItemListPeerActionItemNode: ListViewItemNode {
self.addSubnode(self.activateArea)
}
deinit {
self.iconDisposable.dispose()
}
func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -171,7 +179,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
iconOffset = 1.0
verticalInset = 11.0
verticalOffset = 0.0
leftInset = (item.icon == nil ? 16.0 : 59.0) + params.leftInset
leftInset = (item.icon == nil && item.iconSignal == nil ? 16.0 : 59.0) + params.leftInset
case .peerList:
iconOffset = 3.0
verticalInset = 14.0
@ -232,6 +240,15 @@ class ItemListPeerActionItemNode: ListViewItemNode {
strongSelf.iconNode.image = item.icon
if let image = item.icon {
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0) + iconOffset, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
} else if let iconSignal = item.iconSignal {
let imageSize = CGSize(width: 28.0, height: 28.0)
strongSelf.iconDisposable.set((iconSignal
|> deliverOnMainQueue).start(next: { [weak self] image in
if let strongSelf = self, let image {
strongSelf.iconNode.image = image
}
}))
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - imageSize.width) / 2.0) + iconOffset, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize))
}
if strongSelf.backgroundNode.supernode == nil {

View file

@ -94,7 +94,7 @@
- (UIImage *)paintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (UIImage *)stillPaintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)image forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;
- (void)clearPaintingData;
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item;

View file

@ -3,8 +3,8 @@
#import <LegacyComponents/TGModernGalleryImageItemImageView.h>
#import <AVFoundation/AVFoundation.h>
@class TGPhotoEntitiesContainerView;
@protocol TGMediaEditableItem;
@protocol TGPhotoDrawingEntitiesView;
@interface TGMediaPickerGalleryVideoItemView : TGModernGalleryItemView <TGModernGalleryEditableItemView>
@ -34,7 +34,7 @@
- (UIImage *)screenImage;
- (UIImage *)transitionImage;
- (CGRect)editorTransitionViewRect;
- (TGPhotoEntitiesContainerView *)entitiesView;
- (UIView<TGPhotoDrawingEntitiesView> *)entitiesView;
- (id<TGMediaEditableItem>)editableMediaItem;

View file

@ -8,21 +8,20 @@
@interface TGPaintingData : NSObject
@property (nonatomic, readonly) NSString *imagePath;
@property (nonatomic, readonly) NSString *dataPath;
@property (nonatomic, readonly) NSArray *entities;
@property (nonatomic, readonly) TGPaintUndoManager *undoManager;
@property (nonatomic, readonly) NSArray *stickers;
@property (nonatomic, readonly) NSData *data;
@property (nonatomic, readonly) NSData *drawingData;
@property (nonatomic, readonly) NSData *entitiesData;
@property (nonatomic, readonly) UIImage *image;
@property (nonatomic, readonly) UIImage *stillImage;
@property (nonatomic, readonly) NSArray *stickers;
@property (nonatomic, readonly) bool hasAnimation;
+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager;
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation;
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities;
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData;
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath;

View file

@ -1,7 +1,7 @@
#import <UIKit/UIKit.h>
@class PGPhotoEditorView;
@class TGPhotoEntitiesContainerView;
@protocol TGPhotoDrawingEntitiesView;
@interface TGPhotoAvatarCropView : UIView
@ -23,7 +23,7 @@
@property (nonatomic, readonly) bool isTracking;
@property (nonatomic, readonly) bool isAnimating;
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square;
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView<TGPhotoDrawingEntitiesView> *)fullEntitiesView square:(bool)square;
- (void)setSnapshotImage:(UIImage *)image;
- (void)setSnapshotView:(UIView *)snapshotView;

View file

@ -12,7 +12,7 @@
@class AVPlayer;
@protocol TGPhotoPaintStickersContext;
@class TGPhotoEntitiesContainerView;
@protocol TGPhotoDrawingEntitiesView;
typedef enum {
TGPhotoEditorControllerGenericIntent = 0,
@ -60,9 +60,11 @@ typedef enum {
@property (nonatomic, strong) PGCameraShotMetadata *metadata;
@property (nonatomic, strong) NSArray *faces;
@property (nonatomic, strong) NSString *senderName;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) TGPhotoEntitiesContainerView *entitiesView;
@property (nonatomic, strong) UIView<TGPhotoDrawingEntitiesView> *entitiesView;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab;

View file

@ -89,6 +89,8 @@
- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer;
- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer;
- (void)setupWithEntitiesData:(NSData *)entitiesData;
@end
@protocol TGPhotoDrawingInterfaceController <NSObject>
@ -134,6 +136,8 @@
- (UIView<TGPhotoSolidRoundedButtonView> *)solidRoundedButton:(NSString *)title action:(void(^)(void))action;
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size;
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize;
- (UIView<TGPhotoDrawingEntitiesView> *)drawingEntitiesViewWithSize:(CGSize)size;
@end

View file

@ -2,7 +2,7 @@
@interface TGPhotoVideoEditor : NSObject
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;

View file

@ -2969,14 +2969,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if (adjustments.paintingData.stickers.count > 0)
dict[@"stickers"] = adjustments.paintingData.stickers;
bool animated = false;
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
if (entity.animated) {
animated = true;
break;
}
}
bool animated = adjustments.paintingData.hasAnimation;
if (animated) {
dict[@"isAnimation"] = @true;
if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) {

View file

@ -929,10 +929,8 @@
grouping = false;
}
}
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
if (entity.animated) {
grouping = true;
}
if (adjustments.paintingData.hasAnimation) {
grouping = false;
}
}
}
@ -1159,14 +1157,7 @@
if (adjustments.paintingData.stickers.count > 0)
dict[@"stickers"] = adjustments.paintingData.stickers;
bool animated = false;
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
if (entity.animated) {
animated = true;
break;
}
}
bool animated = adjustments.paintingData.hasAnimation;
if (animated) {
dict[@"isAnimation"] = @true;
if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) {
@ -1448,10 +1439,8 @@
grouping = false;
}
}
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
if (entity.animated) {
grouping = true;
}
if (adjustments.paintingData.hasAnimation) {
grouping = false;
}
}
}

View file

@ -715,7 +715,7 @@
[_queue dispatch:block];
}
- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video
{
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
@ -725,6 +725,7 @@
NSURL *imagesDirectory = video ? _videoPaintingImagesUrl : _paintingImagesUrl;
NSURL *imageUrl = [imagesDirectory URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", [TGStringUtils md5:itemId]]];
NSURL *dataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.dat", [TGStringUtils md5:itemId]]];
NSURL *entitiesDataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@_entities.dat", [TGStringUtils md5:itemId]]];
[_paintingImageCache setImage:image forKey:itemId attributes:NULL];
@ -733,12 +734,16 @@
bool imageSuccess = [imageData writeToURL:imageUrl options:NSDataWritingAtomic error:nil];
[[NSFileManager defaultManager] removeItemAtURL:dataUrl error:nil];
bool dataSuccess = [data writeToURL:dataUrl options:NSDataWritingAtomic error:nil];
bool entitiesDataSuccess = [entitiesData writeToURL:entitiesDataUrl options:NSDataWritingAtomic error:nil];
if (imageSuccess && imageOutUrl != NULL)
*imageOutUrl = imageUrl;
if (dataSuccess && dataOutUrl != NULL)
*dataOutUrl = dataUrl;
if (entitiesDataSuccess && entitiesDataOutUrl != NULL)
*entitiesDataOutUrl = entitiesDataUrl;
if (video)
[_storeVideoPaintingImages addObject:imageUrl];

View file

@ -21,8 +21,6 @@
#import <LegacyComponents/TGSecretTimerMenu.h>
#import "TGPhotoEntitiesContainerView.h"
@interface TGMediaPickerGalleryModel ()
{
TGMediaPickerGalleryInterfaceView *_interfaceView;
@ -363,7 +361,7 @@
UIView *referenceParentView = nil;
UIImage *image = nil;
TGPhotoEntitiesContainerView *entitiesView = nil;
UIView<TGPhotoDrawingEntitiesView> *entitiesView = nil;
id<TGMediaEditableItem> editableMediaItem = item.editableMediaItem;
@ -373,7 +371,7 @@
screenImage = [(UIImageView *)editorReferenceView image];
referenceView = editorReferenceView;
if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject isKindOfClass:[TGPhotoEntitiesContainerView class]]) {
if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject conformsToProtocol:@protocol(TGPhotoDrawingEntitiesView)]) {
entitiesView = editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject;
}
}

View file

@ -20,8 +20,7 @@
#import "TGMediaPickerGalleryPhotoItem.h"
#import "TGPhotoEntitiesContainerView.h"
#import "TGPhotoPaintController.h"
#import "TGPhotoDrawingController.h"
#import <LegacyComponents/TGMenuView.h>
@ -42,7 +41,7 @@
UIView *_contentView;
UIView *_contentWrapperView;
TGPhotoEntitiesContainerView *_entitiesContainerView;
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
SMetaDisposable *_adjustmentsDisposable;
SMetaDisposable *_attributesDisposable;
@ -90,11 +89,7 @@
_contentWrapperView = [[UIView alloc] init];
[_contentView addSubview:_contentWrapperView];
_entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init];
_entitiesContainerView.userInteractionEnabled = false;
[_contentWrapperView addSubview:_entitiesContainerView];
_fileInfoLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 20)];
_fileInfoLabel.backgroundColor = [UIColor clearColor];
_fileInfoLabel.font = TGSystemFontOfSize(13);
@ -143,7 +138,11 @@
[super setItem:item synchronously:synchronously];
_entitiesContainerView.stickersContext = item.stickersContext;
if (_entitiesView == nil) {
_entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.asset.originalSize];
_entitiesView.userInteractionEnabled = false;
[_contentWrapperView addSubview:_entitiesView];
}
_imageSize = item.asset.originalSize;
[self reset];
@ -223,7 +222,7 @@
return;
[strongSelf layoutEntities];
[strongSelf->_entitiesContainerView setupWithPaintingData:next.paintingData];
[strongSelf->_entitiesView setupWithEntitiesData:next.paintingData.entitiesData];
}]];
}
@ -486,27 +485,27 @@
_contentView.transform = rotationTransform;
_contentView.frame = previewFrame;
CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
CGRect fittedCropRect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false];
CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
CGRect fittedCropRect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false];
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height);
CGFloat contentScale = _contentView.bounds.size.width / fittedCropRect.size.width;
_contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale);
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height);
CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
_entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
_entitiesContainerView.transform = CGAffineTransformMakeRotation(rotation);
CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
_entitiesView.transform = CGAffineTransformMakeRotation(rotation);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation);
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
CGFloat scale = fittedOriginalSize.width / originalSize.width;
CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoPaintController fittedCropRect:cropRect centerScale:scale]);
CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoDrawingController fittedCropRect:cropRect centerScale:scale]);
CGPoint boundsCenter = TGPaintCenterOfRect(_contentWrapperView.bounds);
_entitiesContainerView.center = TGPaintAddPoints(boundsCenter, offset);
_entitiesView.center = TGPaintAddPoints(boundsCenter, offset);
}
@end

View file

@ -32,8 +32,7 @@
#import "TGMediaPickerScrubberHeaderView.h"
#import "TGPhotoEditorPreviewView.h"
#import "TGPhotoEntitiesContainerView.h"
#import "TGPhotoPaintController.h"
#import "TGPhotoDrawingController.h"
#import <LegacyComponents/TGModernGalleryVideoView.h>
#import "TGModernGalleryVideoContentView.h"
@ -84,7 +83,7 @@
UIImageView *_paintingImageView;
UIView *_contentView;
UIView *_contentWrapperView;
TGPhotoEntitiesContainerView *_entitiesContainerView;
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
NSTimer *_positionTimer;
TGObserverProxy *_didPlayToEndObserver;
@ -174,12 +173,7 @@
_contentWrapperView = [[UIView alloc] init];
[_contentView addSubview:_contentWrapperView];
_entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init];
_entitiesContainerView.hidden = true;
_entitiesContainerView.userInteractionEnabled = false;
[_contentWrapperView addSubview:_entitiesContainerView];
_curtainView = [[UIView alloc] init];
_curtainView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_curtainView.backgroundColor = [UIColor blackColor];
@ -425,8 +419,14 @@
_scrubberView.allowsTrimming = false;
_videoDimensions = item.dimensions;
_entitiesContainerView.stickersContext = item.stickersContext;
if (_entitiesView == nil) {
_entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.dimensions];
_entitiesView.hidden = true;
_entitiesView.userInteractionEnabled = false;
[_contentWrapperView addSubview:_entitiesView];
}
__weak TGMediaPickerGalleryVideoItemView *weakSelf = self;
[_videoDurationVar set:[[[item.durationSignal deliverOn:[SQueue mainQueue]] catch:^SSignal *(__unused id error)
{
@ -504,8 +504,8 @@
if (baseAdjustments.sendAsGif || ([strongSelf itemIsLivePhoto]))
[strongSelf setPlayButtonHidden:true animated:false];
[strongSelf->_entitiesContainerView setupWithPaintingData:adjustments.paintingData];
[strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying];
[strongSelf->_entitiesView setupWithEntitiesData:adjustments.paintingData.entitiesData];
[strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying];
[strongSelf->_photoEditor importAdjustments:adjustments];
if (!strongSelf.isPlaying) {
@ -823,24 +823,24 @@
_contentView.transform = rotationTransform;
_contentView.frame = _imageView.frame;
CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height);
CGFloat contentScale = ratio;
_contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale);
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height);
CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
_entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
_entitiesContainerView.transform = CGAffineTransformMakeRotation(0.0);
CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
_entitiesView.transform = CGAffineTransformMakeRotation(0.0);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, 0.0);
__unused CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
}
- (TGPhotoEntitiesContainerView *)entitiesView {
return _entitiesContainerView;
- (UIView<TGPhotoDrawingEntitiesView> *)entitiesView {
return _entitiesView;
}
- (void)singleTap
@ -1176,7 +1176,7 @@
strongSelf->_videoView.userInteractionEnabled = false;
[strongSelf->_playerContainerView insertSubview:strongSelf->_videoView belowSubview:strongSelf->_paintingImageView];
strongSelf->_entitiesContainerView.hidden = false;
strongSelf->_entitiesView.hidden = false;
[strongSelf->_videoView setNeedsTransitionIn];
[strongSelf->_videoView performTransitionInIfNeeded];
@ -1196,7 +1196,7 @@
[strongSelf->_player play];
}
[strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying];
[strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying];
strongSelf->_positionTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(positionTimerEvent) interval:0.25 repeat:true];
[strongSelf positionTimerEvent];
@ -1211,11 +1211,11 @@
{
if (_player.rate > FLT_EPSILON) {
[_scrubberView setIsPlaying:true];
[_entitiesContainerView updateVisibility:true];
[_entitiesView updateVisibility:true];
}
else {
[_scrubberView setIsPlaying:false];
[_entitiesContainerView updateVisibility:false];
[_entitiesView updateVisibility:false];
}
}
}
@ -1251,7 +1251,7 @@
[self positionTimerEvent];
}
[_entitiesContainerView updateVisibility:true];
[_entitiesView updateVisibility:true];
}
- (void)playIfAvailable
@ -1274,7 +1274,7 @@
[_positionTimer invalidate];
_positionTimer = nil;
[_entitiesContainerView updateVisibility:false];
[_entitiesView updateVisibility:false];
}
- (void)togglePlayback

View file

@ -12,7 +12,6 @@
{
UIImage *_image;
UIImage *_stillImage;
NSData *_data;
UIImage *(^_imageRetrievalBlock)(void);
UIImage *(^_stillImageRetrievalBlock)(void);
@ -21,21 +20,21 @@
@implementation TGPaintingData
+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation
{
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
paintingData->_data = data;
paintingData->_drawingData = data;
paintingData->_image = image;
paintingData->_stillImage = stillImage;
paintingData->_entities = entities;
paintingData->_undoManager = undoManager;
paintingData->_entitiesData = entitiesData;
paintingData->_hasAnimation = hasAnimation;
return paintingData;
}
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities {
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData {
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
paintingData->_imagePath = imagePath;
paintingData->_entities = entities;
paintingData->_entitiesData = entitiesData;
return paintingData;
}
@ -49,7 +48,7 @@
- (instancetype)dataForAnimation
{
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
paintingData->_entities = _entities;
paintingData->_entitiesData = _entitiesData;
return paintingData;
}
@ -58,17 +57,17 @@
[[TGPaintingData queue] dispatch:^
{
NSURL *dataUrl = nil;
NSURL *entitiesDataUrl = nil;
NSURL *imageUrl = nil;
NSData *compressedData = TGPaintGZipDeflate(data.data);
[context setPaintingData:compressedData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl imageUrl:&imageUrl forVideo:video];
NSData *compressedDrawingData = TGPaintGZipDeflate(data.drawingData);
NSData *compressedEntitiesData = TGPaintGZipDeflate(data.entitiesData);
[context setPaintingData:compressedDrawingData entitiesData:compressedEntitiesData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl entitiesDataUrl:&entitiesDataUrl imageUrl:&imageUrl forVideo:video];
__weak TGMediaEditingContext *weakContext = context;
[[SQueue mainQueue] dispatch:^
{
data->_dataPath = dataUrl.path;
data->_imagePath = imageUrl.path;
data->_data = nil;
data->_imageRetrievalBlock = ^UIImage *
{
@ -100,20 +99,6 @@
}];
}
- (void)dealloc
{
[self.undoManager reset];
}
- (NSData *)data
{
if (_data != nil)
return _data;
else if (_dataPath != nil)
return TGPaintGZipInflate([[NSData alloc] initWithContentsOfFile:_dataPath]);
else
return nil;
}
- (UIImage *)image
{
@ -128,7 +113,7 @@
- (UIImage *)stillImage
{
if (_stillImage != nil)
return _stillImage;
return _stillImage;
else if (_stillImageRetrievalBlock != nil)
return _stillImageRetrievalBlock();
else
@ -137,23 +122,14 @@
- (NSArray *)stickers
{
NSMutableSet *stickers = [[NSMutableSet alloc] init];
for (TGPhotoPaintEntity *entity in self.entities)
{
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
[stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
}
return [stickers allObjects];
}
- (bool)hasAnimation
{
for (TGPhotoPaintEntity *entity in self.entities)
{
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]] && ((TGPhotoPaintStickerEntity *)entity).animated)
return true;
}
return false;
return @[];
// NSMutableSet *stickers = [[NSMutableSet alloc] init];
// for (TGPhotoPaintEntity *entity in self.entities)
// {
// if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
// [stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
// }
// return [stickers allObjects];
}
- (BOOL)isEqual:(id)object
@ -165,7 +141,7 @@
return false;
TGPaintingData *data = (TGPaintingData *)object;
return [data.entities isEqual:self.entities] && ((data.data != nil && [data.data isEqualToData:self.data]) || (data.data == nil && self.data == nil));
return [data.entitiesData isEqual:self.entitiesData] && ((data.drawingData != nil && [data.drawingData isEqualToData:self.drawingData]) || (data.drawingData == nil && self.drawingData == nil));
}
+ (SQueue *)queue

View file

@ -9,7 +9,6 @@
#import "TGPhotoEditorInterfaceAssets.h"
#import "PGPhotoEditorView.h"
#import "TGPhotoEntitiesContainerView.h"
const CGFloat TGPhotoAvatarCropViewOverscreenSize = 1000;
const CGFloat TGPhotoAvatarCropViewCurtainSize = 300;
@ -46,13 +45,13 @@ const CGFloat TGPhotoAvatarCropViewCurtainMargin = 200;
__weak PGPhotoEditorView *_fullPreviewView;
__weak UIImageView *_fullPaintingView;
__weak TGPhotoEntitiesContainerView *_fullEntitiesView;
__weak UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
}
@end
@implementation TGPhotoAvatarCropView
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView<TGPhotoDrawingEntitiesView> *)fullEntitiesView square:(bool)square
{
self = [super initWithFrame:CGRectZero];
if (self != nil)

View file

@ -3,7 +3,6 @@
@class PGPhotoEditor;
@class PGPhotoTool;
@class TGPhotoEditorPreviewView;
@class TGPhotoEntitiesContainerView;
@class PGPhotoEditorView;
@class TGMediaPickerGalleryVideoScrubber;
@ -23,12 +22,12 @@
@property (nonatomic, weak) UIView *dotMarkerView;
@property (nonatomic, weak) PGPhotoEditorView *fullPreviewView;
@property (nonatomic, weak) UIImageView *fullPaintingView;
@property (nonatomic, weak) TGPhotoEntitiesContainerView *fullEntitiesView;
@property (nonatomic, weak) UIView<TGPhotoDrawingEntitiesView> *fullEntitiesView;
@property (nonatomic, weak) TGMediaPickerGalleryVideoScrubber *scrubberView;
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion senderName:(NSString *)senderName;
- (void)setImage:(UIImage *)image;
- (void)setSnapshotImage:(UIImage *)snapshotImage;

View file

@ -16,11 +16,11 @@
#import "TGMediaPickerGalleryVideoScrubber.h"
#import "TGModernGalleryVideoView.h"
#import "TGPhotoEntitiesContainerView.h"
#import "TGPhotoPaintStickersContext.h"
#import "TGPhotoPaintController.h"
#import "TGPhotoDrawingController.h"
const CGFloat TGPhotoAvatarPreviewPanelSize = 96.0f;
const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanelSize + 40.0f;
@ -48,6 +48,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
TGModernButton *_cancelButton;
UILabel *_titleLabel;
UILabel *_subtitleLabel;
UIView<TGPhotoSolidRoundedButtonView> *_doneButton;
bool _wasPlayingBeforeCropping;
@ -56,6 +57,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
bool _isForum;
bool _isSuggestion;
NSString *_senderName;
}
@property (nonatomic, weak) PGPhotoEditor *photoEditor;
@ -65,7 +67,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
@implementation TGPhotoAvatarPreviewController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion {
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion senderName:(NSString *)senderName {
self = [super initWithContext:context];
if (self != nil)
{
@ -73,6 +75,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
self.previewView = previewView;
_isForum = isForum;
_isSuggestion = isSuggestion;
_senderName = senderName;
}
return self;
}
@ -181,7 +184,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_coverLabel.backgroundColor = [UIColor clearColor];
_coverLabel.font = TGSystemFontOfSize(14.0f);
_coverLabel.textColor = [UIColor whiteColor];
_coverLabel.text = TGLocalized(@"PhotoEditor.SelectCoverFrame");
_coverLabel.text = _isSuggestion ? TGLocalized(@"PhotoEditor.SelectCoverFrameSuggestion") : TGLocalized(@"PhotoEditor.SelectCoverFrame");
[_coverLabel sizeToFit];
[_portraitToolsWrapperView addSubview:_coverLabel];
@ -193,10 +196,19 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_titleLabel.backgroundColor = [UIColor clearColor];
_titleLabel.font = TGBoldSystemFontOfSize(17.0f);
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.text = self.item.isVideo ? TGLocalized(@"PhotoEditor.SetProfileVideo") : TGLocalized(@"PhotoEditor.SetProfilePhoto");
_titleLabel.text = self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoTitle") : TGLocalized(@"Conversation.SuggestedPhotoTitle");
[_titleLabel sizeToFit];
[_wrapperView addSubview:_titleLabel];
_subtitleLabel = [[UILabel alloc] init];
_subtitleLabel.backgroundColor = [UIColor clearColor];
_subtitleLabel.font = TGSystemFontOfSize(14.0f);
_subtitleLabel.textColor = [UIColor whiteColor];
_subtitleLabel.numberOfLines = 2;
_subtitleLabel.textAlignment = NSTextAlignmentCenter;
_subtitleLabel.text = [NSString stringWithFormat:self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoText") : TGLocalized(@"Conversation.SuggestedPhotoText"), _senderName];
[_wrapperView addSubview:_subtitleLabel];
if (!self.item.isVideo) {
_cancelButton = [[TGModernButton alloc] init];
[_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal];
@ -369,15 +381,17 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
[_cropView animateTransitionIn];
_cancelButton.alpha = 0.0;
_titleLabel.alpha = 0.0;
_doneButton.alpha = 0.0;
_cancelButton.alpha = 0.0f;
_titleLabel.alpha = 0.0f;
_subtitleLabel.alpha = 0.0f;
_doneButton.alpha = 0.0f;
[UIView animateWithDuration:0.3f animations:^
{
_cancelButton.alpha = 1.0;
_titleLabel.alpha = 1.0;
_doneButton.alpha = 1.0;
_cancelButton.alpha = 1.0f;
_titleLabel.alpha = 1.0f;
_subtitleLabel.alpha = 1.0f;
_doneButton.alpha = 1.0f;
_portraitToolsWrapperView.alpha = 1.0f;
_landscapeToolsWrapperView.alpha = 1.0f;
@ -468,7 +482,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
if (self.switchingToTab == TGPhotoEditorPaintTab)
{
containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
}
CGSize fittedSize = TGScaleToSize(cropRectFrame.size, containerFrame.size);
@ -551,9 +565,10 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_landscapeToolsWrapperView.alpha = 0.0f;
_dotImageView.alpha = 0.0f;
_dotMarkerView.alpha = 0.0f;
_cancelButton.alpha = 0.0;
_titleLabel.alpha = 0.0;
_doneButton.alpha = 0.0;
_cancelButton.alpha = 0.0f;
_titleLabel.alpha = 0.0f;
_subtitleLabel.alpha = 0.0f;
_doneButton.alpha = 0.0f;
} completion:^(__unused BOOL finished)
{
if (!switching) {
@ -670,8 +685,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_landscapeToolsWrapperView.alpha = 0.0f;
_dotImageView.alpha = 0.0f;
_titleLabel.alpha = 0.0f;
_subtitleLabel.alpha = 0.0f;
_cancelButton.alpha = 0.0f;
_doneButton.alpha = 0.0;
_doneButton.alpha = 0.0f;
} completion:nil];
}
@ -804,6 +820,8 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
CGSize buttonSize = CGSizeMake(MIN(referenceSize.width, referenceSize.height) - 16.0 * 2.0, 50.0f);
[_doneButton updateWidth:buttonSize.width];
CGSize subtitleSize = [_subtitleLabel sizeThatFits:CGSizeMake(referenceSize.width - 48.0, referenceSize.height)];
switch (orientation)
{
case UIInterfaceOrientationLandscapeLeft:
@ -820,6 +838,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height);
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
@ -841,6 +860,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height);
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
@ -867,6 +887,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_coverLabel.frame = CGRectMake(floor((_portraitToolsWrapperView.frame.size.width - _coverLabel.frame.size.width) / 2.0), CGRectGetMaxY(_scrubberView.frame) + 6.0, _coverLabel.frame.size.width, _coverLabel.frame.size.height);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), screenEdges.top + floor((44.0 - _titleLabel.frame.size.height) / 2.0), _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height - subtitleSize.height - 8.0, subtitleSize.width, subtitleSize.height);
_cancelButton.frame = CGRectMake(screenEdges.left + 16.0, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);

View file

@ -12,8 +12,18 @@
@property (nonatomic, copy) void (^requestDismiss)(void);
@property (nonatomic, copy) void (^requestApply)(void);
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
- (TGPaintingData *)paintingData;
+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
+ (CGSize)maximumPaintingSize;
@end
extern const CGFloat TGPhotoPaintTopPanelSize;
extern const CGFloat TGPhotoPaintBottomPanelSize;

View file

@ -1,5 +1,4 @@
#import "TGPhotoDrawingController.h"
#import "TGPhotoPaintController.h"
#import "LegacyComponentsInternal.h"
@ -21,17 +20,18 @@
#import "TGPaintingWrapperView.h"
#import "TGPhotoEditorSparseView.h"
#import "TGPhotoEntitiesContainerView.h"
#import "PGPhotoEditor.h"
#import "TGPhotoEditorPreviewView.h"
#import "TGPaintCanvas.h"
#import "TGPainting.h"
const CGFloat TGPhotoPaintTopPanelSize = 44.0f;
const CGFloat TGPhotoPaintBottomPanelSize = 79.0f;
const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f };
const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
@interface TGPhotoDrawingController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
{
id<LegacyComponentsContext> _context;
@ -76,7 +76,7 @@
@implementation TGPhotoDrawingController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
{
self = [super initWithContext:context];
if (self != nil)
@ -84,8 +84,8 @@
_context = context;
_stickersContext = stickersContext;
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
_drawingAdapter = [_stickersContext drawingAdapter:size];
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]);
_drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize];
_interfaceController = (UIViewController<TGPhotoDrawingInterfaceController> *)_drawingAdapter.interfaceController;
__weak TGPhotoDrawingController *weakSelf = self;
@ -218,6 +218,16 @@
[self.view setNeedsLayout];
}
- (void)viewDidLoad
{
[super viewDidLoad];
PGPhotoEditor *photoEditor = _photoEditor;
if (!_skipEntitiesSetup) {
[_entitiesView setupWithEntitiesData:photoEditor.paintingData.entitiesData];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
@ -292,11 +302,11 @@
- (CGSize)fittedContentSize
{
return [TGPhotoPaintController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize];
return [TGPhotoDrawingController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize];
}
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize {
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
CGFloat scale = fittedOriginalSize.width / originalSize.width;
CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale);
@ -308,11 +318,11 @@
- (CGRect)fittedCropRect:(bool)originalSize
{
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize];
return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize];
}
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize {
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
CGFloat scale = fittedOriginalSize.width / originalSize.width;
CGSize size = fittedOriginalSize;
@ -324,7 +334,7 @@
- (CGPoint)fittedCropCenterScale:(CGFloat)scale
{
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect centerScale:scale];
return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect centerScale:scale];
}
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale
@ -488,7 +498,7 @@
- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame
{
CGSize referenceSize = [self referenceViewSize];
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size);
CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
@ -518,7 +528,7 @@
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
_entitiesView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation);
CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]);
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation);
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
@ -554,7 +564,7 @@
- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation
{
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size);
return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
@ -571,7 +581,7 @@
[previewView prepareForTransitionOut];
UIInterfaceOrientation orientation = self.effectiveOrientation;
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height);
CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y);
previewView.frame = rect;
@ -745,7 +755,7 @@
CGSize referenceSize = [self referenceViewSize];
CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize;
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGFloat topInset = [self controllerStatusBarHeight] + 31.0;
CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset;
@ -800,7 +810,7 @@
screenEdges.bottom -= safeAreaInset.bottom;
screenEdges.right -= safeAreaInset.right;
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
PGPhotoEditor *photoEditor = self.photoEditor;
TGPhotoEditorPreviewView *previewView = self.previewView;
@ -880,4 +890,19 @@
return UIRectEdgeTop | UIRectEdgeBottom;
}
+ (CGSize)maximumPaintingSize
{
static dispatch_once_t onceToken;
static CGSize size;
dispatch_once(&onceToken, ^
{
CGSize screenSize = TGScreenSize();
if ((NSInteger)screenSize.height == 480)
size = TGPhotoPaintingLightMaxSize;
else
size = TGPhotoPaintingMaxSize;
});
return size;
}
@end

View file

@ -29,7 +29,6 @@
#import "TGPhotoToolbarView.h"
#import "TGPhotoEditorPreviewView.h"
#import "TGPhotoEntitiesContainerView.h"
#import <LegacyComponents/TGMenuView.h>
@ -38,7 +37,6 @@
#import "TGPhotoCropController.h"
#import "TGPhotoToolsController.h"
#import "TGPhotoPaintController.h"
#import "TGPhotoDrawingController.h"
#import "TGPhotoQualityController.h"
#import "TGPhotoAvatarPreviewController.h"
@ -71,7 +69,7 @@
TGPhotoToolbarView *_landscapeToolbarView;
TGPhotoEditorPreviewView *_previewView;
PGPhotoEditorView *_fullPreviewView;
TGPhotoEntitiesContainerView *_fullEntitiesView;
UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
UIImageView *_fullPaintingView;
PGPhotoEditor *_photoEditor;
@ -288,10 +286,7 @@
case TGPhotoEditorPaintTab:
case TGPhotoEditorEraserTab:
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoPaintController class]])
[strongSelf->_currentTabController handleTabAction:tab];
else
[strongSelf presentTab:TGPhotoEditorPaintTab];
[strongSelf presentTab:TGPhotoEditorPaintTab];
break;
case TGPhotoEditorStickerTab:
@ -353,9 +348,9 @@
_fullPaintingView = [[UIImageView alloc] init];
_fullPaintingView.frame = _fullPreviewView.frame;
_fullEntitiesView = [[TGPhotoEntitiesContainerView alloc] init];
_fullEntitiesView = [_stickersContext drawingEntitiesViewWithSize:CGSizeMake(0, 0)];
_fullEntitiesView.userInteractionEnabled = false;
CGRect rect = [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true];
CGRect rect = [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true];
_fullEntitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
}
@ -1341,7 +1336,7 @@
{
bool skipInitialTransition = (![self presentedFromCamera] && self.navigationController != nil) || self.skipInitialTransition;
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar]];
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar] senderName:self.senderName];
cropController.stickersContext = _stickersContext;
cropController.scrubberView = _scrubberView;
cropController.dotImageView = _dotImageView;

View file

@ -173,10 +173,10 @@
- (void)setupWithPaintingData:(TGPaintingData *)paintingData {
[self removeAll];
for (TGPhotoPaintEntity *entity in paintingData.entities) {
UIView * entityView = [self createEntityViewWithEntity:entity];
[self addSubview:entityView];
}
// for (TGPhotoPaintEntity *entity in paintingData.entities) {
// UIView * entityView = [self createEntityViewWithEntity:entity];
// [self addSubview:entityView];
// }
}
- (TGPhotoPaintEntityView *)createEntityViewWithEntity:(TGPhotoPaintEntity *)entity {

View file

@ -1,28 +1,28 @@
#import "TGPhotoEditorTabController.h"
#import <LegacyComponents/LegacyComponentsContext.h>
@class PGPhotoEditor;
@class TGPhotoEditorPreviewView;
@protocol TGPhotoPaintStickersContext;
@interface TGPhotoPaintController : TGPhotoEditorTabController
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
- (TGPaintingData *)paintingData;
+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
+ (CGSize)maximumPaintingSize;
@end
extern const CGFloat TGPhotoPaintTopPanelSize;
extern const CGFloat TGPhotoPaintBottomPanelSize;
//#import "TGPhotoEditorTabController.h"
//
//#import <LegacyComponents/LegacyComponentsContext.h>
//
//@class PGPhotoEditor;
//@class TGPhotoEditorPreviewView;
//
//@protocol TGPhotoPaintStickersContext;
//
//@interface TGPhotoPaintController : TGPhotoEditorTabController
//
//@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
//
//- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
//
//- (TGPaintingData *)paintingData;
//
//+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
//
//+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
//+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
//+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
//+ (CGSize)maximumPaintingSize;
//
//@end
//
//extern const CGFloat TGPhotoPaintTopPanelSize;
//extern const CGFloat TGPhotoPaintBottomPanelSize;

File diff suppressed because it is too large Load diff

View file

@ -3,10 +3,10 @@
@class PGPhotoEditor;
@class PGPhotoTool;
@class TGPhotoEditorPreviewView;
@class TGPhotoEntitiesContainerView;
@protocol TGPhotoDrawingEntitiesView;
@interface TGPhotoToolsController : TGPhotoEditorTabController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView;
@end

View file

@ -22,9 +22,8 @@
#import "TGPhotoEditorPreviewView.h"
#import "TGPhotoEditorHUDView.h"
#import "TGPhotoEditorSparseView.h"
#import "TGPhotoEntitiesContainerView.h"
#import "TGPhotoPaintController.h"
#import "TGPhotoDrawingController.h"
const CGFloat TGPhotoEditorToolsPanelSize = 180.0f;
const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize + 40.0f;
@ -48,7 +47,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
TGPhotoEditorCollectionView *_portraitCollectionView;
TGPhotoEditorCollectionView *_landscapeCollectionView;
TGPhotoEditorHUDView *_hudView;
TGPhotoEntitiesContainerView *_entitiesView;
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
void (^_changeBlock)(PGPhotoTool *, id, bool);
void (^_interactionBegan)(void);
@ -71,7 +70,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
@implementation TGPhotoToolsController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView
{
self = [super initWithContext:context];
if (self != nil)

View file

@ -12,7 +12,7 @@
@implementation TGPhotoVideoEditor
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
{
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
@ -35,6 +35,7 @@
void (^present)(UIImage *) = ^(UIImage *screenImage) {
TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSuggestedAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab];
controller.senderName = senderName;
controller.stickersContext = stickersContext;
TGMediaAvatarEditorTransition *transition;

View file

@ -76,33 +76,8 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
}
if (dictionary[@"originalSize"])
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
if (dictionary[@"entities"]) {
NSMutableArray *entities = [[NSMutableArray alloc] init];
for (NSDictionary *dict in dictionary[@"entities"]) {
if ([dict[@"type"] isEqualToString:@"sticker"]) {
TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:dict[@"data"] baseSize:[dict[@"baseSize"] CGSizeValue] animated:[dict[@"animated"] boolValue]];
entity.uuid = [dict[@"uuid"] integerValue];
entity.position = [dict[@"position"] CGPointValue];
entity.scale = [dict[@"scale"] floatValue];
entity.angle = [dict[@"angle"] floatValue];
entity.mirrored = [dict[@"mirrored"] boolValue];
[entities addObject:entity];
} else if ([dict[@"type"] isEqualToString:@"text"]) {
UIImage *renderImage = [[UIImage alloc] initWithData:dict[@"data"]];
if (renderImage != nil) {
TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:nil font:nil swatch:nil baseFontSize:0.0 maxWidth:0.0 style:TGPhotoPaintTextEntityStyleRegular];
entity.uuid = [dict[@"uuid"] integerValue];
entity.position = [dict[@"position"] CGPointValue];
entity.scale = [dict[@"scale"] floatValue];
entity.angle = [dict[@"angle"] floatValue];
entity.renderImage = renderImage;
[entities addObject:entity];
}
}
}
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entities:entities];
if (dictionary[@"entitiesData"]) {
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"]];
} else if (dictionary[@"paintingImagePath"]) {
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
}
@ -240,42 +215,8 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
if (self.paintingData.imagePath != nil) {
dict[@"paintingImagePath"] = self.paintingData.imagePath;
}
NSMutableArray *entities = [[NSMutableArray alloc] init];
if (self.paintingData.entities != nil) {
for (TGPhotoPaintEntity *entity in self.paintingData.entities) {
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) {
TGPhotoPaintStickerEntity *stickerEntity = (TGPhotoPaintStickerEntity *)entity;
NSMutableDictionary *sticker = [[NSMutableDictionary alloc] init];
sticker[@"type"] = @"sticker";
sticker[@"baseSize"] = [NSValue valueWithCGSize:stickerEntity.baseSize];
sticker[@"uuid"] = @(stickerEntity.uuid);
sticker[@"data"] = stickerEntity.document;
sticker[@"position"] = [NSValue valueWithCGPoint:stickerEntity.position];
sticker[@"scale"] = @(stickerEntity.scale);
sticker[@"angle"] = @(stickerEntity.angle);
sticker[@"mirrored"] = @(stickerEntity.mirrored);
sticker[@"animated"] = @(stickerEntity.animated);
[entities addObject:sticker];
} else if ([entity isKindOfClass:[TGPhotoPaintTextEntity class]]) {
TGPhotoPaintTextEntity *textEntity = (TGPhotoPaintTextEntity *)entity;
NSMutableDictionary *text = [[NSMutableDictionary alloc] init];
if (textEntity.renderImage != nil) {
text[@"type"] = @"text";
text[@"uuid"] = @(textEntity.uuid);
text[@"data"] = UIImagePNGRepresentation(textEntity.renderImage);
text[@"position"] = [NSValue valueWithCGPoint:textEntity.position];
text[@"scale"] = @(textEntity.scale);
text[@"angle"] = @(textEntity.angle);
[entities addObject:text];
}
}
}
}
if (entities.count > 0) {
dict[@"entities"] = entities;
if (self.paintingData.entitiesData != nil) {
dict[@"entitiesData"] = self.paintingData.entitiesData;
}
}

View file

@ -53,7 +53,7 @@ public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, t
}
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, senderName: String? = nil, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media)
|> deliverOnMainQueue).start(next: { (value, isImage) in
guard case let .data(data) = value, data.complete else {
@ -93,7 +93,7 @@ public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, didFinishWithImage: { image in
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, senderName: senderName, didFinishWithImage: { image in
if let image = image {
imageCompletion(image)
}

View file

@ -290,18 +290,18 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
self.originalSize = adjustments.originalSize
self.cropRect = adjustments.cropRect.isEmpty ? nil : adjustments.cropRect
var entities: [LegacyPaintEntity] = []
if let paintingData = adjustments.paintingData, let paintingEntities = paintingData.entities {
for paintingEntity in paintingEntities {
if let sticker = paintingEntity as? TGPhotoPaintStickerEntity {
if let account = account, let entity = LegacyPaintStickerEntity(account: account, entity: sticker) {
entities.append(entity)
}
} else if let text = paintingEntity as? TGPhotoPaintTextEntity {
entities.append(LegacyPaintTextEntity(entity: text))
}
}
}
let entities: [LegacyPaintEntity] = []
// if let paintingData = adjustments.paintingData, let paintingEntities = paintingData.entities {
// for paintingEntity in paintingEntities {
// if let sticker = paintingEntity as? TGPhotoPaintStickerEntity {
// if let account = account, let entity = LegacyPaintStickerEntity(account: account, entity: sticker) {
// entities.append(entity)
// }
// } else if let text = paintingEntity as? TGPhotoPaintTextEntity {
// entities.append(LegacyPaintTextEntity(entity: text))
// }
// }
// }
self.entities = entities
super.init()
@ -482,8 +482,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
let contentWrapperView: UIView!
let interfaceController: TGPhotoDrawingInterfaceController!
init(context: AccountContext, size: CGSize) {
let interfaceController = DrawingScreen(context: context, size: size)
init(context: AccountContext, size: CGSize, originalSize: CGSize) {
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize)
self.interfaceController = interfaceController
self.drawingView = interfaceController.drawingView
self.drawingEntitiesView = interfaceController.entitiesView
@ -494,8 +494,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
}
}
public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! {
return LegacyDrawingAdapter(context: self.context, size: size)
public func drawingAdapter(_ size: CGSize, originalSize: CGSize) -> TGPhotoDrawingAdapter! {
return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize)
}
public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! {
@ -504,6 +504,11 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
button.pressed = action
return button
}
public func drawingEntitiesView(with size: CGSize) -> (UIView & TGPhotoDrawingEntitiesView)! {
let view = DrawingEntitiesView(context: self.context, size: size)
return view
}
}
extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView {

View file

@ -13,7 +13,7 @@ public extension EnginePeer {
} else if let lastName = user.lastName, !lastName.isEmpty {
return lastName
} else if let _ = user.phone {
return ""// formatPhoneNumber("+\(phone)")
return "" //formatPhoneNumber("+\(phone)")
} else {
return ""
}
@ -46,7 +46,7 @@ public extension EnginePeer {
} else if let lastName = user.lastName, !lastName.isEmpty {
return lastName
} else if let _ = user.phone {
return ""//formatPhoneNumber("+\(phone)")
return "" //formatPhoneNumber("+\(phone)")
} else {
return strings.User_DeletedAccount
}

View file

@ -1355,13 +1355,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
if let undoOverlayController = strongSelf.undoOverlayController {
undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, undo: true)
undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true)
} else {
var elevatedLayout = true
if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass {
elevatedLayout = false
}
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in
guard let strongSelf = self else {
return true
}

View file

@ -364,6 +364,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
private let context: AccountContext
private let peer: Peer
private let sourceCorners: SourceCorners
private let isSuggested: Bool
private var presentationData: PresentationData
@ -403,10 +404,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
private let editDisposable = MetaDisposable ()
public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, synchronousLoad: Bool = false) {
public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, isSuggested: Bool = false, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, synchronousLoad: Bool = false) {
self.context = context
self.peer = peer
self.sourceCorners = sourceCorners
self.isSuggested = isSuggested
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.replaceRootController = replaceRootController
@ -460,6 +462,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
if strongSelf.centralEntryIndex == nil {
strongSelf.centralEntryIndex = 0
}
if strongSelf.isSuggested, let firstEntry = entries.first {
strongSelf.navigationItem.title = !firstEntry.videoRepresentations.isEmpty ? strongSelf.presentationData.strings.Conversation_SuggestedVideoTitle : strongSelf.presentationData.strings.Conversation_SuggestedPhotoTitle
}
if strongSelf.isViewLoaded {
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? {
self?.deleteEntry(entry)

View file

@ -532,7 +532,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
var highlightedSide: Bool?
public let stripContainerNode: ASDisplayNode
public let highlightContainerNode: ASDisplayNode
private let setByYouNode: ImmediateTextNode
public let setByYouNode: ImmediateTextNode
private let setByYouImageNode: ImageNode
private var setByYouTapRecognizer: UITapGestureRecognizer?
@ -883,7 +883,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
self.currentIndexUpdated?()
}
if let size = self.validLayout {
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring))
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
}
}

View file

@ -152,13 +152,8 @@ public final class QrCodeScanScreen: ViewController {
}
@objc private func cancelPressed() {
guard let layout = self.validLayout else {
return
}
self.completion(nil)
self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
self.dismiss()
})
self.dismissAnimated()
}
@objc private func myCodePressed() {
@ -210,12 +205,20 @@ public final class QrCodeScanScreen: ViewController {
}
}
public func dismissAnimated() {
guard let layout = self.validLayout else {
return
}
self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
self.dismiss()
})
}
private func completeWithCode(_ code: String) {
guard case .custom = self.subject else {
return
}
self.completion(code)
self.dismiss()
}
override public func loadDisplayNode() {

View file

@ -152,185 +152,3 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
return controller
}
//public final class ChangePhoneNumberController: ViewController, MFMailComposeViewControllerDelegate {
// private var controllerNode: ChangePhoneNumberControllerNode {
// return self.displayNode as! ChangePhoneNumberControllerNode
// }
//
// private let context: AccountContext
//
// private var currentData: (Int32, String?, String)?
// private let requestDisposable = MetaDisposable()
//
// var inProgress: Bool = false {
// didSet {
// if self.inProgress {
// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
// self.navigationItem.rightBarButtonItem = item
// } else {
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
// }
// self.controllerNode.inProgress = self.inProgress
// }
// }
// var loginWithNumber: ((String) -> Void)?
//
// private let hapticFeedback = HapticFeedback()
//
// private var presentationData: PresentationData
//
// public init(context: AccountContext) {
// self.context = context
// self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
//
// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
//
// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
// self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
//
// self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
// self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
// }
//
// required init(coder aDecoder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// deinit {
// self.requestDisposable.dispose()
// }
//
// func updateData(countryCode: Int32, countryName: String, number: String) {
// if self.currentData == nil || self.currentData! != (countryCode, countryName, number) {
// self.currentData = (countryCode, countryName, number)
// if self.isNodeLoaded {
// self.controllerNode.codeAndNumber = (countryCode, countryName, number)
// }
// }
// }
//
// override public func loadDisplayNode() {
// self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData)
// self.displayNodeDidLoad()
// self.controllerNode.selectCountryCode = { [weak self] in
// if let strongSelf = self {
// let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme)
// controller.completeWithCountryCode = { code, name in
// if let strongSelf = self {
// strongSelf.updateData(countryCode: Int32(code), countryName: name, number: strongSelf.controllerNode.codeAndNumber.2)
// strongSelf.controllerNode.activateInput()
// }
// }
// strongSelf.controllerNode.view.endEditing(true)
// strongSelf.push(controller)
// }
// }
//
// loadServerCountryCodes(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine, completion: { [weak self] in
// if let strongSelf = self {
// strongSelf.controllerNode.updateCountryCode()
// }
// })
//
// self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
// }
//
// override public func viewWillAppear(_ animated: Bool) {
// super.viewWillAppear(animated)
//
// self.controllerNode.activateInput()
// }
//
// override public func viewDidAppear(_ animated: Bool) {
// super.viewDidAppear(animated)
//
// self.controllerNode.activateInput()
// }
//
// override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
// super.containerLayoutUpdated(layout, transition: transition)
//
// self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
// }
//
// @objc func nextPressed() {
// let (code, _, number) = self.controllerNode.codeAndNumber
// var phoneNumber = number
// if let code = code {
// phoneNumber = "\(code)\(phoneNumber)"
// }
// if !number.isEmpty {
// self.inProgress = true
// self.requestDisposable.set((self.context.engine.accountData.requestChangeAccountPhoneNumberVerification(phoneNumber: self.controllerNode.currentNumber) |> deliverOnMainQueue).start(next: { [weak self] next in
// if let strongSelf = self {
// strongSelf.inProgress = false
// (strongSelf.navigationController as? NavigationController)?.pushViewController(changePhoneNumberCodeController(context: strongSelf.context, phoneNumber: strongSelf.controllerNode.currentNumber, codeData: next))
// }
// }, error: { [weak self] error in
// if let strongSelf = self {
// strongSelf.inProgress = false
//
// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
//
// let text: String
// var actions: [TextAlertAction] = []
// switch error {
// case .limitExceeded:
// text = presentationData.strings.Login_CodeFloodError
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
// case .invalidPhoneNumber:
// text = presentationData.strings.Login_InvalidPhoneError
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
// case .phoneNumberOccupied:
// text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(context: strongSelf.context, number: phoneNumber)).string
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
// case .phoneBanned:
// text = presentationData.strings.Login_PhoneBannedError
// actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
// actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
// guard let strongSelf = self else {
// return
// }
// let formattedNumber = formatPhoneNumber(context: strongSelf.context, number: number)
// let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
// let systemVersion = UIDevice.current.systemVersion
// let locale = Locale.current.identifier
// let carrier = CTCarrier()
// let mnc = carrier.mobileNetworkCode ?? "none"
//
// strongSelf.presentEmailComposeController(address: "login@stel.com", subject: presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string)
// }))
// case .generic:
// text = presentationData.strings.Login_UnknownError
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
// }
//
// strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
// }
// }))
// } else {
// self.hapticFeedback.error()
// self.controllerNode.animateError()
// }
// }
//
// private func presentEmailComposeController(address: String, subject: String, body: String) {
// if MFMailComposeViewController.canSendMail() {
// let composeController = MFMailComposeViewController()
// composeController.setToRecipients([address])
// composeController.setSubject(subject)
// composeController.setMessageBody(body, isHTML: false)
// composeController.mailComposeDelegate = self
//
// self.view.window?.rootViewController?.present(composeController, animated: true, completion: nil)
// } else {
// self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
// }
// }
//
// public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
// controller.dismiss(animated: true, completion: nil)
// }
//}

View file

@ -44,14 +44,14 @@ public enum PrivacyIntroControllerMode {
}
}
func title(strings: PresentationStrings) -> String {
func title(context: AccountContext, strings: PresentationStrings) -> String {
switch self {
case .passcode:
return strings.PasscodeSettings_Title
case .twoStepVerification:
return strings.TwoStepAuth_AdditionalPassword
case let .changePhoneNumber(phoneNumber):
return formatPhoneNumber(phoneNumber)
return formatPhoneNumber(context: context, number: phoneNumber)
}
}

View file

@ -118,7 +118,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
if self.animationNode.isHidden {
self.iconNode.image = self.mode.icon(theme: presentationData.theme)
}
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(context: self.context, strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.mode.text(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
self.noticeNode.attributedText = NSAttributedString(string: self.mode.notice(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
self.buttonTextNode.attributedText = NSAttributedString(string: self.mode.buttonTitle(strings: presentationData.strings), font: buttonFont, textColor: presentationData.theme.list.itemAccentColor, paragraphAlignment: .center)

View file

@ -11,6 +11,7 @@ import PresentationDataUtils
import AccountContext
import UndoUI
import ItemListPeerActionItem
import AvatarNode
enum SelectivePrivacySettingsKind {
case presence
@ -414,8 +415,8 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.setPublicPhoto?()
})
case let .removePublicPhoto(theme, text, _, _):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
case let .removePublicPhoto(_, text, peer, image):
return ItemListPeerActionItem(presentationData: presentationData, icon: nil, iconSignal: peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.removePublicPhoto?()
})
case let .publicPhotoInfo(_, text):
@ -676,10 +677,10 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
}
if case .profilePhoto = kind, let peer = peer {
if case .profilePhoto = kind, let peer = peer, state.setting != .everybody {
if let publicPhoto = publicPhoto {
entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_UpdatePublicPhoto))
entries.append(.removePublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_RemovePublicPhoto, peer, publicPhoto))
entries.append(.removePublicPhoto(presentationData.theme, !publicPhoto.videoRepresentations.isEmpty ? presentationData.strings.Privacy_ProfilePhoto_RemovePublicVideo : presentationData.strings.Privacy_ProfilePhoto_RemovePublicPhoto, peer, publicPhoto))
} else {
entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_SetPublicPhoto))
}

View file

@ -699,13 +699,6 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
return entries
}
public enum InstalledStickerPacksControllerMode {
case general
case modal
case masks
case emoji
}
public func installedStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, archivedPacks: [ArchivedStickerPackItem]? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void = { _ in }, focusOnItemTag: InstalledStickerPacksEntryTag? = nil) -> ViewController {
let initialState = InstalledStickerPacksControllerState().withUpdatedEditing(mode == .modal).withUpdatedSelectedPackIds(mode == .modal ? Set() : nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)

View file

@ -539,7 +539,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode {
return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue()))
|> then(
requestContextResults(context: context, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16)
requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16)
|> map { results -> ChatContextResultCollection? in
return results?.results
}

View file

@ -535,3 +535,48 @@ extension ChatContextResultCollection {
}
}
}
public func requestContextResults(engine: TelegramEngine, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal<RequestChatContextResultsResult?, NoError> {
return engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|> `catch` { error -> Signal<RequestChatContextResultsResult?, NoError> in
return .single(nil)
}
|> mapToSignal { resultsStruct -> Signal<RequestChatContextResultsResult?, NoError> in
let results = resultsStruct?.results
var collection = existingResults
var updated: Bool = false
if let existingResults = existingResults, let results = results {
var newResults: [ChatContextResult] = []
var existingIds = Set<String>()
for result in existingResults.results {
newResults.append(result)
existingIds.insert(result.id)
}
for result in results.results {
if !existingIds.contains(result.id) {
newResults.append(result)
existingIds.insert(result.id)
updated = true
}
}
collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, results: newResults, cacheTimeout: existingResults.cacheTimeout)
} else {
collection = results
updated = true
}
if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated {
let nextResults = requestContextResults(engine: engine, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit)
if collection.results.count > 10 {
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
|> then(nextResults)
} else {
return nextResults
}
} else if let collection = collection {
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
} else {
return .single(nil)
}
}
}

View file

@ -337,6 +337,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
@ -348,8 +349,11 @@ swift_library(
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatInputNode",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View file

@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatControllerInteraction",
module_name = "ChatControllerInteraction",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TextSelectionNode:TextSelectionNode",
"//submodules/ContextUI:ContextUI",
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/UndoUI:UndoUI",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TextFormat:TextFormat",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
],
visibility = [
"//visibility:public",
],
)

View file

@ -15,33 +15,28 @@ import UndoUI
import TelegramPresentationData
import ChatPresentationInterfaceState
import TextFormat
import WallpaperBackgroundNode
import AnimationCache
import MultiAnimationRenderer
struct ChatInterfaceHighlightedState: Equatable {
let messageStableId: UInt32
public struct ChatInterfaceHighlightedState: Equatable {
public let messageStableId: UInt32
static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool {
public init(messageStableId: UInt32) {
self.messageStableId = messageStableId
}
public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool {
return lhs.messageStableId == rhs.messageStableId
}
}
struct ChatInterfaceStickerSettings: Equatable {
let loopAnimatedStickers: Bool
public struct ChatInterfacePollActionState: Equatable {
public var pollMessageIdsInProgress: [MessageId: [Data]] = [:]
public init(loopAnimatedStickers: Bool) {
self.loopAnimatedStickers = loopAnimatedStickers
public init(pollMessageIdsInProgress: [MessageId: [Data]] = [:]) {
self.pollMessageIdsInProgress = pollMessageIdsInProgress
}
public init(stickerSettings: StickerSettings) {
self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers
}
static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool {
return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers
}
}
struct ChatInterfacePollActionState: Equatable {
var pollMessageIdsInProgress: [MessageId: [Data]] = [:]
}
public enum ChatControllerInteractionSwipeAction {
@ -54,128 +49,150 @@ public enum ChatControllerInteractionReaction {
case reaction(MessageReaction.Reaction)
}
struct UnreadMessageRangeKey: Hashable {
var peerId: PeerId
var namespace: MessageId.Namespace
public struct UnreadMessageRangeKey: Hashable {
public var peerId: PeerId
public var namespace: MessageId.Namespace
public init(peerId: PeerId, namespace: MessageId.Namespace) {
self.peerId = peerId
self.namespace = namespace
}
}
public class ChatPresentationContext {
public weak var backgroundNode: WallpaperBackgroundNode?
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
self.backgroundNode = backgroundNode
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
}
}
public protocol ChatMessageTransitionProtocol: ASDisplayNode {
}
public final class ChatControllerInteraction {
enum OpenPeerSource {
public enum OpenPeerSource {
case `default`
case reaction
case groupParticipant
}
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
let openPeerMention: (String) -> Void
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
let activateMessagePinch: (PinchSourceContainerNode) -> Void
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
let navigateToMessage: (MessageId, MessageId) -> Void
let navigateToMessageStandalone: (MessageId) -> Void
let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
let tapMessage: ((Message) -> Void)?
let clickThroughMessage: () -> Void
let toggleMessagesSelection: ([MessageId], Bool) -> Void
let sendCurrentMessage: (Bool) -> Void
let sendMessage: (String) -> Void
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void
let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
let activateSwitchInline: (PeerId?, String) -> Void
let openUrl: (String, Bool, Bool?, Message?) -> Void
let shareCurrentLocation: () -> Void
let shareAccountContact: () -> Void
let sendBotCommand: (MessageId?, String) -> Void
let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void
let openWallpaper: (Message) -> Void
let openTheme: (Message) -> Void
let openHashtag: (String?, String) -> Void
let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void
let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void
let openMessageShareMenu: (MessageId) -> Void
let presentController: (ViewController, Any?) -> Void
let presentControllerInCurrent: (ViewController, Any?) -> Void
let navigationController: () -> NavigationController?
let chatControllerNode: () -> ASDisplayNode?
let presentGlobalOverlayController: (ViewController, Any?) -> Void
let callPeer: (PeerId, Bool) -> Void
let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
let openCheckoutOrReceipt: (MessageId) -> Void
let openSearch: () -> Void
let setupReply: (MessageId) -> Void
let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction
let navigateToFirstDateMessage: (Int32, Bool) -> Void
let requestRedeliveryOfFailedMessages: (MessageId) -> Void
let addContact: (String) -> Void
let rateCall: (Message, CallId, Bool) -> Void
let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void
let requestOpenMessagePollResults: (MessageId, MediaId) -> Void
let openAppStorePage: () -> Void
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
let seekToTimecode: (Message, Double, Bool) -> Void
let scheduleCurrentMessage: () -> Void
let sendScheduledMessagesNow: ([MessageId]) -> Void
let editScheduledMessagesTime: ([MessageId]) -> Void
let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void
let displayImportedMessageTooltip: (ASDisplayNode) -> Void
let displaySwipeToReplyHint: () -> Void
let dismissReplyMarkupMessage: (Message) -> Void
let openMessagePollResults: (MessageId, Data) -> Void
let openPollCreation: (Bool?) -> Void
let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
let displayPsa: (String, ASDisplayNode) -> Void
let displayDiceTooltip: (TelegramMediaDice) -> Void
let animateDiceSuccess: (Bool, Bool) -> Void
let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void
let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void
let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId, Bool, Bool) -> Void
let openReplyThreadOriginalMessage: (Message) -> Void
let openMessageStats: (MessageId) -> Void
let editMessageMedia: (MessageId, Bool) -> Void
let copyText: (String) -> Void
let displayUndo: (UndoOverlayContent) -> Void
let isAnimatingMessage: (UInt32) -> Bool
let getMessageTransitionNode: () -> ChatMessageTransitionNode?
let updateChoosingSticker: (Bool) -> Void
let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void
let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
let openJoinLink: (String) -> Void
let openWebView: (String, String, Bool, Bool) -> Void
let activateAdAction: (EngineMessage.Id) -> Void
public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
public let openPeerMention: (String) -> Void
public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
public let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
public let activateMessagePinch: (PinchSourceContainerNode) -> Void
public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
public let navigateToMessage: (MessageId, MessageId) -> Void
public let navigateToMessageStandalone: (MessageId) -> Void
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
public let tapMessage: ((Message) -> Void)?
public let clickThroughMessage: () -> Void
public let toggleMessagesSelection: ([MessageId], Bool) -> Void
public let sendCurrentMessage: (Bool) -> Void
public let sendMessage: (String) -> Void
public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void
public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
public let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool
public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
public let activateSwitchInline: (PeerId?, String) -> Void
public let openUrl: (String, Bool, Bool?, Message?) -> Void
public let shareCurrentLocation: () -> Void
public let shareAccountContact: () -> Void
public let sendBotCommand: (MessageId?, String) -> Void
public let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void
public let openWallpaper: (Message) -> Void
public let openTheme: (Message) -> Void
public let openHashtag: (String?, String) -> Void
public let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void
public let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void
public let openMessageShareMenu: (MessageId) -> Void
public let presentController: (ViewController, Any?) -> Void
public let presentControllerInCurrent: (ViewController, Any?) -> Void
public let navigationController: () -> NavigationController?
public let chatControllerNode: () -> ASDisplayNode?
public let presentGlobalOverlayController: (ViewController, Any?) -> Void
public let callPeer: (PeerId, Bool) -> Void
public let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
public let openCheckoutOrReceipt: (MessageId) -> Void
public let openSearch: () -> Void
public let setupReply: (MessageId) -> Void
public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction
public let navigateToFirstDateMessage: (Int32, Bool) -> Void
public let requestRedeliveryOfFailedMessages: (MessageId) -> Void
public let addContact: (String) -> Void
public let rateCall: (Message, CallId, Bool) -> Void
public let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void
public let requestOpenMessagePollResults: (MessageId, MediaId) -> Void
public let openAppStorePage: () -> Void
public let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
public let seekToTimecode: (Message, Double, Bool) -> Void
public let scheduleCurrentMessage: () -> Void
public let sendScheduledMessagesNow: ([MessageId]) -> Void
public let editScheduledMessagesTime: ([MessageId]) -> Void
public let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void
public let displayImportedMessageTooltip: (ASDisplayNode) -> Void
public let displaySwipeToReplyHint: () -> Void
public let dismissReplyMarkupMessage: (Message) -> Void
public let openMessagePollResults: (MessageId, Data) -> Void
public let openPollCreation: (Bool?) -> Void
public let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
public let displayPsa: (String, ASDisplayNode) -> Void
public let displayDiceTooltip: (TelegramMediaDice) -> Void
public let animateDiceSuccess: (Bool, Bool) -> Void
public let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void
public let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void
public let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
public let openMessageReplies: (MessageId, Bool, Bool) -> Void
public let openReplyThreadOriginalMessage: (Message) -> Void
public let openMessageStats: (MessageId) -> Void
public let editMessageMedia: (MessageId, Bool) -> Void
public let copyText: (String) -> Void
public let displayUndo: (UndoOverlayContent) -> Void
public let isAnimatingMessage: (UInt32) -> Bool
public let getMessageTransitionNode: () -> ChatMessageTransitionProtocol?
public let updateChoosingSticker: (Bool) -> Void
public let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
public let openJoinLink: (String) -> Void
public let openWebView: (String, String, Bool, Bool) -> Void
public let activateAdAction: (EngineMessage.Id) -> Void
let requestMessageUpdate: (MessageId, Bool) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
let dismissTextInput: () -> Void
let scrollToMessageId: (MessageIndex) -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void
public let dismissTextInput: () -> Void
public let scrollToMessageId: (MessageIndex) -> Void
var canPlayMedia: Bool = false
var hiddenMedia: [MessageId: [Media]] = [:]
var expandedTranslationMessageStableIds: Set<UInt32> = Set()
var selectionState: ChatInterfaceSelectionState?
var highlightedState: ChatInterfaceHighlightedState?
var contextHighlightedState: ChatInterfaceHighlightedState?
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
var pollActionState: ChatInterfacePollActionState
var currentPollMessageWithTooltip: MessageId?
var currentPsaMessageWithTooltip: MessageId?
var stickerSettings: ChatInterfaceStickerSettings
var searchTextHighightState: (String, [MessageIndex])?
var unreadMessageRange: [UnreadMessageRangeKey: Range<MessageId.Id>] = [:]
var seenOneTimeAnimatedMedia = Set<MessageId>()
var currentMessageWithLoadingReplyThread: MessageId?
var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
let presentationContext: ChatPresentationContext
var playNextOutgoingGift: Bool = false
public var canPlayMedia: Bool = false
public var hiddenMedia: [MessageId: [Media]] = [:]
public var expandedTranslationMessageStableIds: Set<UInt32> = Set()
public var selectionState: ChatInterfaceSelectionState?
public var highlightedState: ChatInterfaceHighlightedState?
public var contextHighlightedState: ChatInterfaceHighlightedState?
public var automaticMediaDownloadSettings: MediaAutoDownloadSettings
public var pollActionState: ChatInterfacePollActionState
public var currentPollMessageWithTooltip: MessageId?
public var currentPsaMessageWithTooltip: MessageId?
public var stickerSettings: ChatInterfaceStickerSettings
public var searchTextHighightState: (String, [MessageIndex])?
public var unreadMessageRange: [UnreadMessageRangeKey: Range<MessageId.Id>] = [:]
public var seenOneTimeAnimatedMedia = Set<MessageId>()
public var currentMessageWithLoadingReplyThread: MessageId?
public var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let presentationContext: ChatPresentationContext
public var playNextOutgoingGift: Bool = false
init(
public init(
openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool,
openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void,
openPeerMention: @escaping (String) -> Void,
@ -253,7 +270,7 @@ public final class ChatControllerInteraction {
copyText: @escaping (String) -> Void,
displayUndo: @escaping (UndoOverlayContent) -> Void,
isAnimatingMessage: @escaping (UInt32) -> Bool,
getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?,
getMessageTransitionNode: @escaping () -> ChatMessageTransitionProtocol?,
updateChoosingSticker: @escaping (Bool) -> Void,
commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void,
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,

View file

@ -0,0 +1,45 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatEntityKeyboardInputNode",
module_name = "ChatEntityKeyboardInputNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/PremiumUI:PremiumUI",
"//submodules/UndoUI:UndoUI",
"//submodules/ContextUI:ContextUI",
"//submodules/GalleryUI:GalleryUI",
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/StickerPeekUI:StickerPeekUI",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
],
visibility = [
"//visibility:public",
],
)

View file

@ -12,7 +12,6 @@ import MultiAnimationRenderer
import Postbox
import TelegramCore
import ComponentDisplayAdapters
import SettingsUI
import TextFormat
import PagerComponent
import AppBundle
@ -25,17 +24,37 @@ import AttachmentTextInputPanelNode
import TelegramPresentationData
import TelegramNotices
import StickerPeekUI
import ChatInputNode
import TelegramUIPreferences
import MultiplexedVideoNode
import ChatControllerInteraction
import FeaturedStickersScreen
final class EntityKeyboardGifContent: Equatable {
let hasRecentGifs: Bool
let component: GifPagerContentComponent
public struct ChatMediaInputPaneScrollState {
let absoluteOffset: CGFloat?
let relativeChange: CGFloat
}
public final class ChatMediaInputGifPaneTrendingState {
public let files: [MultiplexedVideoNodeFile]
public let nextOffset: String?
init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) {
self.files = files
self.nextOffset = nextOffset
}
}
public final class EntityKeyboardGifContent: Equatable {
public let hasRecentGifs: Bool
public let component: GifPagerContentComponent
public init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
self.hasRecentGifs = hasRecentGifs
self.component = component
}
static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
if lhs.hasRecentGifs != rhs.hasRecentGifs {
return false
}
@ -46,14 +65,14 @@ final class EntityKeyboardGifContent: Equatable {
}
}
final class ChatEntityKeyboardInputNode: ChatInputNode {
struct InputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent?
var gifs: EntityKeyboardGifContent?
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
public final class ChatEntityKeyboardInputNode: ChatInputNode {
public struct InputData: Equatable {
public var emoji: EmojiPagerContentComponent
public var stickers: EmojiPagerContentComponent?
public var gifs: EntityKeyboardGifContent?
public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
init(
public init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent?,
gifs: EntityKeyboardGifContent?,
@ -66,7 +85,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
let hasPremium: Signal<Bool, NoError>
if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId {
hasPremium = .single(true)
@ -83,7 +102,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
return hasPremium
}
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal<InputData, NoError> {
public static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal<InputData, NoError> {
let animationCache = context.animationCache
let animationRenderer = context.animationRenderer
@ -94,7 +113,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId)
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: true, hasTrending: true)
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|> map { appConfiguration -> [String] in
@ -153,7 +172,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading: false,
loadMoreToken: nil,
displaySearchWithPlaceholder: nil,
searchInitiallyHidden: false
searchInitiallyHidden: true
)
))
@ -238,11 +257,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
var externalTopPanelContainerImpl: PagerExternalTopPanelContainer?
override var externalTopPanelContainer: UIView? {
public override var externalTopPanelContainer: UIView? {
return self.externalTopPanelContainerImpl
}
var switchToTextInput: (() -> Void)?
public var switchToTextInput: (() -> Void)?
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
@ -257,7 +276,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
var canSwitchToTextInputAutomatically: Bool {
public var canSwitchToTextInputAutomatically: Bool {
if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId {
if centralId == AnyHashable("emoji") {
return false
@ -321,7 +340,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading: false,
loadMoreToken: nil,
displaySearchWithPlaceholder: presentationData.strings.GifSearch_SearchGifPlaceholder,
searchInitiallyHidden: false
searchInitiallyHidden: true
)
)
}
@ -352,7 +371,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading: isLoading,
loadMoreToken: nil,
displaySearchWithPlaceholder: nil,
searchInitiallyHidden: false
searchInitiallyHidden: true
)
)
}
@ -385,7 +404,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading: isLoading,
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: nil,
searchInitiallyHidden: false
searchInitiallyHidden: true
)
)
}
@ -466,7 +485,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
isLoading: isLoading,
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: nil,
searchInitiallyHidden: false
searchInitiallyHidden: true
)
)
}
@ -492,8 +511,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
private weak var currentUndoOverlayController: UndoOverlayController?
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) {
public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) {
self.context = context
self.currentInputData = currentInputData
self.defaultToEmojiTab = defaultToEmojiTab
@ -517,7 +535,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
return
}
@ -935,7 +953,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
guard let controllerInteraction = controllerInteraction else {
return
}
let controller = installedStickerPacksController(context: context, mode: .modal)
let controller = context.sharedContext.makeInstalledStickerPacksController(context: context, mode: .modal)
controller.navigationPresentation = .modal
controllerInteraction.navigationController()?.pushViewController(controller)
},
@ -1230,7 +1248,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
}
}
func markInputCollapsed() {
public func markInputCollapsed() {
self.isMarkInputCollapsed = true
}
@ -1242,14 +1260,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
}
func simulateUpdateLayout(isVisible: Bool) {
public func simulateUpdateLayout(isVisible: Bool) {
guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _, isExpanded) = self.currentState else {
return
}
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded)
let innerTransition: Transition
@ -1318,11 +1336,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
topPanelInsets: UIEdgeInsets(),
emojiContent: self.currentInputData.emoji,
stickerContent: stickerContent,
maskContent: nil,
gifContent: gifContent?.component,
hasRecentGifs: gifContent?.hasRecentGifs ?? false,
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
defaultToEmojiTab: self.defaultToEmojiTab,
externalTopPanelContainer: self.externalTopPanelContainerImpl,
externalBottomPanelContainer: nil,
displayTopPanelBackground: false,
topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in
guard let strongSelf = self else {
return
@ -1502,6 +1523,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
namespace = Namespaces.ItemCollection.CloudStickerPacks
case .emoji:
namespace = Namespaces.ItemCollection.CloudEmojiPacks
case .masks:
namespace = Namespaces.ItemCollection.CloudMaskPacks
}
self.stableReorderableGroupOrder.removeValue(forKey: category)
@ -1685,7 +1708,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
}
}
final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
private let context: AccountContext
public var insertText: ((NSAttributedString) -> Void)?
@ -1698,7 +1721,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
init(
public init(
context: AccountContext,
isDark: Bool,
areCustomEmojiEnabled: Bool
@ -1713,8 +1736,8 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
}
//super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
// super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.clipsToBounds = true
@ -1818,7 +1841,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
navigationController: {
return nil
},
requestUpdate: { _ in
requestUpdate: { _ in
},
updateSearchQuery: { _, _ in
},
@ -1874,7 +1897,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
public override func layoutSubviews() {
super.layoutSubviews()
guard let inputNode = self.inputNode else {
@ -2136,3 +2159,112 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
}
}
}
public class PaneGifSearchForQueryResult {
public let files: [MultiplexedVideoNodeFile]
public let nextOffset: String?
public let isComplete: Bool
public let isStale: Bool
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
self.files = files
self.nextOffset = nextOffset
self.isComplete = isComplete
self.isStale = isStale
}
}
public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
let botName = searchBots.gifBotUsername ?? "gif"
return context.engine.peers.resolvePeerByName(name: botName)
}
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
}
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
if delayRequest {
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
} else {
maybeDelayedContextResults = results
}
return maybeDelayedContextResults
} else {
return .single((nil, true, false))
}
}
return contextBot
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
let results = collection.results
var references: [MultiplexedVideoNodeFile] = []
for result in results {
switch result {
case let .externalReference(externalReference):
var imageResource: TelegramMediaResource?
var thumbnailResource: TelegramMediaResource?
var thumbnailIsVideo: Bool = false
var uniqueId: Int64?
if let content = externalReference.content {
imageResource = content.resource
if let resource = content.resource as? WebFileReferenceMediaResource {
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
}
}
if let thumbnail = externalReference.thumbnail {
thumbnailResource = thumbnail.resource
if thumbnail.mimeType.hasPrefix("video/") {
thumbnailIsVideo = true
}
}
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
var previews: [TelegramMediaImageRepresentation] = []
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
if let thumbnailResource = thumbnailResource {
if thumbnailIsVideo {
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
dimensions: dimensions,
resource: thumbnailResource
))
} else {
previews.append(TelegramMediaImageRepresentation(
dimensions: dimensions,
resource: thumbnailResource,
progressiveSizes: [],
immediateThumbnailData: nil,
hasVideo: false,
isPersonal: false
))
}
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
}
case let .internalReference(internalReference):
if let file = internalReference.file {
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
}
}
}
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
} else if incompleteResults {
return .single(nil)
} else {
return .complete()
}
}
|> deliverOnMainQueue
|> beforeStarted {
updateActivity?(true)
}
|> afterCompleted {
updateActivity?(false)
}
}

View file

@ -7,117 +7,10 @@ import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import WebSearchUI
import AppBundle
class PaneGifSearchForQueryResult {
let files: [MultiplexedVideoNodeFile]
let nextOffset: String?
let isComplete: Bool
let isStale: Bool
init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
self.files = files
self.nextOffset = nextOffset
self.isComplete = isComplete
self.isStale = isStale
}
}
func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
let botName = searchBots.gifBotUsername ?? "gif"
return context.engine.peers.resolvePeerByName(name: botName)
}
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let results = requestContextResults(context: context, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
}
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
if delayRequest {
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
} else {
maybeDelayedContextResults = results
}
return maybeDelayedContextResults
} else {
return .single((nil, true, false))
}
}
return contextBot
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
let results = collection.results
var references: [MultiplexedVideoNodeFile] = []
for result in results {
switch result {
case let .externalReference(externalReference):
var imageResource: TelegramMediaResource?
var thumbnailResource: TelegramMediaResource?
var thumbnailIsVideo: Bool = false
var uniqueId: Int64?
if let content = externalReference.content {
imageResource = content.resource
if let resource = content.resource as? WebFileReferenceMediaResource {
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
}
}
if let thumbnail = externalReference.thumbnail {
thumbnailResource = thumbnail.resource
if thumbnail.mimeType.hasPrefix("video/") {
thumbnailIsVideo = true
}
}
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
var previews: [TelegramMediaImageRepresentation] = []
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
if let thumbnailResource = thumbnailResource {
if thumbnailIsVideo {
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
dimensions: dimensions,
resource: thumbnailResource
))
} else {
previews.append(TelegramMediaImageRepresentation(
dimensions: dimensions,
resource: thumbnailResource,
progressiveSizes: [],
immediateThumbnailData: nil,
hasVideo: false,
isPersonal: false
))
}
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
}
case let .internalReference(internalReference):
if let file = internalReference.file {
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
}
}
}
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
} else if incompleteResults {
return .single(nil)
} else {
return .complete()
}
}
|> deliverOnMainQueue
|> beforeStarted {
updateActivity?(true)
}
|> afterCompleted {
updateActivity?(false)
}
}
import ChatControllerInteraction
import MultiplexedVideoNode
import ChatPresentationInterfaceState
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private let context: AccountContext

View file

@ -6,6 +6,7 @@ import Display
import TelegramPresentationData
import ActivityIndicator
import AppBundle
import FeaturedStickersScreen
private func generateLoupeIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)

View file

@ -9,10 +9,13 @@ import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import EntityKeyboard
import ChatControllerInteraction
import MultiplexedVideoNode
import FeaturedStickersScreen
private let searchBarHeight: CGFloat = 52.0
protocol PaneSearchContentNode {
public protocol PaneSearchContentNode {
var ready: Signal<Void, NoError> { get }
var deactivateSearchBar: (() -> Void)? { get set }
var updateActivity: ((Bool) -> Void)? { get set }
@ -28,7 +31,7 @@ protocol PaneSearchContentNode {
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)?
}
final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
private let context: AccountContext
private let mode: ChatMediaInputSearchMode
public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode
@ -40,15 +43,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
private var validLayout: CGSize?
var onCancel: (() -> Void)?
public var onCancel: (() -> Void)?
var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
var ready: Signal<Void, NoError> {
public var ready: Signal<Void, NoError> {
return self.contentNode.ready
}
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
self.context = context
self.mode = mode
self.controllerInteraction = controllerInteraction
@ -102,7 +105,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
}
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
@ -117,15 +120,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
}
func updateQuery(_ query: String) {
public func updateQuery(_ query: String) {
self.searchBar.updateQuery(query)
}
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
}
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
self.validLayout = size
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
@ -138,11 +141,11 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition)
}
func deactivate() {
public func deactivate() {
self.searchBar.deactivate(clear: true)
}
func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
if let placeholder = placeholder {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
@ -170,7 +173,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
}
}
func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
if case let .animated(duration, curve) = transition {
if let size = self.validLayout {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)

View file

@ -16,20 +16,10 @@ import Emoji
import AppBundle
import OverlayStatusController
import UndoUI
final class StickerPaneSearchInteraction {
let open: (StickerPackCollectionInfo) -> Void
let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void
let sendSticker: (FileMediaReference, UIView, CGRect) -> Void
let getItemIsPreviewed: (StickerPackItem) -> Bool
init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
self.open = open
self.install = install
self.sendSticker = sendSticker
self.getItemIsPreviewed = getItemIsPreviewed
}
}
import ChatControllerInteraction
import FeaturedStickersScreen
import ChatPresentationInterfaceState
import FeaturedStickersScreen
private enum StickerSearchEntryId: Equatable, Hashable {
case sticker(String?, Int64)

View file

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputNode",
module_name = "ChatInputNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,34 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ChatPresentationInterfaceState
open class ChatInputNode: ASDisplayNode {
public var interfaceInteraction: ChatPanelInterfaceInteraction?
open var ready: Signal<Void, NoError> {
return .single(Void())
}
open var externalTopPanelContainer: UIView? {
return nil
}
public var topBackgroundExtension: CGFloat = 0.0
public var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var hideInput: Bool = false
public var adjustLayoutForHiddenInput: Bool = false
public var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var followsDefaultHeight: Bool = false
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
}
open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
return (0.0, 0.0)
}
}

View file

@ -165,11 +165,14 @@ public final class EmojiStatusSelectionComponent: Component {
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent,
stickerContent: nil,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: true,
topPanelExtensionUpdated: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in

View file

@ -482,3 +482,69 @@ public final class EmojiTextAttachmentView: UIView {
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height))
}
}
public final class CustomEmojiContainerView: UIView {
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
self.emojiViewProvider = emojiViewProvider
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
var nextIndexById: [Int64: Int] = [:]
var validKeys = Set<InlineStickerItemLayer.Key>()
for (rect, emoji) in emojiRects {
let index: Int
if let nextIndex = nextIndexById[emoji.fileId] {
index = nextIndex
} else {
index = 0
}
nextIndexById[emoji.fileId] = index + 1
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
let view: UIView
if let current = self.emojiLayers[key] {
view = current
} else if let newView = self.emojiViewProvider(emoji) {
view = newView
self.addSubview(newView)
self.emojiLayers[key] = view
} else {
continue
}
if let view = view as? EmojiTextAttachmentView {
view.updateTextColor(textColor)
}
let itemSize: CGFloat = floor(24.0 * fontSize / 17.0)
let size = CGSize(width: itemSize, height: itemSize)
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0) + 1.0), size: size)
validKeys.insert(key)
}
var removeKeys: [InlineStickerItemLayer.Key] = []
for (key, view) in self.emojiLayers {
if !validKeys.contains(key) {
removeKeys.append(key)
view.removeFromSuperview()
}
}
for key in removeKeys {
self.emojiLayers.removeValue(forKey: key)
}
}
}

View file

@ -2101,8 +2101,8 @@ public final class EmojiPagerContentComponent: Component {
public final class InputInteraction {
public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void
public let deleteBackwards: () -> Void
public let openStickerSettings: () -> Void
public let deleteBackwards: (() -> Void)?
public let openStickerSettings: (() -> Void)?
public let openFeatured: () -> Void
public let openSearch: () -> Void
public let addGroupAction: (AnyHashable, Bool) -> Void
@ -2122,8 +2122,8 @@ public final class EmojiPagerContentComponent: Component {
public init(
performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void,
deleteBackwards: @escaping () -> Void,
openStickerSettings: @escaping () -> Void,
deleteBackwards: (() -> Void)?,
openStickerSettings: (() -> Void)?,
openFeatured: @escaping () -> Void,
openSearch: @escaping () -> Void,
addGroupAction: @escaping (AnyHashable, Bool) -> Void,
@ -6310,7 +6310,8 @@ public final class EmojiPagerContentComponent: Component {
selectedItems: Set<MediaId> = Set(),
topStatusTitle: String? = nil,
topicTitle: String? = nil,
topicColor: Int32? = nil
topicColor: Int32? = nil,
hasSearch: Bool = true
) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -7088,15 +7089,17 @@ public final class EmojiPagerContentComponent: Component {
var displaySearchWithPlaceholder: String?
var searchInitiallyHidden = true
if isReactionSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder
} else if isStatusSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder
} else if isTopicIconSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchTopicIconsPlaceholder
} else if isEmojiSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
searchInitiallyHidden = false
if hasSearch {
if isReactionSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder
} else if isStatusSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder
} else if isTopicIconSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchTopicIconsPlaceholder
} else if isEmojiSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
searchInitiallyHidden = false
}
}
return EmojiPagerContentComponent(
@ -7160,7 +7163,9 @@ public final class EmojiPagerContentComponent: Component {
animationRenderer: MultiAnimationRenderer,
stickerNamespaces: [ItemCollectionId.Namespace],
stickerOrderedItemListCollectionIds: [Int32],
chatPeerId: EnginePeer.Id?
chatPeerId: EnginePeer.Id?,
hasSearch: Bool,
hasTrending: Bool
) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -7212,7 +7217,7 @@ public final class EmojiPagerContentComponent: Component {
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false),
context.account.viewTracker.featuredStickerPacks(),
hasTrending ? context.account.viewTracker.featuredStickerPacks() : .single([]),
context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))),
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager),
peerSpecificPack
@ -7674,8 +7679,8 @@ public final class EmojiPagerContentComponent: Component {
itemLayoutType: .detailed,
itemContentUniqueId: nil,
warpContentsOnEdges: false,
displaySearchWithPlaceholder: strings.StickersSearch_SearchStickersPlaceholder,
searchInitiallyHidden: false,
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
searchInitiallyHidden: true,
searchIsPlaceholderOnly: true,
emptySearchResults: nil,
enableLongPress: false,

View file

@ -58,6 +58,7 @@ public final class EntityKeyboardComponent: Component {
public enum ReorderCategory {
case stickers
case emoji
case masks
}
public struct GifSearchEmoji: Equatable {
@ -92,11 +93,14 @@ public final class EntityKeyboardComponent: Component {
public let topPanelInsets: UIEdgeInsets
public let emojiContent: EmojiPagerContentComponent?
public let stickerContent: EmojiPagerContentComponent?
public let maskContent: EmojiPagerContentComponent?
public let gifContent: GifPagerContentComponent?
public let hasRecentGifs: Bool
public let availableGifSearchEmojies: [GifSearchEmoji]
public let defaultToEmojiTab: Bool
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
public let externalBottomPanelContainer: PagerExternalTopPanelContainer?
public let displayTopPanelBackground: Bool
public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
public let hideTopPanelUpdated: (Bool, Transition) -> Void
@ -118,11 +122,14 @@ public final class EntityKeyboardComponent: Component {
topPanelInsets: UIEdgeInsets,
emojiContent: EmojiPagerContentComponent?,
stickerContent: EmojiPagerContentComponent?,
maskContent: EmojiPagerContentComponent?,
gifContent: GifPagerContentComponent?,
hasRecentGifs: Bool,
availableGifSearchEmojies: [GifSearchEmoji],
defaultToEmojiTab: Bool,
externalTopPanelContainer: PagerExternalTopPanelContainer?,
externalBottomPanelContainer: PagerExternalTopPanelContainer?,
displayTopPanelBackground: Bool,
topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void,
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
hideTopPanelUpdated: @escaping (Bool, Transition) -> Void,
@ -143,11 +150,14 @@ public final class EntityKeyboardComponent: Component {
self.topPanelInsets = topPanelInsets
self.emojiContent = emojiContent
self.stickerContent = stickerContent
self.maskContent = maskContent
self.gifContent = gifContent
self.hasRecentGifs = hasRecentGifs
self.availableGifSearchEmojies = availableGifSearchEmojies
self.defaultToEmojiTab = defaultToEmojiTab
self.externalTopPanelContainer = externalTopPanelContainer
self.externalBottomPanelContainer = externalBottomPanelContainer
self.displayTopPanelBackground = displayTopPanelBackground
self.topPanelExtensionUpdated = topPanelExtensionUpdated
self.hideInputUpdated = hideInputUpdated
self.hideTopPanelUpdated = hideTopPanelUpdated
@ -184,6 +194,9 @@ public final class EntityKeyboardComponent: Component {
if lhs.stickerContent != rhs.stickerContent {
return false
}
if lhs.maskContent != rhs.maskContent {
return false
}
if lhs.gifContent != rhs.gifContent {
return false
}
@ -199,6 +212,9 @@ public final class EntityKeyboardComponent: Component {
if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer {
return false
}
if lhs.displayTopPanelBackground != rhs.displayTopPanelBackground {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
@ -268,11 +284,96 @@ public final class EntityKeyboardComponent: Component {
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
if transition.userData(MarkInputCollapsed.self) != nil {
self.searchComponent = nil
}
if let maskContent = component.maskContent {
var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = []
for itemGroup in maskContent.itemGroups {
if let id = itemGroup.supergroupId.base as? String {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"saved": .saved,
"recent": .recent,
"premium": .premium
]
let titleMapping: [String: String] = [
"saved": component.strings.Stickers_Favorites,
"recent": component.strings.Stickers_Recent,
"premium": component.strings.EmojiInput_PanelTitlePremium
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topMaskItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: false,
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
icon: icon,
theme: component.theme,
useAccentColor: false,
title: title,
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
} else {
if !itemGroup.items.isEmpty {
if let animationData = itemGroup.items[0].animationData {
topMaskItems.append(EntityKeyboardTopPanelComponent.Item(
id: itemGroup.supergroupId,
isReorderable: !itemGroup.isFeatured,
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
context: maskContent.context,
item: itemGroup.headerItem ?? animationData,
isFeatured: itemGroup.isFeatured,
isPremiumLocked: itemGroup.isPremiumLocked,
animationCache: maskContent.animationCache,
animationRenderer: maskContent.animationRenderer,
theme: component.theme,
title: itemGroup.title ?? "",
pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil)
}
))
))
}
}
}
}
contents.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(maskContent)))
contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent(
id: "masks",
theme: component.theme,
items: topMaskItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
defaultActiveItemId: maskContent.itemGroups.first?.groupId,
activeContentItemIdUpdated: masksContentItemIdUpdated,
reorderItems: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.reorderPacks(category: .stickers, items: items)
}
))))
contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon"))
if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings {
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
}
if let gifContent = component.gifContent {
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent)))
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
@ -345,16 +446,6 @@ public final class EntityKeyboardComponent: Component {
}
))))
contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon"))
// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
// content: AnyComponent(BundleIconComponent(
// name: "Chat/Input/Media/EntityInputSearchIcon",
// tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
// maxSize: nil
// )),
// action: { [weak self] in
// self?.openSearch()
// }
// ).minSize(CGSize(width: 38.0, height: 38.0)))))
}
if let stickerContent = component.stickerContent {
@ -459,28 +550,22 @@ public final class EntityKeyboardComponent: Component {
}
))))
contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon"))
// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
// content: AnyComponent(BundleIconComponent(
// name: "Chat/Input/Media/EntityInputSearchIcon",
// tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
// maxSize: nil
// )),
// action: { [weak self] in
// self?.openSearch()
// }
// ).minSize(CGSize(width: 38.0, height: 38.0)))))
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings {
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputSettingsIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
}
let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
if let emojiContent = component.emojiContent {
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent)))
@ -564,36 +649,39 @@ public final class EntityKeyboardComponent: Component {
}
))))
contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon"))
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
if let _ = deleteBackwards {
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputGlobeIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.switchToTextInput()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
}
if let _ = deleteBackwards {
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputGlobeIcon",
name: "Chat/Input/Media/EntityInputClearIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.switchToTextInput()
action: {
deleteBackwards?()
AudioServicesPlaySystemSound(1155)
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/EntityInputClearIcon",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
).withHoldAction({
deleteBackwards?()
AudioServicesPlaySystemSound(1155)
}
).withHoldAction({
deleteBackwards?()
AudioServicesPlaySystemSound(1155)
}).minSize(CGSize(width: 38.0, height: 38.0)))))
}).minSize(CGSize(width: 38.0, height: 38.0)))))
}
let panelHideBehavior: PagerComponentPanelHideBehavior
if self.searchComponent != nil {
@ -618,17 +706,18 @@ public final class EntityKeyboardComponent: Component {
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
theme: component.theme,
overflowHeight: component.hiddenInputHeight,
displayBackground: component.externalTopPanelContainer == nil
displayBackground: component.externalTopPanelContainer == nil && component.displayTopPanelBackground
)),
externalTopPanelContainer: component.externalTopPanelContainer,
bottomPanel: component.displayBottomPanel ? AnyComponent(EntityKeyboardBottomPanelComponent(
theme: component.theme,
containerInsets: component.containerInsets,
deleteBackwards: { [weak self] in
self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards()
self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards?()
AudioServicesPlaySystemSound(0x451)
}
)) : nil,
externalBottomPanelContainer: component.externalBottomPanelContainer,
panelStateUpdated: { [weak self] panelState, transition in
guard let strongSelf = self else {
return
@ -661,6 +750,8 @@ public final class EntityKeyboardComponent: Component {
return stickersContentItemIdUpdated
} else if id == AnyHashable("emoji") {
return emojiContentItemIdUpdated
} else if id == AnyHashable("masks") {
return masksContentItemIdUpdated
}
return nil
}
@ -670,7 +761,7 @@ public final class EntityKeyboardComponent: Component {
)
transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize))
let accountContext = component.emojiContent?.context ?? component.stickerContent?.context
let accountContext = component.emojiContent?.context ?? component.maskContent?.context
if let searchComponent = self.searchComponent, let accountContext = accountContext {
var animateIn = false
let searchView: ComponentHostView<EntitySearchContentEnvironment>

View file

@ -401,11 +401,14 @@ private final class TopicIconSelectionComponent: Component {
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: component.emojiContent,
stickerContent: nil,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
displayTopPanelBackground: true,
topPanelExtensionUpdated: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { _, _ in },

View file

@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultiplexedVideoNode",
module_name = "MultiplexedVideoNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/ContextUI:ContextUI",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/SoftwareVideo:SoftwareVideo",
],
visibility = [
"//visibility:public",
],
)

View file

@ -74,24 +74,24 @@ private final class VisibleVideoItem {
}
}
final class MultiplexedVideoNodeFile {
let file: FileMediaReference
let contextResult: (ChatContextResultCollection, ChatContextResult)?
public final class MultiplexedVideoNodeFile {
public let file: FileMediaReference
public let contextResult: (ChatContextResultCollection, ChatContextResult)?
init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
self.file = file
self.contextResult = contextResult
}
}
final class MultiplexedVideoNodeFiles {
let saved: [MultiplexedVideoNodeFile]
let trending: [MultiplexedVideoNodeFile]
let isSearch: Bool
let canLoadMore: Bool
let isStale: Bool
public final class MultiplexedVideoNodeFiles {
public let saved: [MultiplexedVideoNodeFile]
public let trending: [MultiplexedVideoNodeFile]
public let isSearch: Bool
public let canLoadMore: Bool
public let isStale: Bool
init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) {
public init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) {
self.saved = saved
self.trending = trending
self.isSearch = isSearch
@ -100,36 +100,37 @@ final class MultiplexedVideoNodeFiles {
}
}
final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private let account: Account
private var theme: PresentationTheme
private var strings: PresentationStrings
private let trackingNode: MultiplexedVideoTrackingNode
var didScroll: ((CGFloat, CGFloat) -> Void)?
var didEndScrolling: (() -> Void)?
var reactionSelected: ((String) -> Void)?
var topInset: CGFloat = 0.0 {
public var didScroll: ((CGFloat, CGFloat) -> Void)?
public var didEndScrolling: (() -> Void)?
public var reactionSelected: ((String) -> Void)?
public var topInset: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
var bottomInset: CGFloat = 0.0 {
public var bottomInset: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
var idealHeight: CGFloat = 93.0 {
public var idealHeight: CGFloat = 93.0 {
didSet {
self.setNeedsLayout()
}
}
private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false)
public private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false)
func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
public func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
self.files = files
self.ignoreDidScroll = true
@ -145,7 +146,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:]
private let contextContainerNode: ContextControllerSourceNode
let scrollNode: ASScrollNode
public let scrollNode: ASScrollNode
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
@ -157,14 +158,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private let timebase: CMTimebase
var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
var enableVideoNodes = false
public var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
public var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
public var enableVideoNodes = false
private var currentActivatingId: VisibleVideoItem.Id?
private var isFinishingActivation = false
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
self.account = account
self.theme = theme
self.strings = strings
@ -343,7 +344,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
private var validSize: CGSize?
func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
public func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
self.theme = theme
self.strings = strings
if self.validSize == nil || !self.validSize!.equalTo(size) {
@ -359,18 +360,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private var ignoreDidScroll: Bool = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreDidScroll {
self.updateImmediatelyVisibleItems()
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.didEndScrolling?()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.didEndScrolling?()
}
@ -738,7 +739,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
func frameForItem(_ id: MediaId) -> CGRect? {
public func frameForItem(_ id: MediaId) -> CGRect? {
for item in self.displayItems {
if item.file.file.media.fileId == id {
return item.frame
@ -747,7 +748,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
return nil
}
func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
public func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
if let result = self.internalFileAt(point: point) {
return (result.1, result.2, result.3)
} else {

View file

@ -13,7 +13,7 @@ private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction {
}
}
final class SoftwareVideoThumbnailNode: ASDisplayNode {
public final class SoftwareVideoThumbnailNode: ASDisplayNode {
private let usePlaceholder: Bool
private var placeholder: MultiplexedVideoPlaceholderNode?
private var theme: PresentationTheme?
@ -21,7 +21,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
var disposable = MetaDisposable()
var ready: (() -> Void)? {
public var ready: (() -> Void)? {
didSet {
if self.layer.contents != nil {
self.ready?()
@ -29,6 +29,10 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
}
}
public convenience init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false) {
self.init(account: account, fileReference: fileReference, synchronousLoad: synchronousLoad, existingPlaceholder: nil)
}
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false, existingPlaceholder: MultiplexedVideoPlaceholderNode? = nil) {
self.usePlaceholder = usePlaceholder
if usePlaceholder {
@ -102,7 +106,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
self.disposable.dispose()
}
func update(theme: PresentationTheme, size: CGSize) {
public func update(theme: PresentationTheme, size: CGSize) {
if self.usePlaceholder {
self.theme = theme
}
@ -112,7 +116,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
}
}
func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
self.asolutePosition = (absoluteRect, containerSize)
if let placeholder = self.placeholder {
placeholder.updateAbsoluteRect(absoluteRect, within: containerSize)

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "mask.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

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