mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-19 17:51:29 +00:00
Various improvements
This commit is contained in:
parent
bc04d3c8e9
commit
41ae916106
@ -14412,3 +14412,19 @@ Sorry for the inconvenience.";
|
||||
"Stars.SendMessage.AdjustmentAction" = "OK";
|
||||
|
||||
"Stars.SendMessage.PriceFree" = "Free";
|
||||
|
||||
"Notification.TodoTasks_1" = "%@ task";
|
||||
"Notification.TodoTasks_any" = "%@ tasks";
|
||||
|
||||
"Notification.TodoCompleted" = "%1$@ marked \"%2$@\" as done.";
|
||||
"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone.";
|
||||
"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\".";
|
||||
"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\".";
|
||||
|
||||
"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done.";
|
||||
"Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not done.";
|
||||
"Notification.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\".";
|
||||
"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\".";
|
||||
|
||||
"Stars.Transaction.GiftTransfer" = "Gift Transfer";
|
||||
"Stars.Intro.Transaction.GiftTransfer" = "Gift Transfer";
|
||||
|
@ -43,6 +43,7 @@ public enum PremiumIntroSource {
|
||||
case animatedEmoji
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
case auth(String)
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ public enum PremiumDemoSubject {
|
||||
case business
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
|
@ -21,6 +21,7 @@ public enum AttachmentButtonType: Equatable {
|
||||
case gallery
|
||||
case file
|
||||
case location
|
||||
case todo
|
||||
case quickReply
|
||||
case contact
|
||||
case poll
|
||||
@ -36,6 +37,8 @@ public enum AttachmentButtonType: Equatable {
|
||||
return "file"
|
||||
case .location:
|
||||
return "location"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .quickReply:
|
||||
return "quickReply"
|
||||
case .contact:
|
||||
@ -71,6 +74,12 @@ public enum AttachmentButtonType: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .todo:
|
||||
if case .todo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .quickReply:
|
||||
if case .quickReply = rhs {
|
||||
return true
|
||||
|
@ -195,6 +195,9 @@ private final class AttachButtonComponent: CombinedComponent {
|
||||
case .location:
|
||||
name = strings.Attachment_Location
|
||||
imageName = "Chat/Attach Menu/Location"
|
||||
case .todo:
|
||||
name = "To Do List"
|
||||
imageName = "Chat/Attach Menu/Todo"
|
||||
case .contact:
|
||||
name = strings.Attachment_Contact
|
||||
imageName = "Chat/Attach Menu/Contact"
|
||||
@ -1267,6 +1270,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
@ -1490,6 +1494,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_File
|
||||
case .location:
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Location
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
accessibilityTitle = "To Do List"
|
||||
case .contact:
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Contact
|
||||
case .poll:
|
||||
|
@ -333,7 +333,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
|
||||
if let cutoutRect {
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height))
|
||||
//TODO:fix
|
||||
context.fillEllipse(in: cutoutRect)
|
||||
}
|
||||
})
|
||||
let unroundedImage: UIImage?
|
||||
|
@ -176,6 +176,8 @@ public final class BrowserBookmarksScreen: ViewController {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
|
||||
|
@ -108,7 +108,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/ComposePollUI",
|
||||
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
|
@ -20,7 +20,7 @@ import AsyncDisplayKit
|
||||
import UndoUI
|
||||
import PeerNameColorItem
|
||||
import EntityKeyboard
|
||||
import ComposePollUI
|
||||
import ListComposePollOptionComponent
|
||||
import ChatEntityKeyboardInputNode
|
||||
import ComponentFlow
|
||||
import ChatPresentationInterfaceState
|
||||
|
@ -8,7 +8,7 @@ import TextNodeWithEntities
|
||||
import AccountContext
|
||||
import ItemListUI
|
||||
import ComponentFlow
|
||||
import ComposePollUI
|
||||
import ListComposePollOptionComponent
|
||||
import TextFieldComponent
|
||||
|
||||
public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem {
|
||||
|
@ -423,6 +423,20 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
|
||||
messageText = content.displayUrl
|
||||
}
|
||||
case let todo as TelegramMediaTodo:
|
||||
let pollPrefix = "☑️ "
|
||||
let entityOffset = (pollPrefix as NSString).length
|
||||
messageText = "\(pollPrefix)\(todo.text)"
|
||||
for entity in todo.textEntities {
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
if customEmojiRanges == nil {
|
||||
customEmojiRanges = []
|
||||
}
|
||||
let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
|
||||
customEmojiRanges?.append((range, attribute))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -177,6 +177,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
public let openBoostToUnrestrict: () -> Void
|
||||
public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void
|
||||
public let dismissAllTooltips: () -> Void
|
||||
public let editTodoMessage: (MessageId, Bool) -> Void
|
||||
public let requestLayout: (ContainedViewLayoutTransition) -> Void
|
||||
public let chatController: () -> ViewController?
|
||||
public let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
@ -293,6 +294,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
openBoostToUnrestrict: @escaping () -> Void,
|
||||
updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void,
|
||||
dismissAllTooltips: @escaping () -> Void,
|
||||
editTodoMessage: @escaping (MessageId, Bool) -> Void,
|
||||
updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void,
|
||||
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
|
||||
toggleChatSidebarMode: @escaping () -> Void,
|
||||
@ -412,6 +414,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
self.openBoostToUnrestrict = openBoostToUnrestrict
|
||||
self.updateRecordingTrimRange = updateRecordingTrimRange
|
||||
self.dismissAllTooltips = dismissAllTooltips
|
||||
self.editTodoMessage = editTodoMessage
|
||||
self.updateHistoryFilter = updateHistoryFilter
|
||||
self.updateChatLocationThread = updateChatLocationThread
|
||||
self.toggleChatSidebarMode = toggleChatSidebarMode
|
||||
@ -540,6 +543,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -41,6 +41,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -26,6 +26,49 @@ import ChatPresentationInterfaceState
|
||||
import EmojiSuggestionsComponent
|
||||
import TextFormat
|
||||
import TextFieldComponent
|
||||
import ListComposePollOptionComponent
|
||||
|
||||
public final class ComposedPoll {
|
||||
public struct Text {
|
||||
public let string: String
|
||||
public let entities: [MessageTextEntity]
|
||||
|
||||
public init(string: String, entities: [MessageTextEntity]) {
|
||||
self.string = string
|
||||
self.entities = entities
|
||||
}
|
||||
}
|
||||
|
||||
public let publicity: TelegramMediaPollPublicity
|
||||
public let kind: TelegramMediaPollKind
|
||||
|
||||
public let text: Text
|
||||
public let options: [TelegramMediaPollOption]
|
||||
public let correctAnswers: [Data]?
|
||||
public let results: TelegramMediaPollResults
|
||||
public let deadlineTimeout: Int32?
|
||||
public let usedCustomEmojiFiles: [Int64: TelegramMediaFile]
|
||||
|
||||
public init(
|
||||
publicity: TelegramMediaPollPublicity,
|
||||
kind: TelegramMediaPollKind,
|
||||
text: Text,
|
||||
options: [TelegramMediaPollOption],
|
||||
correctAnswers: [Data]?,
|
||||
results: TelegramMediaPollResults,
|
||||
deadlineTimeout: Int32?,
|
||||
usedCustomEmojiFiles: [Int64: TelegramMediaFile]
|
||||
) {
|
||||
self.publicity = publicity
|
||||
self.kind = kind
|
||||
self.text = text
|
||||
self.options = options
|
||||
self.correctAnswers = correctAnswers
|
||||
self.results = results
|
||||
self.deadlineTimeout = deadlineTimeout
|
||||
self.usedCustomEmojiFiles = usedCustomEmojiFiles
|
||||
}
|
||||
}
|
||||
|
||||
final class ComposePollScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -1585,7 +1628,7 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
|
||||
maxPollAnwsersCount = Int(value)
|
||||
}
|
||||
return InitialData(
|
||||
maxPollTextLength: Int(200),
|
||||
maxPollTextLength: 200,
|
||||
maxPollOptionLength: 100,
|
||||
maxPollAnwsersCount: maxPollAnwsersCount
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,274 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
|
||||
class CreatePollOptionActionItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let enabled: Bool
|
||||
let tag: ItemListItemTag?
|
||||
let sectionId: ItemListSectionId
|
||||
let action: () -> Void
|
||||
|
||||
init(theme: PresentationTheme, title: String, enabled: Bool, tag: ItemListItemTag?, sectionId: ItemListSectionId, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.enabled = enabled
|
||||
self.tag = tag
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = CreatePollOptionActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? CreatePollOptionActionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool {
|
||||
return self.enabled
|
||||
}
|
||||
|
||||
func selected(listView: ListView){
|
||||
listView.clearHighlightAnimated(true)
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
|
||||
private var item: CreatePollOptionActionItem?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: CreatePollOptionActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedIcon: UIImage?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
updatedIcon = PresentationResourcesItemList.addPhoneIcon(item.theme)
|
||||
}
|
||||
let leftInset: CGFloat = 60.0 + params.leftInset
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
let contentSize = CGSize(width: params.width, height: 44.0)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
if let updatedIcon = updatedIcon {
|
||||
strongSelf.iconNode.image = updatedIcon
|
||||
}
|
||||
if let image = strongSelf.iconNode.image {
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 3.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -1,539 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import CheckNode
|
||||
|
||||
struct CreatePollOptionItemEditing {
|
||||
let editable: Bool
|
||||
let hasActiveRevealControls: Bool
|
||||
}
|
||||
|
||||
class CreatePollOptionItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let id: Int
|
||||
let placeholder: String
|
||||
let value: String
|
||||
let isSelected: Bool?
|
||||
let maxLength: Int
|
||||
let editing: CreatePollOptionItemEditing
|
||||
let sectionId: ItemListSectionId
|
||||
let setItemIdWithRevealedOptions: (Int?, Int?) -> Void
|
||||
let updated: (String, Bool) -> Void
|
||||
let next: (() -> Void)?
|
||||
let delete: (Bool) -> Void
|
||||
let canDelete: Bool
|
||||
let canMove: Bool
|
||||
let focused: (Bool) -> Void
|
||||
let toggleSelected: () -> Void
|
||||
let tag: ItemListItemTag?
|
||||
|
||||
init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String, Bool) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, canMove: Bool, focused: @escaping (Bool) -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) {
|
||||
self.presentationData = presentationData
|
||||
self.id = id
|
||||
self.placeholder = placeholder
|
||||
self.value = value
|
||||
self.isSelected = isSelected
|
||||
self.maxLength = maxLength
|
||||
self.editing = editing
|
||||
self.sectionId = sectionId
|
||||
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
|
||||
self.updated = updated
|
||||
self.next = next
|
||||
self.delete = delete
|
||||
self.canDelete = canDelete
|
||||
self.canMove = canMove
|
||||
self.focused = focused
|
||||
self.toggleSelected = toggleSelected
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = CreatePollOptionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(.None) })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? CreatePollOptionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animation)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = false
|
||||
}
|
||||
|
||||
private let titleFont = Font.regular(15.0)
|
||||
|
||||
class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private var checkNode: InteractiveCheckNode?
|
||||
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private let textNode: EditableTextNode
|
||||
private let measureTextNode: TextNode
|
||||
|
||||
private let textLimitNode: TextNode
|
||||
private let reorderControlNode: ItemListEditableReorderControlNode
|
||||
|
||||
private var item: CreatePollOptionItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
override var controlsContainer: ASDisplayNode {
|
||||
return self.containerNode
|
||||
}
|
||||
|
||||
var checkNodeFrame: CGRect? {
|
||||
guard let _ = self.layoutParams, let checkNode = self.checkNode else {
|
||||
return nil
|
||||
}
|
||||
return checkNode.frame
|
||||
}
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.clipsToBounds = true
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.reorderControlNode = ItemListEditableReorderControlNode()
|
||||
|
||||
self.textClippingNode = ASDisplayNode()
|
||||
self.textClippingNode.clipsToBounds = true
|
||||
|
||||
self.textNode = EditableTextNode()
|
||||
self.measureTextNode = TextNode()
|
||||
|
||||
self.textLimitNode = TextNode()
|
||||
self.textLimitNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.textClippingNode.addSubnode(self.textNode)
|
||||
self.containerNode.addSubnode(self.textClippingNode)
|
||||
|
||||
self.containerNode.addSubnode(self.reorderControlNode)
|
||||
self.containerNode.addSubnode(self.textLimitNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
var textColor: UIColor = .black
|
||||
if let item = self.item {
|
||||
textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
}
|
||||
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.delegate = self
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.focused(true)
|
||||
}
|
||||
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: true)
|
||||
self.item?.focused(false)
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
guard let item = self.item else {
|
||||
return false
|
||||
}
|
||||
if text.firstIndex(of: "\n") != nil {
|
||||
if text != "\n" {
|
||||
let currentText = editableTextNode.attributedText?.string ?? ""
|
||||
var updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
|
||||
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
|
||||
if updatedText.count == 1 {
|
||||
updatedText = ""
|
||||
}
|
||||
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
self.editableTextNodeDidUpdateText(editableTextNode)
|
||||
}
|
||||
if let next = item.next {
|
||||
next()
|
||||
} else {
|
||||
editableTextNode.resignFirstResponder()
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: false)
|
||||
}
|
||||
|
||||
private func internalEditableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode, isLosingFocus: Bool) {
|
||||
if let item = self.item {
|
||||
let text = self.textNode.attributedText ?? NSAttributedString()
|
||||
|
||||
var updatedText = text.string
|
||||
var hadReturn = false
|
||||
if updatedText.firstIndex(of: "\n") != nil {
|
||||
hadReturn = true
|
||||
updatedText = updatedText.replacingOccurrences(of: "\n", with: " ")
|
||||
}
|
||||
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
if text.string != updatedAttributedText.string {
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
}
|
||||
item.updated(updatedText, !isLosingFocus && editableTextNode.isFirstResponder())
|
||||
if hadReturn {
|
||||
if let next = item.next {
|
||||
next()
|
||||
} else if !isLosingFocus {
|
||||
editableTextNode.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editableTextNodeBackspaceWhileEmpty(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.delete(editableTextNode.isFirstResponder())
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
|
||||
let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme)
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
|
||||
let leftInset: CGFloat = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
|
||||
let rightInset: CGFloat = 44.0 + params.rightInset
|
||||
|
||||
let textLength = item.value.count
|
||||
let displayTextLimit = textLength > item.maxLength * 70 / 100
|
||||
let remainingCount = item.maxLength - textLength
|
||||
|
||||
let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var measureText = item.value
|
||||
if measureText.hasSuffix("\n") || measureText.isEmpty {
|
||||
measureText += "|"
|
||||
}
|
||||
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black)
|
||||
let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset)
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
return (layout, { [weak self] animation in
|
||||
if let strongSelf = self {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
switch animation {
|
||||
case .System:
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
default:
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
|
||||
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
|
||||
}
|
||||
}
|
||||
|
||||
let revealOffset = strongSelf.revealOffset
|
||||
|
||||
let capitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let keyboardType: UIKeyboardType
|
||||
|
||||
capitalizationType = .sentences
|
||||
autocorrectionType = .default
|
||||
keyboardType = UIKeyboardType.default
|
||||
|
||||
let _ = textApply()
|
||||
if let currentText = strongSelf.textNode.attributedText {
|
||||
if currentText.string != attributedText.string || updatedTheme != nil {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
} else {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
}
|
||||
|
||||
if strongSelf.textNode.keyboardType != keyboardType {
|
||||
strongSelf.textNode.keyboardType = keyboardType
|
||||
}
|
||||
if strongSelf.textNode.autocapitalizationType != capitalizationType {
|
||||
strongSelf.textNode.autocapitalizationType = capitalizationType
|
||||
}
|
||||
if strongSelf.textNode.autocorrectionType != autocorrectionType {
|
||||
strongSelf.textNode.autocorrectionType = autocorrectionType
|
||||
}
|
||||
let returnKeyType: UIReturnKeyType
|
||||
if let _ = item.next {
|
||||
returnKeyType = .next
|
||||
} else {
|
||||
returnKeyType = .done
|
||||
}
|
||||
if strongSelf.textNode.returnKeyType != returnKeyType {
|
||||
strongSelf.textNode.returnKeyType = returnKeyType
|
||||
}
|
||||
|
||||
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
|
||||
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
|
||||
}
|
||||
|
||||
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + 16.0, y: floor((layout.contentSize.height - checkSize.height) / 2.0)), size: checkSize)
|
||||
if let isSelected = item.isSelected {
|
||||
if let checkNode = strongSelf.checkNode {
|
||||
transition.updateFrame(node: checkNode, frame: checkFrame)
|
||||
checkNode.setSelected(isSelected, animated: true)
|
||||
} else {
|
||||
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, strokeColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: item.presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
checkNode.setSelected(isSelected, animated: false)
|
||||
checkNode.valueChanged = { [weak self] value in
|
||||
self?.item?.toggleSelected()
|
||||
}
|
||||
strongSelf.checkNode = checkNode
|
||||
strongSelf.containerNode.addSubnode(checkNode)
|
||||
checkNode.frame = checkFrame
|
||||
transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0))
|
||||
}
|
||||
|
||||
if let checkNode = strongSelf.checkNode {
|
||||
transition.updateAlpha(node: checkNode, alpha: strongSelf.textNode.textView.text.isEmpty && item.placeholder == item.presentationData.strings.CreatePoll_AddOption ? 0.0 : 1.0)
|
||||
}
|
||||
} else if let checkNode = strongSelf.checkNode {
|
||||
strongSelf.checkNode = nil
|
||||
transition.updateFrame(node: checkNode, frame: checkFrame.offsetBy(dx: -checkFrame.maxX, dy: 0.0), completion: { [weak checkNode] _ in
|
||||
checkNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.textClippingNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)))
|
||||
transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0)))
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let bottomStripeWasHidden = strongSelf.bottomStripeNode.isHidden
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
|
||||
if strongSelf.animationForKey("apparentHeight") == nil {
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
let previousX = strongSelf.bottomStripeNode.frame.minX
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
|
||||
if !bottomStripeWasHidden {
|
||||
transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0))
|
||||
}
|
||||
} else {
|
||||
let previousX = strongSelf.bottomStripeNode.frame.minX
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: strongSelf.bottomStripeNode.frame.minY), size: CGSize(width: layout.contentSize.width, height: separatorHeight))
|
||||
if !bottomStripeWasHidden {
|
||||
transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit, transition)
|
||||
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0 + (!item.canMove ? 44.0 + params.rightInset : 0.0), y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height))
|
||||
transition.updateFrameAdditive(node: strongSelf.reorderControlNode, frame: reorderControlFrame)
|
||||
strongSelf.reorderControlNode.isUserInteractionEnabled = item.canMove
|
||||
|
||||
let _ = textLimitApply()
|
||||
strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size)
|
||||
strongSelf.textLimitNode.isHidden = !displayTextLimit
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
strongSelf.setRevealOptions((left: [], right: item.canDelete ? [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] : []))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
guard let params = self.layoutParams, let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
let revealOffset = offset
|
||||
|
||||
let leftInset: CGFloat
|
||||
leftInset = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0)
|
||||
|
||||
if let checkNode = self.checkNode {
|
||||
var checkNodeFrame = checkNode.frame
|
||||
checkNodeFrame.origin.x = params.leftInset + 11.0 + revealOffset
|
||||
transition.updateFrame(node: checkNode, frame: checkNodeFrame)
|
||||
}
|
||||
|
||||
var reorderFrame = self.reorderControlNode.frame
|
||||
reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width
|
||||
transition.updateFrame(node: self.reorderControlNode, frame: reorderFrame)
|
||||
|
||||
var textClippingNodeFrame = self.textClippingNode.frame
|
||||
textClippingNodeFrame.origin.x = revealOffset + leftInset
|
||||
transition.updateFrame(node: self.textClippingNode, frame: textClippingNodeFrame)
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.updateRevealOffsetInternal(offset: -self.bounds.width - 74.0, transition: .animated(duration: 0.2, curve: .spring), completion: { [weak self] in
|
||||
self?.layer.allowsGroupOpacity = false
|
||||
})
|
||||
self.item?.delete(self.textNode.isFirstResponder())
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
//self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
//self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func focus() {
|
||||
self.textNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func selectAll() {
|
||||
self.textNode.textView.selectAll(nil)
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if self.reorderControlNode.frame.contains(point), !self.reorderControlNode.isHidden, !self.isDisplayingRevealedOptions {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
super.animateFrameTransition(progress, currentValue)
|
||||
|
||||
var separatorFrame = self.bottomStripeNode.frame
|
||||
separatorFrame.origin.y = currentValue - UIScreenPixel
|
||||
self.bottomStripeNode.frame = separatorFrame
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue))
|
||||
|
||||
let insets = self.insets
|
||||
let separatorHeight = UIScreenPixel
|
||||
guard let params = self.layoutParams else {
|
||||
return
|
||||
}
|
||||
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: self.containerNode.bounds.width, height: currentValue + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
}
|
||||
}
|
@ -1,683 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import TextFormat
|
||||
import ObjCRuntimeUtils
|
||||
import TextInputMenu
|
||||
import AccountContext
|
||||
|
||||
public enum CreatePollTextInputItemTextLimitMode {
|
||||
case characters
|
||||
case bytes
|
||||
}
|
||||
|
||||
public struct CreatePollTextInputItemTextLimit {
|
||||
public let value: Int
|
||||
public let display: Bool
|
||||
public let mode: CreatePollTextInputItemTextLimitMode
|
||||
|
||||
public init(value: Int, display: Bool, mode: CreatePollTextInputItemTextLimitMode = .characters) {
|
||||
self.value = value
|
||||
self.display = display
|
||||
self.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
public struct ItemListMultilineInputInlineAction {
|
||||
public let icon: UIImage
|
||||
public let action: (() -> Void)?
|
||||
|
||||
public init(icon: UIImage, action: (() -> Void)?) {
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public class CreatePollTextInputItem: ListViewItem, ItemListItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let text: NSAttributedString
|
||||
let placeholder: String
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let capitalization: Bool
|
||||
let autocorrection: Bool
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let action: (() -> Void)?
|
||||
let textUpdated: (NSAttributedString) -> Void
|
||||
let shouldUpdateText: (String) -> Bool
|
||||
let processPaste: ((String) -> Void)?
|
||||
let updatedFocus: ((Bool) -> Void)?
|
||||
let maxLength: CreatePollTextInputItemTextLimit?
|
||||
let minimalHeight: CGFloat?
|
||||
let inlineAction: ItemListMultilineInputInlineAction?
|
||||
public let tag: ItemListItemTag?
|
||||
|
||||
public init(context: AccountContext, presentationData: ItemListPresentationData, text: NSAttributedString, placeholder: String, maxLength: CreatePollTextInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (NSAttributedString) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.text = text
|
||||
self.placeholder = placeholder
|
||||
self.maxLength = maxLength
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.capitalization = capitalization
|
||||
self.autocorrection = autocorrection
|
||||
self.returnKeyType = returnKeyType
|
||||
self.minimalHeight = minimalHeight
|
||||
self.textUpdated = textUpdated
|
||||
self.shouldUpdateText = shouldUpdateText
|
||||
self.processPaste = processPaste
|
||||
self.updatedFocus = updatedFocus
|
||||
self.tag = tag
|
||||
self.action = action
|
||||
self.inlineAction = inlineAction
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = CreatePollTextInputItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? CreatePollTextInputItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let textClippingNode: ASDisplayNode
|
||||
private let textNode: EditableTextNode
|
||||
private let measureTextNode: TextNode
|
||||
|
||||
private let limitTextNode: TextNode
|
||||
private var inlineActionButtonNode: HighlightableButtonNode?
|
||||
|
||||
private var item: CreatePollTextInputItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
private let inputMenu = TextInputMenu()
|
||||
|
||||
public var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.textClippingNode = ASDisplayNode()
|
||||
self.textClippingNode.clipsToBounds = true
|
||||
|
||||
self.textNode = EditableTextNode()
|
||||
self.measureTextNode = TextNode()
|
||||
|
||||
self.limitTextNode = TextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.textClippingNode.addSubnode(self.textNode)
|
||||
self.addSubnode(self.textClippingNode)
|
||||
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
var textColor: UIColor = .black
|
||||
if let item = self.item {
|
||||
textColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]
|
||||
} else {
|
||||
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
|
||||
}
|
||||
self.textNode.clipsToBounds = true
|
||||
self.textNode.delegate = self
|
||||
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: CreatePollTextInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
|
||||
let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.rightInset
|
||||
switch item.style {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
}
|
||||
|
||||
var limitTextString: NSAttributedString?
|
||||
var rightInset: CGFloat = params.rightInset
|
||||
|
||||
if let maxLength = item.maxLength, maxLength.display {
|
||||
let textLength: Int
|
||||
switch maxLength.mode {
|
||||
case .characters:
|
||||
textLength = item.text.string.count
|
||||
case .bytes:
|
||||
textLength = item.text.string.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
|
||||
}
|
||||
let displayTextLimit = textLength > maxLength.value * 70 / 100
|
||||
let remainingCount = maxLength.value - textLength
|
||||
if displayTextLimit {
|
||||
limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
|
||||
rightInset += 30.0 + 4.0
|
||||
}
|
||||
|
||||
let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
if limitTextLayout.size.width > 30.0 {
|
||||
rightInset += 30.0
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
rightInset += inlineAction.icon.size.width + 8.0
|
||||
}
|
||||
|
||||
let itemText = textAttributedStringForStateText(context: item.context, stateText: item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
let measureText = NSMutableAttributedString(attributedString: itemText)
|
||||
let measureRawString = measureText.string
|
||||
if measureRawString.hasSuffix("\n") || measureRawString.isEmpty {
|
||||
measureText.append(NSAttributedString(string: "|", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black))
|
||||
}
|
||||
let attributedText = itemText
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: measureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
var contentHeight: CGFloat = textLayout.size.height + textTopInset + textBottomInset
|
||||
if let minimalHeight = item.minimalHeight {
|
||||
contentHeight = max(minimalHeight, contentHeight)
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: contentHeight)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor]
|
||||
strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
|
||||
}
|
||||
|
||||
strongSelf.inputMenu.updateStrings(item.presentationData.strings)
|
||||
}
|
||||
|
||||
let capitalizationType: UITextAutocapitalizationType = item.capitalization ? .sentences : .none
|
||||
let autocorrectionType: UITextAutocorrectionType = item.autocorrection ? .default : .no
|
||||
|
||||
if strongSelf.textNode.textView.autocapitalizationType != capitalizationType {
|
||||
strongSelf.textNode.textView.autocapitalizationType = capitalizationType
|
||||
}
|
||||
if strongSelf.textNode.textView.autocorrectionType != autocorrectionType {
|
||||
strongSelf.textNode.textView.autocorrectionType = autocorrectionType
|
||||
}
|
||||
if strongSelf.textNode.textView.returnKeyType != item.returnKeyType {
|
||||
strongSelf.textNode.textView.returnKeyType = item.returnKeyType
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
if let currentText = strongSelf.textNode.attributedText {
|
||||
if currentText.string != attributedText.string || updatedTheme != nil {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
}
|
||||
} else {
|
||||
strongSelf.textNode.attributedText = attributedText
|
||||
refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
|
||||
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
|
||||
}
|
||||
|
||||
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
|
||||
|
||||
if strongSelf.animationForKey("apparentHeight") == nil {
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))
|
||||
}
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0))
|
||||
|
||||
let _ = limitTextApply()
|
||||
strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: layout.contentSize.height - 15.0 - limitTextLayout.size.height), size: limitTextLayout.size)
|
||||
if limitTextString != nil {
|
||||
if strongSelf.limitTextNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.limitTextNode)
|
||||
}
|
||||
} else if strongSelf.limitTextNode.supernode != nil {
|
||||
strongSelf.limitTextNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let inlineAction = item.inlineAction {
|
||||
let inlineActionButtonNode: HighlightableButtonNode
|
||||
if let currentInlineActionButtonNode = strongSelf.inlineActionButtonNode {
|
||||
inlineActionButtonNode = currentInlineActionButtonNode
|
||||
} else {
|
||||
inlineActionButtonNode = HighlightableButtonNode()
|
||||
inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal)
|
||||
inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside)
|
||||
strongSelf.addSubnode(inlineActionButtonNode)
|
||||
strongSelf.inlineActionButtonNode = inlineActionButtonNode
|
||||
}
|
||||
inlineActionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - inlineAction.icon.size.width - 11.0, y: 7.0), size: inlineAction.icon.size)
|
||||
} else if let inlineActionButtonNode = strongSelf.inlineActionButtonNode {
|
||||
inlineActionButtonNode.removeFromSupernode()
|
||||
strongSelf.inlineActionButtonNode = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
super.animateFrameTransition(progress, currentValue)
|
||||
|
||||
guard let params = self.layoutParams else {
|
||||
return
|
||||
}
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
let insets = self.insets
|
||||
let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom))
|
||||
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
let textTopInset: CGFloat = 11.0
|
||||
let textBottomInset: CGFloat = 11.0
|
||||
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight))
|
||||
|
||||
self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset)))
|
||||
}
|
||||
|
||||
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.updatedFocus?(true)
|
||||
self.inputMenu.activate()
|
||||
}
|
||||
|
||||
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.item?.updatedFocus?(false)
|
||||
self.inputMenu.deactivate()
|
||||
}
|
||||
|
||||
public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? {
|
||||
if action == makeSelectorFromString("_showTextStyleOptions:") {
|
||||
if #available(iOS 16.0, *) {
|
||||
return ASEditableTextNodeTargetForAction(target: nil)
|
||||
} else {
|
||||
if case .general = self.inputMenu.state {
|
||||
if self.textNode.attributedText == nil || self.textNode.attributedText!.length == 0 || self.textNode.selectedRange.length == 0 {
|
||||
return ASEditableTextNodeTargetForAction(target: nil)
|
||||
}
|
||||
return ASEditableTextNodeTargetForAction(target: self)
|
||||
} else {
|
||||
return ASEditableTextNodeTargetForAction(target: nil)
|
||||
}
|
||||
}
|
||||
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) {
|
||||
if case .format = self.inputMenu.state {
|
||||
return ASEditableTextNodeTargetForAction(target: self)
|
||||
} else {
|
||||
return ASEditableTextNodeTargetForAction(target: nil)
|
||||
}
|
||||
}
|
||||
if case .format = self.inputMenu.state {
|
||||
return ASEditableTextNodeTargetForAction(target: nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc func _showTextStyleOptions(_ sender: Any) {
|
||||
self.inputMenu.format(view: self.textNode.view, rect: self.textNode.selectionRect.offsetBy(dx: 0.0, dy: -self.textNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0))
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
|
||||
var actions = suggestedActions
|
||||
|
||||
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
|
||||
|
||||
} else if let strings = self.item?.presentationData.strings {
|
||||
let children: [UIAction] = [
|
||||
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesBold(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesItalic(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesMonospace(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesLink(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesStrikethrough(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesUnderline(strongSelf)
|
||||
}
|
||||
},
|
||||
UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] (action) in
|
||||
if let strongSelf = self {
|
||||
strongSelf.formatAttributesSpoiler(strongSelf)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: children)
|
||||
actions.insert(formatMenu, at: 3)
|
||||
}
|
||||
|
||||
return UIMenu(children: actions)
|
||||
}
|
||||
|
||||
@objc func formatAttributesBold(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.bold, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesItalic(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.italic, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesMonospace(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.monospace, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesLink(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
//self.interfaceInteraction?.openLinkEditing()
|
||||
}
|
||||
|
||||
@objc func formatAttributesStrikethrough(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.strikethrough, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesUnderline(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.underline, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesSpoiler(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.spoiler, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesQuote(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func formatAttributesCodeBlock(_ sender: Any) {
|
||||
self.inputMenu.back()
|
||||
if let item = self.item {
|
||||
chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false))
|
||||
}
|
||||
}
|
||||
|
||||
public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if let item = self.item {
|
||||
if text.count > 1, let processPaste = item.processPaste {
|
||||
processPaste(text)
|
||||
return false
|
||||
}
|
||||
|
||||
if let action = item.action, text == "\n" {
|
||||
action()
|
||||
return false
|
||||
}
|
||||
|
||||
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||
if !item.shouldUpdateText(newText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let item = self.item {
|
||||
if let _ = self.textNode.attributedText {
|
||||
refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
let updatedText = stateAttributedStringForText(self.textNode.attributedText!)
|
||||
item.textUpdated(updatedText)
|
||||
} else {
|
||||
item.textUpdated(NSAttributedString(string: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
|
||||
if let _ = self.item {
|
||||
let text: String? = UIPasteboard.general.string
|
||||
if let _ = text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
|
||||
/*if !dueToEditing && !self.updatingInputState {
|
||||
}*/
|
||||
|
||||
if let item = self.item {
|
||||
if case .format = self.inputMenu.state {
|
||||
self.inputMenu.deactivate()
|
||||
UIMenuController.shared.update()
|
||||
}
|
||||
|
||||
refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0)
|
||||
refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func focus() {
|
||||
if !self.textNode.textView.isFirstResponder {
|
||||
self.textNode.textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
public func selectAll() {
|
||||
self.textNode.textView.selectAll(nil)
|
||||
}
|
||||
|
||||
public func animateError() {
|
||||
self.textNode.layer.addShakeAnimation()
|
||||
}
|
||||
|
||||
@objc private func inlineActionPressed() {
|
||||
if let action = self.item?.inlineAction?.action {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode: EditableTextNode, theme: PresentationTheme, attribute: NSAttributedString.Key, value: Any?) {
|
||||
if let currentText = textNode.attributedText, textNode.selectedRange.length > 0 {
|
||||
let nsRange = NSRange(location: textNode.selectedRange.location, length: textNode.selectedRange.length)
|
||||
var addAttribute = true
|
||||
var attributesToRemove: [NSAttributedString.Key] = []
|
||||
currentText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||
for (key, _) in attributes {
|
||||
if key == attribute && range == nsRange {
|
||||
addAttribute = false
|
||||
attributesToRemove.append(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = NSMutableAttributedString(attributedString: currentText)
|
||||
for attribute in attributesToRemove {
|
||||
result.removeAttribute(attribute, range: nsRange)
|
||||
}
|
||||
if addAttribute {
|
||||
result.addAttribute(attribute, value: true as Bool, range: nsRange)
|
||||
}
|
||||
|
||||
textNode.attributedText = result
|
||||
textNode.selectedRange = nsRange
|
||||
|
||||
refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0)
|
||||
refreshGenericTextInputAttributes(context: item.context, textView: textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil)
|
||||
|
||||
let updatedText = stateAttributedStringForText(textNode.attributedText!)
|
||||
item.textUpdated(updatedText)
|
||||
}
|
||||
}
|
@ -60,9 +60,12 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/ToastComponent",
|
||||
"//submodules/SemanticStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -33,6 +33,9 @@ import RasterizedCompositionComponent
|
||||
import BadgeComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import ToastComponent
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public enum UniversalVideoGalleryItemContentInfo {
|
||||
case message(Message, Int?)
|
||||
@ -1390,8 +1393,94 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if dismiss {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
if self.adDisposable == nil, let contentInfo = self.item?.contentInfo, case let .message(message, _) = contentInfo {
|
||||
let adContext = self.context.engine.messages.adMessages(peerId: message.id.peerId, messageId: message.id)
|
||||
self.adContext = adContext
|
||||
self.adDisposable = (adContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let message = state.messages.first {
|
||||
Queue.mainQueue().after(2.0, {
|
||||
self.adMessage = message
|
||||
if let validLayout = self.validLayout {
|
||||
self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let adMessage = self.adMessage {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let title = adMessage.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
||||
|
||||
let adSize = self.adView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
ToastContentComponent(
|
||||
icon: AnyComponent(
|
||||
BundleIconComponent(name: "Components/AdMock", tintColor: nil, maxSize: CGSize(width: 30.0, height: 30.0))
|
||||
),
|
||||
content: AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
VStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white))))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(Image(image: PresentationResourcesChatList.searchAdIcon(presentationData.theme, strings: presentationData.strings), size: CGSize(width: 31.0, height: 15.0))))
|
||||
], spacing: 5.0)
|
||||
)),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: adMessage.text, font: Font.regular(14.0), textColor: .white)))
|
||||
))
|
||||
], alignment: .left, spacing: 3.0, fillWidth: false)
|
||||
)),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(
|
||||
AdRemainingProgressComponent(action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.adMessage = nil
|
||||
if let validLayout = self.validLayout {
|
||||
self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
})
|
||||
))
|
||||
], spacing: 16.0, alignment: .alternatingLeftRight)
|
||||
), action: { [weak self] in
|
||||
if let self, let item = self.item, let ad = adMessage.adAttribute {
|
||||
item.performAction(.url(url: ad.url, concealed: false))
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width - sideInset * 2.0, height: 70.0)
|
||||
)
|
||||
if let adView = self.adView.view {
|
||||
if adView.superview == nil {
|
||||
self.view.addSubview(adView)
|
||||
|
||||
adView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
adView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
adView.frame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - adSize.height - 145.0), size: adSize)
|
||||
}
|
||||
} else if let adView = self.adView.view {
|
||||
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
private var adView = ComponentView<Empty>()
|
||||
private var adContext: AdMessagesHistoryContext?
|
||||
private var adDisposable: Disposable?
|
||||
private var adMessage: Message?
|
||||
|
||||
func setupItem(_ item: UniversalVideoGalleryItem) {
|
||||
if self.item?.content.id != item.content.id {
|
||||
var chapters = parseMediaPlayerChapters(item.caption)
|
||||
|
@ -1118,6 +1118,26 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
|
||||
//TODO:localize
|
||||
availableItems[.todo] = DemoPagerComponent.Item(
|
||||
AnyComponentWithIdentity(
|
||||
id: PremiumDemoScreen.Subject.todo,
|
||||
component: AnyComponent(
|
||||
PageComponent(
|
||||
content: AnyComponent(PhoneDemoComponent(
|
||||
context: component.context,
|
||||
position: .top,
|
||||
videoFile: configuration.videos["todo"],
|
||||
decoration: .badgeStars
|
||||
)),
|
||||
title: "To-Do Lists",
|
||||
text: "Plan, assign and complete tasks – seamlessly and efficiently.",
|
||||
textColor: textColor
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let index: Int = 0
|
||||
var items: [DemoPagerComponent.Item] = []
|
||||
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
|
||||
@ -1216,6 +1236,9 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
text = strings.Premium_MessageEffectsInfo
|
||||
case .paidMessages:
|
||||
text = strings.Premium_PaidMessagesInfo
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
text = "Plan, assign and complete tasks – seamlessly and efficiently."
|
||||
default:
|
||||
text = ""
|
||||
}
|
||||
@ -1302,6 +1325,8 @@ private final class DemoSheetContent: CombinedComponent {
|
||||
buttonAnimationName = "premium_unlock"
|
||||
case .paidMessages:
|
||||
buttonText = strings.Premium_PaidMessages_Proceed
|
||||
case .todo:
|
||||
buttonText = strings.Premium_PaidMessages_Proceed
|
||||
default:
|
||||
buttonText = strings.Common_OK
|
||||
}
|
||||
@ -1492,6 +1517,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
@ -1552,6 +1578,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
||||
return .messageEffects
|
||||
case .paidMessages:
|
||||
return .paidMessages
|
||||
case .todo:
|
||||
return .todo
|
||||
case .businessLocation:
|
||||
return .businessLocation
|
||||
case .businessHours:
|
||||
|
@ -309,6 +309,12 @@ public enum PremiumSource: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .todo:
|
||||
if case .todo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .auth(lhsPrice):
|
||||
if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice {
|
||||
return true
|
||||
@ -363,6 +369,7 @@ public enum PremiumSource: Equatable {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
case auth(String)
|
||||
|
||||
var identifier: String? {
|
||||
@ -459,6 +466,8 @@ public enum PremiumSource: Equatable {
|
||||
return "effects"
|
||||
case .paidMessages:
|
||||
return "paid_messages"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .auth:
|
||||
return "auth"
|
||||
}
|
||||
@ -490,6 +499,7 @@ public enum PremiumPerk: CaseIterable {
|
||||
case folderTags
|
||||
case messageEffects
|
||||
case paidMessages
|
||||
case todo
|
||||
|
||||
case businessLocation
|
||||
case businessHours
|
||||
@ -601,6 +611,8 @@ public enum PremiumPerk: CaseIterable {
|
||||
return "effects"
|
||||
case .paidMessages:
|
||||
return "paid_messages"
|
||||
case .todo:
|
||||
return "todo"
|
||||
case .business:
|
||||
return "business"
|
||||
case .businessLocation:
|
||||
@ -672,6 +684,9 @@ public enum PremiumPerk: CaseIterable {
|
||||
return strings.Premium_MessageEffects
|
||||
case .paidMessages:
|
||||
return strings.Premium_PaidMessages
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
return "To-Do Lists"
|
||||
case .businessLocation:
|
||||
return strings.Business_Location
|
||||
case .businessHours:
|
||||
@ -741,6 +756,9 @@ public enum PremiumPerk: CaseIterable {
|
||||
return strings.Premium_MessageEffectsInfo
|
||||
case .paidMessages:
|
||||
return strings.Premium_PaidMessagesInfo
|
||||
case .todo:
|
||||
//TODO:localize
|
||||
return "Plan, assign and complete tasks – seamlessly and efficiently."
|
||||
case .businessLocation:
|
||||
return strings.Business_LocationInfo
|
||||
case .businessHours:
|
||||
@ -810,6 +828,8 @@ public enum PremiumPerk: CaseIterable {
|
||||
return "Premium/Perk/MessageEffects"
|
||||
case .paidMessages:
|
||||
return "Premium/Perk/PaidMessages"
|
||||
case .todo:
|
||||
return "Premium/Perk/PaidMessages"
|
||||
case .businessLocation:
|
||||
return "Premium/BusinessPerk/Location"
|
||||
case .businessHours:
|
||||
|
@ -131,4 +131,24 @@ public final class TelegramMediaTodo: Media, Equatable {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdated(items: [TelegramMediaTodo.Item]) -> TelegramMediaTodo {
|
||||
return TelegramMediaTodo(
|
||||
flags: self.flags,
|
||||
text: self.text,
|
||||
textEntities: self.textEntities,
|
||||
items: items,
|
||||
completions: self.completions
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdated(completions: [TelegramMediaTodo.Completion]) -> TelegramMediaTodo {
|
||||
return TelegramMediaTodo(
|
||||
flags: self.flags,
|
||||
text: self.text,
|
||||
textEntities: self.textEntities,
|
||||
items: self.items,
|
||||
completions: completions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -10,26 +10,36 @@ public enum RequestUpdateTodoMessageError {
|
||||
}
|
||||
|
||||
func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal<Never, RequestUpdateTodoMessageError> {
|
||||
return account.postbox.loadedPeerWithId(messageId.peerId)
|
||||
|> take(1)
|
||||
|> castError(RequestUpdateTodoMessageError.self)
|
||||
|> mapToSignal { peer -> Signal<Never, RequestUpdateTodoMessageError> in
|
||||
if let inputPeer = apiInputPeer(peer) {
|
||||
return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds))
|
||||
|> mapError { _ -> RequestUpdateTodoMessageError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Void, RequestUpdateTodoMessageError> in
|
||||
return account.postbox.transaction { transaction in
|
||||
account.stateManager.addUpdates(result)
|
||||
}
|
||||
|> castError(RequestUpdateTodoMessageError.self)
|
||||
}
|
||||
|> ignoreValues
|
||||
} else {
|
||||
return account.postbox.transaction { transaction -> Signal<Never, RequestUpdateTodoMessageError> in
|
||||
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
|
||||
return .complete()
|
||||
}
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var media: [Media] = []
|
||||
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
var updatedCompletions = todo.completions
|
||||
for id in completedIds {
|
||||
updatedCompletions.append(TelegramMediaTodo.Completion(id: id, date: timestamp, completedBy: account.peerId))
|
||||
}
|
||||
updatedCompletions.removeAll(where: { incompletedIds.contains($0.id) })
|
||||
media = [todo.withUpdated(completions: updatedCompletions)]
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
|
||||
})
|
||||
return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds))
|
||||
|> mapError { _ -> RequestUpdateTodoMessageError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result in
|
||||
account.stateManager.addUpdates(result)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|> castError(RequestUpdateTodoMessageError.self)
|
||||
|> switchToLatest
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public enum AppendTodoMessageError {
|
||||
@ -37,30 +47,30 @@ public enum AppendTodoMessageError {
|
||||
}
|
||||
|
||||
func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal<Never, AppendTodoMessageError> {
|
||||
return account.postbox.loadedPeerWithId(messageId.peerId)
|
||||
|> take(1)
|
||||
|> castError(AppendTodoMessageError.self)
|
||||
|> mapToSignal { peer -> Signal<TelegramMediaTodo?, AppendTodoMessageError> in
|
||||
guard let inputPeer = apiInputPeer(peer) else {
|
||||
return .single(nil)
|
||||
return account.postbox.transaction { transaction -> Signal<Never, AppendTodoMessageError> in
|
||||
guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else {
|
||||
return .complete()
|
||||
}
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var media: [Media] = []
|
||||
if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo {
|
||||
var updatedItems = todo.items
|
||||
updatedItems.append(contentsOf: items)
|
||||
media = [todo.withUpdated(items: updatedItems)]
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media))
|
||||
})
|
||||
return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem }))
|
||||
|> mapError { _ -> AppendTodoMessageError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<TelegramMediaTodo?, AppendTodoMessageError> in
|
||||
return account.postbox.transaction { transaction -> TelegramMediaTodo? in
|
||||
switch result {
|
||||
case let .updates(updates, _, _, _, _):
|
||||
let _ = updates
|
||||
default:
|
||||
break
|
||||
}
|
||||
account.stateManager.addUpdates(result)
|
||||
return nil
|
||||
}
|
||||
|> castError(AppendTodoMessageError.self)
|
||||
|> map { result in
|
||||
account.stateManager.addUpdates(result)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|> castError(AppendTodoMessageError.self)
|
||||
|> switchToLatest
|
||||
|> ignoreValues
|
||||
}
|
||||
|
@ -168,6 +168,11 @@ public enum PresentationResourceKey: Int32 {
|
||||
case chatBubbleFileCloudFetchedIncomingIcon
|
||||
case chatBubbleFileCloudFetchedOutgoingIcon
|
||||
|
||||
case chatBubbleTodoDotIncomingIcon
|
||||
case chatBubbleTodoDotOutgoingIcon
|
||||
case chatBubbleTodoCheckIncomingIcon
|
||||
case chatBubbleTodoCheckOutgoingIcon
|
||||
|
||||
case chatBubbleReplyThumbnailPlayImage
|
||||
|
||||
case chatBubbleDeliveryFailedIcon
|
||||
|
@ -1402,4 +1402,28 @@ public struct PresentationResourcesChat {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleTodoDotIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleTodoDotIncomingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.incoming.accentTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleTodoDotOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleTodoDotOutgoingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.outgoing.accentTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleTodoCheckIncomingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleTodoCheckIncomingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.incoming.accentTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleTodoCheckOutgoingIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleTodoCheckOutgoingIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.outgoing.accentTextColor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1286,12 +1286,81 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
case let .todoCompletions(completed, incompleted):
|
||||
//TODO:release
|
||||
let _ = completed
|
||||
let _ = incompleted
|
||||
var todo: TelegramMediaTodo?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaTodo {
|
||||
todo = media
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let todo {
|
||||
if message.author?.id == accountPeerId {
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
|
||||
resultString = strings.Notification_TodoCompletedYou(completedTask.text)
|
||||
} else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) {
|
||||
resultString = strings.Notification_TodoIncompletedYou(incompletedTask.text)
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
} else {
|
||||
let peerName = message.author?.compactDisplayTitle ?? ""
|
||||
|
||||
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
|
||||
attributes[1] = boldAttributes
|
||||
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
|
||||
resultString = strings.Notification_TodoCompleted(peerName, completedTask.text)
|
||||
} else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) {
|
||||
resultString = strings.Notification_TodoIncompleted(peerName, incompletedTask.text)
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
} else {
|
||||
attributedString = NSAttributedString(string: ".")
|
||||
}
|
||||
case let .todoAppendTasks(tasks):
|
||||
var todoTitle = ""
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
|
||||
for media in message.media {
|
||||
if let todo = media as? TelegramMediaTodo {
|
||||
todoTitle = todo.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if message.author?.id == accountPeerId {
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if tasks.count == 1, let task = tasks.first {
|
||||
resultString = strings.Notification_TodoAddedTaskYou(task.text, todoTitle)
|
||||
} else {
|
||||
resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
|
||||
} else {
|
||||
let peerName = message.author?.compactDisplayTitle ?? ""
|
||||
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
|
||||
attributes[1] = boldAttributes
|
||||
attributes[2] = boldAttributes
|
||||
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if tasks.count == 1, let task = tasks.first {
|
||||
resultString = strings.Notification_TodoAddedTask(peerName, task.text, todoTitle)
|
||||
} else {
|
||||
resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
break
|
||||
|
@ -481,6 +481,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
||||
"//submodules/TelegramUI/Components/GifVideoLayer",
|
||||
"//submodules/TelegramUI/Components/BatchVideoRendering",
|
||||
"//submodules/TelegramUI/Components/ComposeTodoScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -51,6 +51,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
|
||||
|
@ -41,6 +41,7 @@ import ChatMessageInteractiveFileNode
|
||||
import ChatMessageFileBubbleContentNode
|
||||
import ChatMessageWebpageBubbleContentNode
|
||||
import ChatMessagePollBubbleContentNode
|
||||
import ChatMessageTodoBubbleContentNode
|
||||
import ChatMessageItem
|
||||
import ChatMessageItemView
|
||||
import ChatMessageSwipeToReplyNode
|
||||
@ -269,6 +270,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
} else if let _ = media as? TelegramMediaPoll {
|
||||
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
} else if let _ = media as? TelegramMediaTodo {
|
||||
result.append((message, ChatMessageTodoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
} else if let _ = media as? TelegramMediaGiveaway {
|
||||
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
|
@ -449,10 +449,12 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
if "".isEmpty, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode {
|
||||
if let theme = strongSelf.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode {
|
||||
strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode"
|
||||
strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view)
|
||||
backdropNode.addSubnode(strongSelf.highlightedBackgroundNode)
|
||||
} else {
|
||||
strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
|
@ -0,0 +1,35 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageTodoBubbleContentNode",
|
||||
module_name = "ChatMessageTodoBubbleContentNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ChatMessageBackground",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
"//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode",
|
||||
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -647,6 +647,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
@ -502,6 +502,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))
|
||||
|
||||
|
@ -281,7 +281,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let playShakeAnimation: () -> Void
|
||||
public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void
|
||||
public let updateChatLocationThread: (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void
|
||||
|
||||
public let requestToggleTodoMessageItem: (MessageId, Int32, Bool) -> Void
|
||||
public let displayTodoToggleUnavailable: (MessageId) -> Void
|
||||
public var canPlayMedia: Bool = false
|
||||
public var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
public var expandedTranslationMessageStableIds: Set<UInt32> = Set()
|
||||
@ -444,6 +445,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
playShakeAnimation: @escaping () -> Void,
|
||||
displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void,
|
||||
updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void,
|
||||
requestToggleTodoMessageItem: @escaping (MessageId, Int32, Bool) -> Void,
|
||||
displayTodoToggleUnavailable: @escaping (MessageId) -> Void,
|
||||
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
|
||||
pollActionState: ChatInterfacePollActionState,
|
||||
stickerSettings: ChatInterfaceStickerSettings,
|
||||
@ -563,6 +566,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
self.playShakeAnimation = playShakeAnimation
|
||||
self.displayQuickShare = displayQuickShare
|
||||
self.updateChatLocationThread = updateChatLocationThread
|
||||
self.requestToggleTodoMessageItem = requestToggleTodoMessageItem
|
||||
self.displayTodoToggleUnavailable = displayTodoToggleUnavailable
|
||||
|
||||
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
|
||||
|
||||
|
51
submodules/TelegramUI/Components/ComposeTodoScreen/BUILD
Normal file
51
submodules/TelegramUI/Components/ComposeTodoScreen/BUILD
Normal file
@ -0,0 +1,51 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ComposeTodoScreen",
|
||||
module_name = "ComposeTodoScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AlertUI",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/ObjCRuntimeUtils",
|
||||
"//submodules/AttachmentUI",
|
||||
"//submodules/TextInputMenu",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
|
||||
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/ListComposePollOptionComponent",
|
||||
"//submodules/ComposePollUI",
|
||||
"//submodules/Markdown",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,30 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ListComposePollOptionComponent",
|
||||
module_name = "ListComposePollOptionComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/SliderComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
@ -73,6 +73,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let placeholder: NSAttributedString?
|
||||
public let isEnabled: Bool
|
||||
public let resetText: ResetText?
|
||||
public let assumeIsEditing: Bool
|
||||
public let characterLimit: Int?
|
||||
@ -92,6 +93,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
placeholder: NSAttributedString? = nil,
|
||||
isEnabled: Bool = true,
|
||||
resetText: ResetText? = nil,
|
||||
assumeIsEditing: Bool = false,
|
||||
characterLimit: Int,
|
||||
@ -110,6 +112,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.placeholder = placeholder
|
||||
self.isEnabled = isEnabled
|
||||
self.resetText = resetText
|
||||
self.assumeIsEditing = assumeIsEditing
|
||||
self.characterLimit = characterLimit
|
||||
@ -140,6 +143,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.resetText != rhs.resetText {
|
||||
return false
|
||||
}
|
||||
@ -400,6 +406,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.textField.parentState = state
|
||||
}
|
||||
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||
|
||||
transition.setAlpha(view: textFieldView, alpha: component.isEnabled ? 1.0 : 0.3)
|
||||
textFieldView.isUserInteractionEnabled = component.isEnabled
|
||||
}
|
||||
|
||||
if let selection = component.selection {
|
@ -76,6 +76,7 @@ public final class ListSectionContentView: UIView {
|
||||
private let contentItemContainerView: UIView
|
||||
|
||||
public let externalContentBackgroundView: DynamicCornerRadiusView
|
||||
public var automaticallyLayoutExternalContentBackgroundView = true
|
||||
|
||||
public var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private var highlightedItemId: AnyHashable?
|
||||
@ -283,7 +284,9 @@ public final class ListSectionContentView: UIView {
|
||||
}
|
||||
self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition)
|
||||
}
|
||||
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
|
||||
if self.automaticallyLayoutExternalContentBackgroundView {
|
||||
transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame)
|
||||
}
|
||||
transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha)
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius)
|
||||
|
||||
|
@ -435,6 +435,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
@ -3854,6 +3855,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in
|
||||
|
@ -823,6 +823,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, openBoostToUnrestrict: {
|
||||
}, updateRecordingTrimRange: { _, _, _, _ in
|
||||
}, dismissAllTooltips: {
|
||||
}, editTodoMessage: { _, _ in
|
||||
}, updateHistoryFilter: { _ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, toggleChatSidebarMode: {
|
||||
|
@ -835,11 +835,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
)
|
||||
))
|
||||
} else if case .unique = giftAnimationSubject {
|
||||
let reason: String
|
||||
if count < StarsAmount.zero, case let .transaction(transaction, _) = subject {
|
||||
if transaction.flags.contains(.isStarGiftResale) {
|
||||
reason = strings.Stars_Transaction_GiftPurchase
|
||||
} else {
|
||||
reason = strings.Stars_Transaction_GiftTransfer
|
||||
}
|
||||
} else {
|
||||
reason = strings.Stars_Transaction_GiftSale
|
||||
}
|
||||
tableItems.append(.init(
|
||||
id: "reason",
|
||||
title: strings.Stars_Transaction_Giveaway_Reason,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor)))
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: reason, font: tableFont, textColor: tableTextColor)))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
@ -328,7 +328,15 @@ final class StarsTransactionsListPanelComponent: Component {
|
||||
break
|
||||
}
|
||||
}
|
||||
itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase
|
||||
if item.count > StarsAmount.zero {
|
||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftSale
|
||||
} else {
|
||||
if item.flags.contains(.isStarGiftResale) {
|
||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftPurchase
|
||||
} else {
|
||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftTransfer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let _ = item.giveawayMessageId {
|
||||
|
@ -9,17 +9,20 @@ public final class ToastContentComponent: Component {
|
||||
public let content: AnyComponent<Empty>
|
||||
public let insets: UIEdgeInsets
|
||||
public let iconSpacing: CGFloat
|
||||
public let action: (() -> Void)?
|
||||
|
||||
public init(
|
||||
icon: AnyComponent<Empty>,
|
||||
content: AnyComponent<Empty>,
|
||||
insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0),
|
||||
iconSpacing: CGFloat = 10.0
|
||||
iconSpacing: CGFloat = 10.0,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.icon = icon
|
||||
self.content = content
|
||||
self.insets = insets
|
||||
self.iconSpacing = iconSpacing
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool {
|
||||
@ -39,6 +42,8 @@ public final class ToastContentComponent: Component {
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: ToastContentComponent?
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let content = ComponentView<Empty>()
|
||||
@ -63,9 +68,21 @@ public final class ToastContentComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
@objc private func tapped() {
|
||||
self.component?.action?()
|
||||
}
|
||||
|
||||
func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
if self.component == nil {
|
||||
if let _ = component.action {
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
|
||||
}
|
||||
}
|
||||
self.component = component
|
||||
|
||||
let leftInset: CGFloat = component.insets.left
|
||||
let rightInset: CGFloat = component.insets.right
|
||||
let topInset: CGFloat = component.insets.top
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todolist_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todo_check.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "todo_dot.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf
vendored
Normal file
Binary file not shown.
@ -21,7 +21,6 @@ import DeviceLocationManager
|
||||
import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
@ -4127,6 +4126,11 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
}, editTodoMessage: { [weak self] messageId, append in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openTodoEditing(messageId: messageId, append: append)
|
||||
}, updateHistoryFilter: { [weak self] update in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -595,7 +595,17 @@ extension ChatControllerImpl {
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let insets = layout.insets(options: [.input])
|
||||
let location = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 42.0 - UIScreenPixel, y: layout.size.height - insets.bottom - 122.0), size: CGSize())
|
||||
|
||||
var screenWidth = layout.size.width
|
||||
if layout.metrics.isTablet {
|
||||
if layout.size.height == layout.deviceMetrics.screenSize.width {
|
||||
screenWidth = layout.deviceMetrics.screenSize.height
|
||||
} else {
|
||||
screenWidth = layout.deviceMetrics.screenSize.width
|
||||
}
|
||||
}
|
||||
|
||||
let location = CGRect(origin: CGPoint(x: screenWidth - layout.safeInsets.right - 42.0 - UIScreenPixel, y: layout.size.height - insets.bottom - 122.0), size: CGSize())
|
||||
|
||||
let tooltipController = TooltipScreen(
|
||||
account: self.context.account,
|
||||
|
@ -22,6 +22,7 @@ import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import ComposeTodoScreen
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
@ -342,6 +343,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var selectMessagePollOptionDisposables: DisposableDict<MessageId>?
|
||||
var selectPollOptionFeedback: HapticFeedback?
|
||||
|
||||
var updateMessageTodoDisposables: DisposableDict<MessageId>?
|
||||
|
||||
var resolveUrlDisposable: MetaDisposable?
|
||||
|
||||
var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||
@ -4766,6 +4769,68 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return direction ? .right : .left
|
||||
}
|
||||
self.updateChatLocationThread(threadId: threadId, animationDirection: animationDirection ?? defaultDirection)
|
||||
}, requestToggleTodoMessageItem: { [weak self] messageId, itemId, value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let disposables: DisposableDict<MessageId>
|
||||
if let current = self.updateMessageTodoDisposables {
|
||||
disposables = current
|
||||
} else {
|
||||
disposables = DisposableDict()
|
||||
self.updateMessageTodoDisposables = disposables
|
||||
}
|
||||
var completedIds: [Int32] = []
|
||||
var incompletedIds: [Int32] = []
|
||||
if value {
|
||||
completedIds.append(itemId)
|
||||
} else {
|
||||
incompletedIds.append(itemId)
|
||||
}
|
||||
let signal = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: messageId, completedIds: completedIds, incompletedIds: incompletedIds)
|
||||
disposables.set((signal
|
||||
|> deliverOnMainQueue).startStrict(next: { todo in
|
||||
|
||||
}, error: { _ in
|
||||
|
||||
}), forKey: messageId)
|
||||
}, displayTodoToggleUnavailable: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.dismissAllUndoControllers()
|
||||
//TODO:localize
|
||||
if !self.context.isPremium {
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .premiumPaywall(title: nil, text: "Only [Telegram Premium]() subscribers can mark tasks as done.", customUndoText: nil, timeout: nil, linkAction: nil),
|
||||
action: { [weak self] action in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
if case .info = action {
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: context, source: .presence, forceDark: false, dismissed: nil)
|
||||
self.push(controller)
|
||||
}
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(controller, in: .current)
|
||||
} else if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
var peerName = ""
|
||||
if let author = message.author {
|
||||
peerName = EnginePeer(author).compactDisplayTitle
|
||||
}
|
||||
let controller = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .universalImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)!, size: nil, title: nil, text: "\(peerName) has restricted others from editing this to do list.", customUndoText: nil, timeout: nil),
|
||||
action: { _ in
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(controller, in: .current)
|
||||
}
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
|
||||
controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency
|
||||
|
||||
@ -5819,6 +5884,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.updateChatLocationThreadDisposable?.dispose()
|
||||
self.accountPeerDisposable?.dispose()
|
||||
self.contentDataDisposable?.dispose()
|
||||
self.updateMessageTodoDisposables?.dispose()
|
||||
}
|
||||
|
||||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||||
@ -7549,57 +7615,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
self.present(tooltipScreen, in: .current)
|
||||
}
|
||||
|
||||
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return nil
|
||||
}
|
||||
return createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.collapseInput()
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
let message: EnqueueMessage = .message(
|
||||
text: "",
|
||||
attributes: [],
|
||||
inlineStickers: [:],
|
||||
mediaReference: .standalone(media: TelegramMediaPoll(
|
||||
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
|
||||
publicity: poll.publicity,
|
||||
kind: poll.kind,
|
||||
text: poll.text.string,
|
||||
textEntities: poll.text.entities,
|
||||
options: poll.options,
|
||||
correctAnswers: poll.correctAnswers,
|
||||
results: poll.results,
|
||||
isClosed: false,
|
||||
deadlineTimeout: poll.deadlineTimeout
|
||||
)),
|
||||
threadId: strongSelf.chatLocation.threadId,
|
||||
replyToMessageId: nil,
|
||||
replyToStoryId: nil,
|
||||
localGroupingKey: nil,
|
||||
correlationId: nil,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
)
|
||||
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func transformEnqueueMessages(_ messages: [EnqueueMessage], postpone: Bool = false) -> [EnqueueMessage] {
|
||||
let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting
|
||||
return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone)
|
||||
|
@ -33,6 +33,8 @@ import AutomaticBusinessMessageSetupScreen
|
||||
import MediaEditorScreen
|
||||
import CameraScreen
|
||||
import ShareController
|
||||
import ComposeTodoScreen
|
||||
import ComposePollUI
|
||||
|
||||
extension ChatControllerImpl {
|
||||
enum AttachMenuSubject {
|
||||
@ -113,6 +115,8 @@ extension ChatControllerImpl {
|
||||
availableButtons.insert(.poll, at: max(0, availableButtons.count - 1))
|
||||
}
|
||||
|
||||
availableButtons.append(.todo)
|
||||
|
||||
let presentationData = self.presentationData
|
||||
|
||||
var isScheduledMessages = false
|
||||
@ -622,6 +626,26 @@ extension ChatControllerImpl {
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
}
|
||||
case .todo:
|
||||
if strongSelf.context.isPremium {
|
||||
if let controller = strongSelf.configureTodoCreation() as? AttachmentContainable {
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
}
|
||||
} else {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let demoController = strongSelf.context.sharedContext.makePremiumDemoController(context: strongSelf.context, subject: .todo, forceDark: false, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .todo, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
}, dismissed: nil)
|
||||
replaceImpl = { [weak demoController] c in
|
||||
demoController?.replace(with: c)
|
||||
}
|
||||
strongSelf.push(demoController)
|
||||
Queue.mainQueue().after(0.4) {
|
||||
strongSelf.attachmentController?.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
case .gift:
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext {
|
||||
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
|
||||
@ -677,12 +701,12 @@ extension ChatControllerImpl {
|
||||
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
|
||||
}
|
||||
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: { [weak controller] in
|
||||
controller?.refresh()
|
||||
})
|
||||
},
|
||||
dismissed: {
|
||||
dismissed: {
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
})
|
||||
strongSelf.present(alertController, in: .window(.root))
|
||||
@ -1967,4 +1991,147 @@ extension ChatControllerImpl {
|
||||
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
self.push(mainController)
|
||||
}
|
||||
|
||||
func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return nil
|
||||
}
|
||||
return ComposePollScreen(
|
||||
context: self.context,
|
||||
initialData: ComposePollScreen.initialData(context: self.context),
|
||||
peer: EnginePeer(peer),
|
||||
isQuiz: isQuiz,
|
||||
completion: { [weak self] poll in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||
if let self {
|
||||
self.chatDisplayNode.collapseInput()
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
let message: EnqueueMessage = .message(
|
||||
text: "",
|
||||
attributes: [],
|
||||
inlineStickers: [:],
|
||||
mediaReference: .standalone(media: TelegramMediaPoll(
|
||||
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)),
|
||||
publicity: poll.publicity,
|
||||
kind: poll.kind,
|
||||
text: poll.text.string,
|
||||
textEntities: poll.text.entities,
|
||||
options: poll.options,
|
||||
correctAnswers: poll.correctAnswers,
|
||||
results: poll.results,
|
||||
isClosed: false,
|
||||
deadlineTimeout: poll.deadlineTimeout
|
||||
)),
|
||||
threadId: self.chatLocation.threadId,
|
||||
replyToMessageId: nil,
|
||||
replyToStoryId: nil,
|
||||
localGroupingKey: nil,
|
||||
correlationId: nil,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
)
|
||||
self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func configureTodoCreation() -> ViewController? {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return nil
|
||||
}
|
||||
return ComposeTodoScreen(
|
||||
context: self.context,
|
||||
initialData: ComposeTodoScreen.initialData(
|
||||
context: self.context
|
||||
),
|
||||
peer: EnginePeer(peer),
|
||||
completion: { [weak self] todo in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||
if let self {
|
||||
self.chatDisplayNode.collapseInput()
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }
|
||||
})
|
||||
}
|
||||
}, nil)
|
||||
let message: EnqueueMessage = .message(
|
||||
text: "",
|
||||
attributes: [],
|
||||
inlineStickers: [:],
|
||||
mediaReference: .standalone(media: todo),
|
||||
threadId: self.chatLocation.threadId,
|
||||
replyToMessageId: nil,
|
||||
replyToStoryId: nil,
|
||||
localGroupingKey: nil,
|
||||
correlationId: nil,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
)
|
||||
self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func openTodoEditing(messageId: EngineMessage.Id, append: Bool) {
|
||||
guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
guard let existingTodo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo else {
|
||||
return
|
||||
}
|
||||
|
||||
let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message)
|
||||
|
||||
let controller = ComposeTodoScreen(
|
||||
context: self.context,
|
||||
initialData: ComposeTodoScreen.initialData(
|
||||
context: self.context,
|
||||
existingTodo: existingTodo,
|
||||
append: append,
|
||||
canEdit: canEdit
|
||||
),
|
||||
peer: EnginePeer(peer),
|
||||
completion: { [weak self] todo in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if canEdit {
|
||||
let _ = self.context.engine.messages.requestEditMessage(
|
||||
messageId: messageId,
|
||||
text: "",
|
||||
media: .update(.standalone(media: todo)),
|
||||
entities: nil,
|
||||
inlineStickers: [:]
|
||||
).start()
|
||||
} else {
|
||||
let appendedItems = Array(todo.items[existingTodo.items.count ..< todo.items.count])
|
||||
let _ = self.context.engine.messages.appendTodoMessageItems(messageId: messageId, items: appendedItems).start()
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.navigationPresentation = .modal
|
||||
self.push(controller)
|
||||
}
|
||||
}
|
||||
|
@ -790,22 +790,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
self.prefetchManager = InChatPrefetchManager(context: context)
|
||||
|
||||
self.adMessagesContext = adMessagesContext
|
||||
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>
|
||||
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>
|
||||
if case .bubbles = mode, let adMessagesContext {
|
||||
let peerId = adMessagesContext.peerId
|
||||
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
adMessages = .single((nil, []))
|
||||
adMessages = .single((nil, [], nil, nil))
|
||||
} else {
|
||||
if context.sharedContext.immediateExperimentalUISettings.fakeAds {
|
||||
adMessages = context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||
)
|
||||
|> map { peer -> (interPostInterval: Int32?, messages: [Message]) in
|
||||
|> map { peer -> (interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?) in
|
||||
let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
let mappedMessageType: AdMessageAttribute.MessageType = .sponsored
|
||||
attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false))
|
||||
attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false, minDisplayDuration: nil, maxDisplayDuration: nil))
|
||||
|
||||
var messagePeers = SimpleDictionary<PeerId, Peer>()
|
||||
|
||||
@ -874,14 +874,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
associatedStories: [:]
|
||||
)
|
||||
}
|
||||
return (10, fakeAdMessages)
|
||||
return (10, fakeAdMessages, nil, nil)
|
||||
}
|
||||
} else {
|
||||
adMessages = adMessagesContext.state
|
||||
}
|
||||
}
|
||||
} else {
|
||||
adMessages = .single((nil, []))
|
||||
adMessages = .single((nil, [], nil, nil))
|
||||
}
|
||||
|
||||
let clientId = Atomic<Int32>(value: nextClientId)
|
||||
@ -1223,9 +1223,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false)
|
||||
}
|
||||
|
||||
private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) {
|
||||
private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>) {
|
||||
self.adMessagesDisposable = (adMessages
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
@ -1479,25 +1479,33 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
isMigrated = false
|
||||
}
|
||||
|
||||
if data.canEdit && !isPinnedMessages && !isMigrated {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, f in
|
||||
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
|
||||
f(.custom(transition))
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
var activePoll: TelegramMediaPoll?
|
||||
var activeTodo: TelegramMediaTodo?
|
||||
for media in message.media {
|
||||
if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
||||
if !isPollEffectivelyClosed(message: message, poll: poll) {
|
||||
activePoll = poll
|
||||
}
|
||||
} else if let todo = media as? TelegramMediaTodo {
|
||||
activeTodo = todo
|
||||
}
|
||||
}
|
||||
|
||||
if data.canEdit && !isPinnedMessages && !isMigrated {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, f in
|
||||
if let _ = activeTodo {
|
||||
interfaceInteraction.editTodoMessage(messages[0].id, false)
|
||||
f(.dismissWithoutContent)
|
||||
} else {
|
||||
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
|
||||
f(.custom(transition))
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if let activePoll = activePoll, let voters = activePoll.results.voters {
|
||||
var hasSelected = false
|
||||
for result in voters {
|
||||
@ -1515,6 +1523,21 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
if let activeTodo {
|
||||
var canAppend = false
|
||||
if message.author?.id == context.account.peerId || activeTodo.flags.contains(.othersCanAppend) {
|
||||
canAppend = true
|
||||
}
|
||||
if canAppend {
|
||||
actions.append(.action(ContextMenuActionItem(text: "Add a Task", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
interfaceInteraction.editTodoMessage(messages[0].id, true)
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
var canPin = data.canPin
|
||||
if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation {
|
||||
if !message.isForumPost {
|
||||
|
@ -194,6 +194,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
@ -2399,6 +2399,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, playShakeAnimation: {
|
||||
}, displayQuickShare: { _, _ ,_ in
|
||||
}, updateChatLocationThread: { _, _ in
|
||||
}, requestToggleTodoMessageItem: { _, _, _ in
|
||||
}, displayTodoToggleUnavailable: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
|
||||
|
||||
@ -2714,6 +2716,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
mappedSource = .animatedEmoji
|
||||
case .paidMessages:
|
||||
mappedSource = .paidMessages
|
||||
case .todo:
|
||||
mappedSource = .paidMessages
|
||||
case let .auth(price):
|
||||
mappedSource = .auth(price)
|
||||
}
|
||||
@ -2792,6 +2796,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
mappedSubject = .messageEffects
|
||||
case .paidMessages:
|
||||
mappedSubject = .paidMessages
|
||||
case .todo:
|
||||
mappedSubject = .todo
|
||||
case .business:
|
||||
mappedSubject = .business
|
||||
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton
|
||||
|
Loading…
x
Reference in New Issue
Block a user