[WIP] Chat folder emoji

This commit is contained in:
Isaac 2024-12-18 23:34:33 +08:00
parent fe2ebc4e85
commit e18795980e
31 changed files with 1510 additions and 177 deletions

View File

@ -105,6 +105,10 @@ swift_library(
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/ComposePollUI",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",

View File

@ -221,9 +221,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
return filters
}
|> deliverOnMainQueue).startStandalone(completed: {
|> deliverOnMainQueue).startStandalone(completed: {
c?.dismiss(completion: {
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
//TODO:release
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
@ -273,7 +274,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
let filterType = chatListFilterType(data)
updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in
//TODO:release
updatedItems.append(.action(ContextMenuActionItem(text: title.text, icon: { theme in
let imageName: String
switch filterType {
case .generic:
@ -337,8 +339,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
return filters
}).startStandalone()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
//TODO:release
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})

View File

@ -246,7 +246,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
self.tabsNode = SparseNode()
self.tabContainerNode = ChatListFilterTabContainerNode()
self.tabContainerNode = ChatListFilterTabContainerNode(context: context)
self.tabsNode.addSubnode(self.tabContainerNode)
super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource)
@ -1809,10 +1809,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
//TODO:release
let iconColor: UIColor = .white
let overlayController: UndoOverlayController
if !filterPeersAreMuted.areMuted {
let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title).string
let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title.text).string
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
@ -1821,7 +1822,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
} else {
let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title).string
//TODO:release
let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title.text).string
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
@ -3920,7 +3922,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) {
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: ChatFolderTitle) {
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
@ -3962,7 +3964,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
context: self.context,
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
contents: ChatFolderLinkContents(
localFilterId: filterId, title: title,
localFilterId: filterId,
title: title,
peers: [],
alreadyMemberPeerIds: Set(),
memberCounts: [:]
@ -5928,7 +5931,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
}
}
items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in
//TODO:release
items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: badge, icon: { theme in
let imageName: String
if isDisabled {
imageName = "Chat/Context Menu/Lock"
@ -5981,7 +5985,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
let controller = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
})
}

View File

@ -19,6 +19,12 @@ import ContextUI
import AsyncDisplayKit
import UndoUI
import PeerNameColorItem
import EntityKeyboard
import ComposePollUI
import ChatEntityKeyboardInputNode
import ComponentFlow
import ChatPresentationInterfaceState
import ComponentDisplayAdapters
private enum FilterSection: Int32, Hashable {
case include
@ -28,6 +34,8 @@ private enum FilterSection: Int32, Hashable {
private final class ChatListFilterPresetControllerArguments {
let context: AccountContext
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
let updateName: (ChatFolderTitle) -> Void
let toggleNameAnimations: () -> Void
let openAddIncludePeer: () -> Void
let openAddExcludePeer: () -> Void
let deleteIncludePeer: (EnginePeer.Id) -> Void
@ -49,6 +57,8 @@ private final class ChatListFilterPresetControllerArguments {
init(
context: AccountContext,
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
updateName: @escaping (ChatFolderTitle) -> Void,
toggleNameAnimations: @escaping () -> Void,
openAddIncludePeer: @escaping () -> Void,
openAddExcludePeer: @escaping () -> Void,
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
@ -69,6 +79,8 @@ private final class ChatListFilterPresetControllerArguments {
) {
self.context = context
self.updateState = updateState
self.updateName = updateName
self.toggleNameAnimations = toggleNameAnimations
self.openAddIncludePeer = openAddIncludePeer
self.openAddExcludePeer = openAddExcludePeer
self.deleteIncludePeer = deleteIncludePeer
@ -216,8 +228,8 @@ private enum ChatListFilterRevealedItemId: Equatable {
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case screenHeader
case nameHeader(String)
case name(placeholder: String, value: String)
case nameHeader(title: String, enableAnimations: Bool)
case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool)
case includePeersHeader(String)
case addIncludePeer(title: String)
case includeCategory(index: Int, category: ChatListFilterIncludeCategory, title: String, isRevealed: Bool)
@ -234,7 +246,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case inviteLinkCreate(hasLinks: Bool)
case inviteLink(Int, ExportedChatFolderLink)
case inviteLinkInfo(text: String)
case tagColorHeader(name: String, color: PeerNameColors.Colors?, isPremium: Bool)
case tagColorHeader(name: ChatFolderTitle, color: PeerNameColors.Colors?, isPremium: Bool)
case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?, isPremium: Bool)
case tagColorFooter
@ -362,21 +374,36 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
switch self {
case .screenHeader:
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section)
case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .done, clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.name = value
state.changedName = true
return state
case let .nameHeader(title, enableAnimations):
//TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: {
arguments.toggleNameAnimations()
}, sectionId: self.section)
case let .name(placeholder, value, inputMode, enableAnimations):
return ItemListFilterTitleInputItem(
context: arguments.context,
presentationData: presentationData,
text: value,
enableAnimations: enableAnimations,
placeholder: placeholder,
maxLength: 12,
inputMode: inputMode,
sectionId: self.section,
textUpdated: { value in
arguments.updateName(ChatFolderTitle(attributedString: value, enableAnimations: true))
},
toggleInputMode: {
arguments.updateState { current in
var state = current
if state.nameInputMode == .emoji {
state.nameInputMode = .keyboard
} else {
state.nameInputMode = .emoji
}
return state
}
}
}, action: {
arguments.clearFocus()
}, cleared: {
arguments.focusOnName()
})
)
case .includePeersHeader(let text), .excludePeersHeader(let text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case .includePeerInfo(let text), .excludePeerInfo(let text):
@ -462,13 +489,13 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
arguments.expandSection(.exclude)
})
case let .tagColorHeader(name, color, isPremium):
var badge: String?
var badgeStyle: ItemListSectionHeaderItem.BadgeStyle?
var badge: ChatFolderTitle?
var badgeStyle: ChatListFilterTagSectionHeaderItem.BadgeStyle?
var accessoryText: ItemListSectionHeaderAccessoryText?
if isPremium {
if let color {
badge = name.uppercased()
badgeStyle = ItemListSectionHeaderItem.BadgeStyle(
badge = ChatFolderTitle(text: name.text.uppercased(), entities: name.entities, enableAnimations: name.enableAnimations)
badgeStyle = ChatListFilterTagSectionHeaderItem.BadgeStyle(
background: color.main.withMultipliedAlpha(0.1),
foreground: color.main
)
@ -478,7 +505,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
} else if color != nil {
accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelPremiumExpired, color: .generic)
}
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section)
return ChatListFilterTagSectionHeaderItem(context: arguments.context, presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section)
case let .tagColor(colors, color, isPremium):
return PeerNameColorItem(
theme: presentationData.theme,
@ -518,9 +545,41 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
}
}
extension ChatFolderTitle {
init(attributedString: NSAttributedString, enableAnimations: Bool) {
let inputStateText = ChatTextInputStateText(attributedText: attributedString)
self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in
if case let .customEmoji(_, fileId) = attribute.type {
return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId))
}
return nil
}, enableAnimations: enableAnimations)
}
var rawAttributedString: NSAttributedString {
let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range)
}
return nil
})
return inputStateText.attributedText()
}
func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: self.rawAttributedString)
result.addAttributes([
.font: font,
.foregroundColor: textColor
], range: NSRange(location: 0, length: result.length))
return result
}
}
private struct ChatListFilterPresetControllerState: Equatable {
var name: String
var name: ChatFolderTitle
var changedName: Bool
var nameInputMode: ListComposePollOptionComponent.InputMode = .keyboard
var color: PeerNameColor?
var colorUpdated: Bool = false
var includeCategories: ChatListFilterPeerCategories
@ -534,7 +593,7 @@ private struct ChatListFilterPresetControllerState: Equatable {
var expandedSections: Set<FilterSection>
var isComplete: Bool {
if self.name.isEmpty {
if self.name.text.isEmpty {
return false
}
@ -565,8 +624,8 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
entries.append(.screenHeader)
}
entries.append(.nameHeader(presentationData.strings.ChatListFolder_NameSectionHeader))
entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name))
entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations))
entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name.rawAttributedString, inputMode: state.nameInputMode, enableAnimations: state.name.enableAnimations))
entries.append(.includePeersHeader(presentationData.strings.ChatListFolder_IncludedSectionHeader))
if includePeers.count < limit {
@ -648,6 +707,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance)
}
//TODO:localize
entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium))
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium))
entries.append(.tagColorFooter)
@ -1015,11 +1075,11 @@ func chatListFilterType(_ data: ChatListFilterData) -> ChatListFilterType {
}
private extension ChatListFilter {
var title: String {
var title: ChatFolderTitle {
if case let .filter(_, title, _, _) = self {
return title
} else {
return ""
return ChatFolderTitle(text: "", entities: [], enableAnimations: true)
}
}
@ -1040,14 +1100,338 @@ private extension ChatListFilter {
}
}
private final class ChatListFilterPresetController: ItemListController {
private let context: AccountContext
private var currentLayout: ContainerViewLayout?
var titleItemNode: (() -> ItemListFilterTitleInputItemNode?)?
var currentInputMode: ListComposePollOptionComponent.InputMode?
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
private var inputMediaNodeDataDisposable: Disposable?
private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
private var inputMediaNode: ChatEntityKeyboardInputNode?
private var inputMediaNodeBackground = SimpleLayer()
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
init<ItemGenerationArguments>(
context: AccountContext,
state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)),
NoError>
) {
self.context = context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil)
self.inputMediaNodeDataPromise.set(
ChatEntityKeyboardInputNode.inputData(
context: self.context,
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasTrending: false,
hasSearch: true,
hasStickers: false,
hasGifs: false,
hideBackground: true,
sendGif: nil
)
)
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
self.inputMediaNodeData = value
})
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
sendSticker: { _, _, _, _, _, _, _, _, _ in
return false
},
sendEmoji: { _, _, _ in
},
sendGif: { _, _, _, _, _ in
return false
},
sendBotContextResultAsGif: { _, _ , _, _, _, _ in
return false
},
updateChoosingSticker: { _ in
},
switchToTextInput: { [weak self] in
guard let self else {
return
}
self.currentInputMode = .keyboard
self.update(transition: .animated(duration: 0.4, curve: .spring))
},
dismissTextInput: {
},
insertText: { [weak self] text in
guard let self else {
return
}
guard let titleItemNode = self.titleItemNode?() else {
return
}
guard let textFieldView = titleItemNode.textFieldView else {
return
}
if titleItemNode.textFieldState.isEditing {
textFieldView.insertText(text: text)
}
},
backwardsDeleteText: { [weak self] in
guard let self else {
return
}
guard let titleItemNode = self.titleItemNode?() else {
return
}
guard let textFieldView = titleItemNode.textFieldView else {
return
}
if titleItemNode.textFieldState.isEditing {
textFieldView.backwardsDeleteText()
}
},
openStickerEditor: {
},
presentController: { [weak self] c, a in
guard let self else {
return
}
self.present(c, in: .window(.root), with: a)
},
presentGlobalOverlayController: { [weak self] c, a in
guard let self else {
return
}
self.presentInGlobalOverlay(c, with: a)
},
getNavigationController: { [weak self] () -> NavigationController? in
guard let self else {
return nil
}
if let navigationController = self.navigationController as? NavigationController {
return navigationController
}
return nil
},
requestLayout: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
}
)
}
@MainActor required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.inputMediaNodeDataDisposable?.dispose()
}
private func updateInputMediaNode(
context: AccountContext,
availableSize: CGSize,
bottomInset: CGFloat,
inputHeight: CGFloat,
effectiveInputHeight: CGFloat,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
transition: ComponentTransition
) -> CGFloat {
let bottomInset: CGFloat = bottomInset + 8.0
let bottomContainerInset: CGFloat = 0.0
let needsInputActivation: Bool = !"".isEmpty
var height: CGFloat = 0.0
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
let inputMediaNode: ChatEntityKeyboardInputNode
var inputMediaNodeTransition = transition
var animateIn = false
if let current = self.inputMediaNode {
inputMediaNode = current
} else {
animateIn = true
inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none)
inputMediaNode = ChatEntityKeyboardInputNode(
context: context,
currentInputData: inputData,
updatedInputData: self.inputMediaNodeDataPromise.get(),
defaultToEmojiTab: true,
opaqueTopPanelBackground: false,
useOpaqueTheme: true,
interaction: self.inputMediaInteraction,
chatPeerId: nil,
stateContext: self.inputMediaNodeStateContext
)
inputMediaNode.clipsToBounds = true
inputMediaNode.externalTopPanelContainerImpl = nil
inputMediaNode.useExternalSearchContainer = true
if inputMediaNode.view.superview == nil {
self.inputMediaNodeBackground.removeAllAnimations()
self.displayNode.layer.addSublayer(self.inputMediaNodeBackground)
self.displayNode.view.addSubview(inputMediaNode.view)
}
self.inputMediaNode = inputMediaNode
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let presentationInterfaceState = ChatPresentationInterfaceState(
chatWallpaper: .builtin(WallpaperSettings()),
theme: presentationData.theme,
strings: presentationData.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
limitsConfiguration: context.currentLimitsConfiguration.with { $0 },
fontSize: presentationData.chatFontSize,
bubbleCorners: presentationData.chatBubbleCorners,
accountPeerId: context.account.peerId,
mode: .standard(.default),
chatLocation: .peer(id: context.account.peerId),
subject: nil,
peerNearbyData: nil,
greetingData: nil,
pendingUnpinnedAllMessages: false,
activeGroupCallInfo: nil,
hasActiveGroupCall: false,
importState: nil,
threadData: nil,
isGeneralThreadClosed: nil,
replyMessage: nil,
accountPeerColor: nil,
businessIntro: nil
)
self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false)
let inputNodeHeight = heightAndOverflow.0
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0))
if needsInputActivation {
let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
}
if animateIn {
var targetFrame = inputNodeFrame
targetFrame.origin.y = availableSize.height
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame)
let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0))
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame)
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
} else {
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
}
height = heightAndOverflow.0
} else {
if let inputMediaNode = self.inputMediaNode {
self.inputMediaNode = nil
var targetFrame = inputMediaNode.frame
targetFrame.origin.y = availableSize.height
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
if let inputMediaNode {
Queue.mainQueue().after(0.3) {
inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
inputMediaNode?.view.removeFromSuperview()
})
}
}
})
transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in
Queue.mainQueue().after(0.3) {
guard let self else {
return
}
if self.currentInputMode == .keyboard {
self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in
guard let self else {
return
}
if finished {
self.inputMediaNodeBackground.removeFromSuperlayer()
}
self.inputMediaNodeBackground.removeAllAnimations()
})
}
}
})
}
}
return height
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.currentLayout = layout
let inputHeight = self.updateInputMediaNode(
context: self.context,
availableSize: layout.size,
bottomInset: layout.intrinsicInsets.bottom,
inputHeight: layout.inputHeight ?? 0.0,
effectiveInputHeight: layout.deviceMetrics.standardInputHeight(inLandscape: false),
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
transition: ComponentTransition(transition)
)
var innerLayout = layout
innerLayout.inputHeight = max(innerLayout.inputHeight ?? 0.0, inputHeight)
super.containerLayoutUpdated(innerLayout, transition: transition)
}
func update(transition: ContainedViewLayoutTransition) {
if let currentLayout = self.currentLayout {
self.containerLayoutUpdated(currentLayout, transition: transition)
}
}
}
func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
let initialName: String
let initialName: ChatFolderTitle
if let initialPreset {
initialName = initialPreset.title
} else {
initialName = ""
initialName = ChatFolderTitle(text: "", entities: [], enableAnimations: true)
}
var initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: [])
var initialState = ChatListFilterPresetControllerState(
name: initialName,
changedName: initialPreset != nil,
color: initialPreset?.data?.color,
includeCategories: initialPreset?.data?.categories ?? [],
excludeMuted: initialPreset?.data?.excludeMuted ?? false,
excludeRead: initialPreset?.data?.excludeRead ?? false,
excludeArchived: initialPreset?.data?.excludeArchived ?? false,
additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [],
additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [],
expandedSections: []
)
initialState.colorUpdated = true
let updatedCurrentPreset: Signal<ChatListFilter?, NoError>
@ -1061,6 +1445,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
updatedCurrentPreset = .single(nil)
}
var withController: (((ChatListFilterPresetController) -> Void) -> Void)?
let stateValue = Atomic(value: initialState)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
@ -1076,24 +1462,29 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
case .generic:
state.name = initialName
case .unmuted:
state.name = presentationData.strings.ChatListFolder_NameNonMuted
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonMuted, entities: [], enableAnimations: true)
case .unread:
state.name = presentationData.strings.ChatListFolder_NameUnread
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameUnread, entities: [], enableAnimations: true)
case .channels:
state.name = presentationData.strings.ChatListFolder_NameChannels
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameChannels, entities: [], enableAnimations: true)
case .groups:
state.name = presentationData.strings.ChatListFolder_NameGroups
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameGroups, entities: [], enableAnimations: true)
case .bots:
state.name = presentationData.strings.ChatListFolder_NameBots
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameBots, entities: [], enableAnimations: true)
case .contacts:
state.name = presentationData.strings.ChatListFolder_NameContacts
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameContacts, entities: [], enableAnimations: true)
case .nonContacts:
state.name = presentationData.strings.ChatListFolder_NameNonContacts
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonContacts, entities: [], enableAnimations: true)
}
}
}
return state
})
withController?({ c in
let state = stateValue.with({ $0 })
c.currentInputMode = state.nameInputMode
c.update(transition: .animated(duration: 0.5, curve: .spring))
})
}
var skipStateAnimation = false
@ -1180,6 +1571,30 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
updateState: { f in
updateState(f)
},
updateName: { name in
if name != stateValue.with({ $0 }).name {
updateState { current in
var name = name
name.enableAnimations = current.name.enableAnimations
var state = current
state.name = name
state.changedName = true
return state
}
}
},
toggleNameAnimations: {
updateState { current in
var name = current.name
name.enableAnimations = !current.name.enableAnimations
var state = current
state.name = name
state.changedName = true
return state
}
},
openAddIncludePeer: {
let _ = combineLatest(
queue: Queue.mainQueue(),
@ -1714,7 +2129,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
let controller = ChatListFilterPresetController(context: context, state: signal)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, d in
controller?.present(c, in: .window(.root), with: d)
@ -1741,6 +2156,21 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
}
controller.view.endEditing(true)
}
withController = { [weak controller] f in
guard let controller = controller else {
return
}
f(controller)
}
controller.titleItemNode = { [weak controller] in
var foundItemNode: ItemListFilterTitleInputItemNode?
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListFilterTitleInputItemNode {
foundItemNode = itemNode
}
}
return foundItemNode
}
controller.attemptNavigation = { _ in
if let attemptNavigationImpl {
attemptNavigationImpl({ value in
@ -1812,7 +2242,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
return controller
}
func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, checkIfExists: Bool, title: String, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, pushPremiumController: @escaping (ViewController) -> Void, completed: @escaping () -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) {
func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, checkIfExists: Bool, title: ChatFolderTitle, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, pushPremiumController: @escaping (ViewController) -> Void, completed: @escaping () -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) {
if peerIds.isEmpty {
completed()
return

View File

@ -289,7 +289,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
}
if case let .filter(_, title, _, _) = filter {
folderCount += 1
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
//TODO:release
entries.append(.preset(index: PresetIndex(value: entries.count), title: title.text, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
}
}
@ -299,7 +300,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount {
entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection))
for filter in filteredSuggestedFilters {
entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data))
//TODO:release
entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title.text, label: filter.description, preset: filter.data))
}
if filters.isEmpty {
entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder))
@ -387,7 +389,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
let id = context.engine.peers.generateNewChatListFilterId(filters: filters)
filters.append(.filter(id: id, title: title, emoticon: nil, data: data))
//TODO:release
filters.append(.filter(id: id, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: nil, data: data))
return filters
}
|> deliverOnMainQueue).start(next: { _ in

View File

@ -4,6 +4,8 @@ import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
private let pressed: () -> Void
@ -55,6 +57,7 @@ private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
}
private final class ItemNode: ASDisplayNode {
private let context: AccountContext
private let pressed: (Bool) -> Void
private let requestedDeletion: () -> Void
@ -63,11 +66,11 @@ private final class ItemNode: ASDisplayNode {
private let extractedBackgroundNode: ASImageNode
private let titleContainer: ASDisplayNode
private let titleNode: ImmediateTextNode
private let titleActiveNode: ImmediateTextNode
private let titleNode: ImmediateTextNodeWithEntities
private let titleActiveNode: ImmediateTextNodeWithEntities
private let shortTitleContainer: ASDisplayNode
private let shortTitleNode: ImmediateTextNode
private let shortTitleActiveNode: ImmediateTextNode
private let shortTitleNode: ImmediateTextNodeWithEntities
private let shortTitleActiveNode: ImmediateTextNodeWithEntities
private let badgeContainerNode: ASDisplayNode
private let badgeTextNode: ImmediateTextNode
private let badgeBackgroundActiveNode: ASImageNode
@ -84,11 +87,12 @@ private final class ItemNode: ASDisplayNode {
private var isDisabled: Bool = false
private var theme: PresentationTheme?
private var currentTitle: (String, String)?
private var currentTitle: (ChatFolderTitle, ChatFolderTitle)?
private var pointerInteraction: PointerInteraction?
init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) {
init(context: AccountContext, pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) {
self.context = context
self.pressed = pressed
self.requestedDeletion = requestedDeletion
@ -102,23 +106,23 @@ private final class ItemNode: ASDisplayNode {
self.titleContainer = ASDisplayNode()
self.titleNode = ImmediateTextNode()
self.titleNode = ImmediateTextNodeWithEntities()
self.titleNode.displaysAsynchronously = false
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleActiveNode = ImmediateTextNode()
self.titleActiveNode = ImmediateTextNodeWithEntities()
self.titleActiveNode.displaysAsynchronously = false
self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleActiveNode.alpha = 0.0
self.shortTitleContainer = ASDisplayNode()
self.shortTitleNode = ImmediateTextNode()
self.shortTitleNode = ImmediateTextNodeWithEntities()
self.shortTitleNode.displaysAsynchronously = false
self.shortTitleNode.alpha = 0.0
self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.shortTitleActiveNode = ImmediateTextNode()
self.shortTitleActiveNode = ImmediateTextNodeWithEntities()
self.shortTitleActiveNode.displaysAsynchronously = false
self.shortTitleActiveNode.alpha = 0.0
self.shortTitleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
@ -194,7 +198,7 @@ private final class ItemNode: ASDisplayNode {
self.pressed(self.isDisabled)
}
func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isReordering: Bool, canReorderAllChats: Bool, isDisabled: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
func updateText(strings: PresentationStrings, title: ChatFolderTitle, shortTitle: ChatFolderTitle, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isReordering: Bool, canReorderAllChats: Bool, isDisabled: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
self.isEditing = isEditing
self.isDisabled = isDisabled
@ -221,7 +225,7 @@ private final class ItemNode: ASDisplayNode {
self.unreadCount = unreadCount
}
self.buttonNode.accessibilityLabel = title
self.buttonNode.accessibilityLabel = title.text
if unreadCount > 0 {
if self.buttonNode.accessibilityValue == nil || unreadCountUpdated {
self.buttonNode.accessibilityValue = strings.VoiceOver_Chat_UnreadMessages(Int32(unreadCount))
@ -271,11 +275,31 @@ private final class ItemNode: ASDisplayNode {
transition.updateAlpha(node: self.shortTitleNode, alpha: deselectionAlpha)
transition.updateAlpha(node: self.shortTitleActiveNode, alpha: selectionAlpha)
let titleArguments = TextNodeWithEntities.Arguments(
context: self.context,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
placeholderColor: presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
self.titleNode.arguments = titleArguments
self.titleActiveNode.arguments = titleArguments
self.shortTitleNode.arguments = titleArguments
self.shortTitleActiveNode.arguments = titleArguments
self.titleNode.visibility = title.enableAnimations
self.titleActiveNode.visibility = title.enableAnimations
self.shortTitleNode.visibility = title.enableAnimations
self.shortTitleActiveNode.visibility = title.enableAnimations
if themeUpdated || titleUpdated {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
self.shortTitleActiveNode.attributedText = NSAttributedString(string: shortTitle, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
//TODO:release
self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
}
if unreadCount != 0 {
@ -467,7 +491,7 @@ public struct ChatListFilterTabEntryUnreadCount: Equatable {
public enum ChatListFilterTabEntry: Equatable {
case all(unreadCount: Int)
case filter(id: Int32, text: String, unread: ChatListFilterTabEntryUnreadCount)
case filter(id: Int32, text: ChatFolderTitle, unread: ChatListFilterTabEntryUnreadCount)
public var id: ChatListFilterTabEntryId {
switch self {
@ -478,19 +502,19 @@ public enum ChatListFilterTabEntry: Equatable {
}
}
func title(strings: PresentationStrings) -> String {
func title(strings: PresentationStrings) -> ChatFolderTitle {
switch self {
case .all:
return strings.ChatList_Tabs_AllChats
return ChatFolderTitle(text: strings.ChatList_Tabs_AllChats, entities: [], enableAnimations: true)
case let .filter(_, text, _):
return text
}
}
func shortTitle(strings: PresentationStrings) -> String {
func shortTitle(strings: PresentationStrings) -> ChatFolderTitle {
switch self {
case .all:
return strings.ChatList_Tabs_All
return ChatFolderTitle(text: strings.ChatList_Tabs_All, entities: [], enableAnimations: true)
case let .filter(_, text, _):
return text
}
@ -498,6 +522,7 @@ public enum ChatListFilterTabEntry: Equatable {
}
public final class ChatListFilterTabContainerNode: ASDisplayNode {
private let context: AccountContext
private let scrollNode: ASScrollNode
private let selectedLineNode: ASImageNode
private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:]
@ -546,7 +571,8 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
}
}
public override init() {
public init(context: AccountContext) {
self.context = context
self.scrollNode = ASScrollNode()
self.selectedLineNode = ASImageNode()
@ -778,7 +804,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
} else {
itemNodeTransition = .immediate
wasAdded = true
itemNode = ItemNode(pressed: { [weak self] disabled in
itemNode = ItemNode(context: self.context, pressed: { [weak self] disabled in
self?.tabSelected?(filter.id, disabled)
}, requestedDeletion: { [weak self] in
self?.tabRequestedDeletion?(filter.id)
@ -831,7 +857,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
selectionFraction = 0.0
}
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition)
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition)
}
var removeKeys: [ChatListFilterTabEntryId] = []
for (id, _) in self.itemNodes {

View File

@ -0,0 +1,364 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ActivityIndicator
import ItemListUI
import AccountContext
import TelegramCore
import TextNodeWithEntities
public class ChatListFilterTagSectionHeaderItem: ListViewItem, ItemListItem {
public struct BadgeStyle: Equatable {
public var background: UIColor
public var foreground: UIColor
public init(background: UIColor, foreground: UIColor) {
self.background = background
self.foreground = foreground
}
}
let context: AccountContext
let presentationData: ItemListPresentationData
let text: String
let badge: ChatFolderTitle?
let badgeStyle: BadgeStyle?
let multiline: Bool
let activityIndicator: ItemListSectionHeaderActivityIndicator
let accessoryText: ItemListSectionHeaderAccessoryText?
let actionText: String?
let action: (() -> Void)?
public let sectionId: ItemListSectionId
public let isAlwaysPlain: Bool = true
public init(context: AccountContext, presentationData: ItemListPresentationData, text: String, badge: ChatFolderTitle? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) {
self.context = context
self.presentationData = presentationData
self.text = text
self.badge = badge
self.badgeStyle = badgeStyle
self.multiline = multiline
self.activityIndicator = activityIndicator
self.accessoryText = accessoryText
self.actionText = actionText
self.action = action
self.sectionId = sectionId
}
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 = ChatListFilterTagSectionHeaderItemNode()
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 {
guard let nodeValue = node() as? ChatListFilterTagSectionHeaderItemNode else {
assertionFailure()
return
}
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 ChatListFilterTagSectionHeaderItemNode: ListViewItemNode {
private var item: ChatListFilterTagSectionHeaderItem?
private let titleNode: TextNode
private var badgeBackgroundLayer: SimpleLayer?
private var badgeTextNode: TextNodeWithEntities?
private let accessoryTextNode: TextNode
private var accessoryImageNode: ASImageNode?
private var activityIndicator: ActivityIndicator?
private var actionNode: TextNode?
private var actionButtonNode: HighlightableButtonNode?
private let activateArea: AccessibilityAreaNode
public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.accessoryTextNode = TextNode()
self.accessoryTextNode.isUserInteractionEnabled = false
self.accessoryTextNode.contentMode = .left
self.accessoryTextNode.contentsScale = UIScreen.main.scale
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = [.staticText, .header]
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.accessoryTextNode)
self.addSubnode(self.activateArea)
}
public func asyncLayout() -> (_ item: ChatListFilterTagSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeActionLayout = TextNode.asyncLayout(self.actionNode)
let makeBadgeTextLayout = TextNodeWithEntities.asyncLayout(self.badgeTextNode)
let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode)
let previousItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
var badgeLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
if let badge = item.badge {
if item.badgeStyle != nil {
let badgeFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize * 12.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badge.attributedString(font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
} else {
let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0)
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badge.attributedString(font: badgeFont, textColor: item.badgeStyle?.foreground ?? item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
}
}
let badgeSpacing: CGFloat = 6.0
var textRightInset: CGFloat = 20.0
if let badgeLayoutAndApply {
textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing
}
var actionLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let actionText = item.actionText {
let actionLayoutAndApplyValue = makeActionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: actionText, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
actionLayoutAndApply = actionLayoutAndApplyValue
textRightInset += actionLayoutAndApplyValue.0.size.width + 2.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var accessoryTextString: NSAttributedString?
var accessoryIcon: UIImage?
if let accessoryText = item.accessoryText {
let color: UIColor
switch accessoryText.color {
case .generic:
color = item.presentationData.theme.list.sectionHeaderTextColor
case .destructive:
color = item.presentationData.theme.list.freeTextErrorColor
}
accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color)
accessoryIcon = accessoryText.icon
}
let (accessoryLayout, accessoryApply) = makeAccessoryTextLayout(TextNodeLayoutArguments(attributedString: accessoryTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
var insets = UIEdgeInsets()
contentSize = CGSize(width: params.width, height: titleLayout.size.height + 13.0)
switch neighbors.top {
case .none:
insets.top += 24.0
case .otherSection:
insets.top += 28.0
default:
break
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
let _ = accessoryApply()
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.text
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size)
if let (actionLayout, actionApply) = actionLayoutAndApply {
let actionButtonNode: HighlightableButtonNode
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
actionButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -4.0)
strongSelf.addSubnode(actionButtonNode)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
}
let actionNode = actionApply()
if strongSelf.actionNode !== actionNode {
strongSelf.actionNode?.removeFromSupernode()
strongSelf.actionNode = actionNode
actionButtonNode.addSubnode(actionNode)
}
actionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - actionLayout.size.width, y: 7.0), size: actionLayout.size)
actionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionLayout.size)
} else {
if let actionNode = strongSelf.actionNode {
strongSelf.actionNode = nil
actionNode.removeFromSupernode()
}
if let actionButtonNode = strongSelf.actionButtonNode {
strongSelf.actionButtonNode = nil
actionButtonNode.removeFromSupernode()
}
}
if let badgeLayoutAndApply {
let badgeTextNode = badgeLayoutAndApply.1(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
))
let badgeSideInset: CGFloat = 4.0
let badgeBackgroundSize: CGSize
if item.badgeStyle != nil {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
} else {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
let badgeBackgroundLayer: SimpleLayer
if let current = strongSelf.badgeBackgroundLayer {
badgeBackgroundLayer = current
} else {
badgeBackgroundLayer = SimpleLayer()
strongSelf.badgeBackgroundLayer = badgeBackgroundLayer
strongSelf.layer.addSublayer(badgeBackgroundLayer)
}
if strongSelf.badgeTextNode !== badgeTextNode {
strongSelf.badgeTextNode?.textNode.removeFromSupernode()
strongSelf.badgeTextNode = badgeTextNode
strongSelf.addSubnode(badgeTextNode.textNode)
}
badgeBackgroundLayer.frame = badgeBackgroundFrame
badgeBackgroundLayer.backgroundColor = item.badgeStyle?.background.cgColor ?? item.presentationData.theme.list.itemCheckColors.fillColor.cgColor
badgeBackgroundLayer.cornerRadius = 5.0
badgeTextNode.textNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size)
if item.badge?.enableAnimations ?? false {
badgeTextNode.visibilityRect = .infinite
} else {
badgeTextNode.visibilityRect = CGRect()
}
} else {
if let badgeTextNode = strongSelf.badgeTextNode {
strongSelf.badgeTextNode = nil
badgeTextNode.textNode.removeFromSupernode()
}
if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer {
strongSelf.badgeBackgroundLayer = nil
badgeBackgroundLayer.removeFromSuperlayer()
}
}
var accessoryTextOffset: CGFloat = 0.0
if let accessoryIcon = accessoryIcon {
accessoryTextOffset += accessoryIcon.size.width + 3.0
}
strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width - accessoryTextOffset, y: 7.0), size: accessoryLayout.size)
if let accessoryIcon = accessoryIcon {
let accessoryImageNode: ASImageNode
if let currentAccessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode = currentAccessoryImageNode
} else {
accessoryImageNode = ASImageNode()
accessoryImageNode.displaysAsynchronously = false
accessoryImageNode.displayWithoutProcessing = true
strongSelf.addSubnode(accessoryImageNode)
strongSelf.accessoryImageNode = accessoryImageNode
}
accessoryImageNode.image = accessoryIcon
accessoryImageNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryIcon.size.width, y: 7.0), size: accessoryIcon.size)
} else if let accessoryImageNode = strongSelf.accessoryImageNode {
accessoryImageNode.removeFromSupernode()
strongSelf.accessoryImageNode = nil
}
if previousItem?.activityIndicator != item.activityIndicator {
if item.activityIndicator.hasActivity {
let activityIndicator: ActivityIndicator
if let currentActivityIndicator = strongSelf.activityIndicator {
activityIndicator = currentActivityIndicator
} else {
activityIndicator = ActivityIndicator(type: .custom(item.presentationData.theme.list.sectionHeaderTextColor, 18.0, 1.0, false))
strongSelf.addSubnode(activityIndicator)
strongSelf.activityIndicator = activityIndicator
}
activityIndicator.isHidden = false
if previousItem != nil {
activityIndicator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
}
} else if let activityIndicator = strongSelf.activityIndicator {
activityIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { finished in
if finished {
activityIndicator.isHidden = true
}
})
}
}
var activityIndicatorOrigin: CGPoint?
switch item.activityIndicator {
case .left:
activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel)
case .right:
activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel)
default:
break
}
if let activityIndicatorOrigin = activityIndicatorOrigin {
strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0))
}
}
})
}
}
@objc private func actionButtonPressed() {
self.item?.action?()
}
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)
}
}

View File

@ -0,0 +1,275 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
import ItemListUI
import ComponentFlow
import ComposePollUI
import TextFieldComponent
public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let text: NSAttributedString
let enableAnimations: Bool
let placeholder: String
let maxLength: Int
let inputMode: ListComposePollOptionComponent.InputMode?
let enabled: Bool
public let sectionId: ItemListSectionId
let textUpdated: (NSAttributedString) -> Void
let updatedFocus: ((Bool) -> Void)?
let toggleInputMode: () -> Void
public let tag: ItemListItemTag?
public init(
context: AccountContext,
presentationData: ItemListPresentationData,
text: NSAttributedString,
enableAnimations: Bool,
placeholder: String,
maxLength: Int = 0,
inputMode: ListComposePollOptionComponent.InputMode?,
enabled: Bool = true,
tag: ItemListItemTag? = nil,
sectionId: ItemListSectionId,
textUpdated: @escaping (NSAttributedString) -> Void,
updatedFocus: ((Bool) -> Void)? = nil,
toggleInputMode: @escaping () -> Void
) {
self.context = context
self.presentationData = presentationData
self.text = text
self.enableAnimations = enableAnimations
self.placeholder = placeholder
self.maxLength = maxLength
self.inputMode = inputMode
self.enabled = enabled
self.tag = tag
self.sectionId = sectionId
self.textUpdated = textUpdated
self.updatedFocus = updatedFocus
self.toggleInputMode = toggleInputMode
}
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 = ItemListFilterTitleInputItemNode()
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? ItemListFilterTitleInputItemNode {
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 ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
let textFieldState = TextFieldComponent.ExternalState()
private let textField = ComponentView<Empty>()
private let componentState = EmptyComponentState()
private var item: ItemListFilterTitleInputItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
var textFieldView: ListComposePollOptionComponent.View? {
return self.textField.view as? ListComposePollOptionComponent.View
}
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()
super.init(layerBacked: false, dynamicBounce: false)
}
override public func didLoad() {
super.didLoad()
}
public func asyncLayout() -> (_ item: ItemListFilterTitleInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { [weak self] item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightInset: CGFloat = 16.0 + params.rightInset
let _ = rightInset
let separatorHeight = UIScreenPixel
let contentSize = CGSize(width: params.width, height: 44.0)
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)
let _ = attributedPlaceholderText
return (layout, {
guard let self else {
return
}
self.item = item
if let _ = updatedTheme {
self.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
self.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
self.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
}
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
if self.topStripeNode.supernode == nil {
self.insertSubnode(self.topStripeNode, at: 1)
}
if self.bottomStripeNode.supernode == nil {
self.insertSubnode(self.bottomStripeNode, at: 2)
}
if self.maskNode.supernode == nil {
self.insertSubnode(self.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
self.topStripeNode.isHidden = true
default:
hasTopCorners = true
self.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
self.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
self.bottomStripeNode.isHidden = hasCorners
}
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
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.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
self.textField.parentState = self.componentState
self.componentState._updated = { [weak self] transition, _ in
guard let self, let item = self.item else {
return
}
guard let textFieldView = self.textFieldView else {
return
}
item.textUpdated(textFieldView.currentAttributedText)
}
let textFieldSize = self.textField.update(
transition: .immediate,
component: AnyComponent(ListComposePollOptionComponent(
externalState: self.textFieldState,
context: item.context,
theme: item.presentationData.theme,
strings: item.presentationData.strings,
placeholder: NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor),
resetText: self.textField.view == nil ? ListComposePollOptionComponent.ResetText(value: item.text) : nil,
characterLimit: item.maxLength,
enableInlineAnimations: item.enableAnimations,
emptyLineHandling: .notAllowed,
returnKeyAction: { [weak self] in
guard let self else {
return
}
let _ = self
},
backspaceKeyAction: nil,
selection: nil,
inputMode: item.inputMode,
toggleInputMode: { [weak self] in
guard let self else {
return
}
self.item?.toggleInputMode()
}
)),
environment: {},
containerSize: CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: layout.size.height)
)
let textFieldFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.view.addSubview(textFieldView)
}
textFieldView.frame = textFieldFrame
}
})
}
}
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)
}
public func focus() {
}
public func selectAll() {
}
}

View File

@ -4295,7 +4295,8 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if !result.isEmpty {
result.append(", ")
}
result.append(title)
//TODO:release
result.append(title.text)
}
}
@ -4422,9 +4423,10 @@ func chatListItemTags(location: ChatListControllerLocation, accountPeerId: Engin
if data.color != nil {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
//TODO:release
result.append(ChatListItemContent.Tag(
id: id,
title: title,
title: title.text,
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
))
}

View File

@ -650,6 +650,7 @@ final class ChatSendMessageContextScreenComponent: Component {
), animated: !transition.animation.isImmediate)
} else {
actionsStackNode = ContextControllerActionsStackNode(
context: component.context,
getController: {
return nil
},

View File

@ -655,7 +655,7 @@ final class ComposePollScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
resetText: self.resetPollText.flatMap { resetText in
return ListComposePollOptionComponent.ResetText(value: resetText)
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
},
assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag,
characterLimit: component.initialData.maxPollTextLength,
@ -750,7 +750,7 @@ final class ComposePollScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
resetText: pollOption.resetText.flatMap { resetText in
return ListComposePollOptionComponent.ResetText(value: resetText)
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
},
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
characterLimit: component.initialData.maxPollOptionLength,
@ -1139,7 +1139,7 @@ final class ComposePollScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
resetText: self.resetQuizAnswerText.flatMap { resetText in
return ListComposePollOptionComponent.ResetText(value: resetText)
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
},
assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag,
characterLimit: component.initialData.maxPollTextLength,

View File

@ -16,9 +16,9 @@ import SwiftSignalKit
public final class ListComposePollOptionComponent: Component {
public final class ResetText: Equatable {
public let value: String
public let value: NSAttributedString
public init(value: String) {
public init(value: NSAttributedString) {
self.value = value
}
@ -72,9 +72,11 @@ public final class ListComposePollOptionComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let placeholder: NSAttributedString?
public let resetText: ResetText?
public let assumeIsEditing: Bool
public let characterLimit: Int?
public let enableInlineAnimations: Bool
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
public let returnKeyAction: (() -> Void)?
public let backspaceKeyAction: (() -> Void)?
@ -88,9 +90,11 @@ public final class ListComposePollOptionComponent: Component {
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
placeholder: NSAttributedString? = nil,
resetText: ResetText? = nil,
assumeIsEditing: Bool = false,
characterLimit: Int,
enableInlineAnimations: Bool = true,
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
returnKeyAction: (() -> Void)?,
backspaceKeyAction: (() -> Void)?,
@ -103,9 +107,11 @@ public final class ListComposePollOptionComponent: Component {
self.context = context
self.theme = theme
self.strings = strings
self.placeholder = placeholder
self.resetText = resetText
self.assumeIsEditing = assumeIsEditing
self.characterLimit = characterLimit
self.enableInlineAnimations = enableInlineAnimations
self.emptyLineHandling = emptyLineHandling
self.returnKeyAction = returnKeyAction
self.backspaceKeyAction = backspaceKeyAction
@ -128,6 +134,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.resetText != rhs.resetText {
return false
}
@ -137,6 +146,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
@ -244,6 +256,14 @@ public final class ListComposePollOptionComponent: Component {
}
}
public var currentAttributedText: NSAttributedString {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
return textFieldView.inputState.inputText
} else {
return NSAttributedString(string: "")
}
}
public var textFieldView: TextFieldComponent.View? {
return self.textField.view as? TextFieldComponent.View
}
@ -327,11 +347,18 @@ public final class ListComposePollOptionComponent: Component {
insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0),
hideKeyboard: component.inputMode == .emoji,
customInputView: nil,
placeholder: component.placeholder,
resetText: component.resetText.flatMap { resetText in
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
let result = NSMutableAttributedString(attributedString: resetText.value)
result.addAttributes([
.font: Font.regular(17.0),
.foregroundColor: component.theme.list.itemPrimaryTextColor
], range: NSRange(location: 0, length: result.length))
return result
},
isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit,
enableInlineAnimations: component.enableInlineAnimations,
emptyLineHandling: component.emptyLineHandling,
formatMenuAvailability: .none,
returnKeyType: .next,

View File

@ -125,6 +125,8 @@ public final class ContextMenuActionItem {
public let id: AnyHashable?
public let text: String
public let entities: [MessageTextEntity]
public let enableEntityAnimations: Bool
public let textColor: ContextMenuActionItemTextColor
public let textFont: ContextMenuActionItemFont
public let textLayout: ContextMenuActionItemTextLayout
@ -143,6 +145,8 @@ public final class ContextMenuActionItem {
convenience public init(
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
textFont: ContextMenuActionItemFont = .regular,
@ -161,6 +165,8 @@ public final class ContextMenuActionItem {
self.init(
id: id,
text: text,
entities: entities,
enableEntityAnimations: enableEntityAnimations,
textColor: textColor,
textLayout: textLayout,
textFont: textFont,
@ -185,6 +191,8 @@ public final class ContextMenuActionItem {
public init(
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
textFont: ContextMenuActionItemFont = .regular,
@ -202,6 +210,8 @@ public final class ContextMenuActionItem {
) {
self.id = id
self.text = text
self.entities = entities
self.enableEntityAnimations = enableEntityAnimations
self.textColor = textColor
self.textFont = textFont
self.textLayout = textLayout
@ -258,6 +268,7 @@ func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) ->
final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate {
private weak var controller: ContextController?
private let context: AccountContext?
private var presentationData: PresentationData
private let configuration: ContextController.Configuration
@ -324,6 +335,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
init(
controller: ContextController,
context: AccountContext?,
presentationData: PresentationData,
configuration: ContextController.Configuration,
beginDismiss: @escaping (ContextMenuActionResult) -> Void,
@ -333,6 +345,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
attemptTransitionControllerIntoNavigation: @escaping () -> Void
) {
self.controller = controller
self.context = context
self.presentationData = presentationData
self.configuration = configuration
self.beginDismiss = beginDismiss
@ -704,7 +717,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
}
if let controller = self.controller {
let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration)
let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration, context: self.context)
self.contentReady.set(sourceContainer.ready.get())
self.itemsReady.set(.single(true))
self.sourceContainer = sourceContainer
@ -2437,6 +2450,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
}
private let context: AccountContext?
private var presentationData: PresentationData
private let configuration: ContextController.Configuration
@ -2491,8 +2505,9 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var getOverlayViews: (() -> [UIView])?
convenience public init(presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false) {
convenience public init(context: AccountContext? = nil, presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false) {
self.init(
context: context,
presentationData: presentationData,
configuration: ContextController.Configuration(
sources: [ContextController.Source(
@ -2511,6 +2526,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public init(
context: AccountContext? = nil,
presentationData: PresentationData,
configuration: ContextController.Configuration,
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
@ -2518,6 +2534,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
workaroundUseLegacyImplementation: Bool = false,
disableScreenshots: Bool = false
) {
self.context = context
self.presentationData = presentationData
self.configuration = configuration
self.recognizer = recognizer
@ -2586,7 +2603,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
override public func loadDisplayNode() {
self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in
self.displayNode = ContextControllerNode(controller: self, context: self.context, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in
self?.dismiss(result: result, completion: nil)
}, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in
guard let strongSelf = self else {

View File

@ -15,6 +15,7 @@ import MultiAnimationRenderer
import AnimationUI
import ComponentFlow
import LottieComponent
import TextNodeWithEntities
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
var wantsFullWidth: Bool { get }
@ -71,6 +72,7 @@ public final class ContextControllerPreviewReaction {
public protocol ContextControllerActionsStackItem: AnyObject {
func node(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
@ -94,13 +96,14 @@ public protocol ContextControllerActionsListItemNode: ASDisplayNode {
}
public final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode {
private let context: AccountContext?
private let getController: () -> ContextControllerProtocol?
private let requestDismiss: (ContextMenuActionResult) -> Void
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
private var item: ContextMenuActionItem
private let highlightBackgroundNode: ASDisplayNode
private let titleLabelNode: ImmediateTextNode
private let titleLabelNode: ImmediateTextNodeWithEntities
private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode
private let additionalIconNode: ASImageNode
@ -115,11 +118,13 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
private var iconDisposable: Disposable?
public init(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void,
item: ContextMenuActionItem
) {
self.context = context
self.getController = getController
self.requestDismiss = requestDismiss
self.requestUpdateAction = requestUpdateAction
@ -130,7 +135,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
self.highlightBackgroundNode.isUserInteractionEnabled = false
self.highlightBackgroundNode.alpha = 0.0
self.titleLabelNode = ImmediateTextNode()
self.titleLabelNode = ImmediateTextNodeWithEntities()
self.titleLabelNode.isAccessibilityElement = false
self.titleLabelNode.displaysAsynchronously = false
@ -262,6 +267,17 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
let subtitleColor = presentationData.theme.contextMenu.secondaryColor
if let context = self.context {
self.titleLabelNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1),
attemptSynchronous: true
)
}
self.titleLabelNode.visibility = self.item.enableEntityAnimations
var subtitle: NSAttributedString?
switch self.item.textLayout {
case .singleLine:
@ -296,16 +312,32 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
}
if self.item.parseMarkdown {
let attributedText = parseMarkdownIntoAttributedString(
self.item.text,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor),
linkAttribute: { value in return ("URL", value) }
if self.item.parseMarkdown || !self.item.entities.isEmpty {
let attributedText: NSAttributedString
if !self.item.entities.isEmpty {
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range)
}
return nil
})
let result = NSMutableAttributedString(attributedString: inputStateText.attributedText())
result.addAttributes([
.font: titleFont,
.foregroundColor: titleColor
], range: NSRange(location: 0, length: result.length))
attributedText = result
} else {
attributedText = parseMarkdownIntoAttributedString(
self.item.text,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor),
linkAttribute: { value in return ("URL", value) }
)
)
)
}
self.titleLabelNode.attributedText = attributedText
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
self.titleLabelNode.highlightAttributeAction = { attributes in
@ -692,6 +724,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
}
}
private let context: AccountContext?
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let getController: () -> ContextControllerProtocol?
private let requestDismiss: (ContextMenuActionResult) -> Void
@ -708,11 +741,13 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
}
init(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
items: [ContextMenuItem]
) {
self.context = context
self.requestUpdate = requestUpdate
self.getController = getController
self.requestDismiss = requestDismiss
@ -724,6 +759,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
case let .action(actionItem):
return Item(
node: ContextControllerActionsListActionItemNode(
context: context,
getController: getController,
requestDismiss: requestDismiss,
requestUpdateAction: { id, action in
@ -794,6 +830,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
let addedNode = Item(
node: ContextControllerActionsListActionItemNode(
context: self.context,
getController: self.getController,
requestDismiss: self.requestDismiss,
requestUpdateAction: { [weak self] id, action in
@ -981,12 +1018,14 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
}
public func node(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerActionsStackItemNode {
return Node(
context: context,
getController: getController,
requestDismiss: requestDismiss,
requestUpdate: requestUpdate,
@ -1082,6 +1121,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
}
func node(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
@ -1246,6 +1286,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
private var tipDisposable: Disposable?
init(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
@ -1262,6 +1303,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
self.requestUpdate = requestUpdate
self.item = item
self.node = item.node(
context: context,
getController: getController,
requestDismiss: requestDismiss,
requestUpdate: requestUpdate,
@ -1396,6 +1438,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
}
}
private let context: AccountContext?
private let getController: () -> ContextControllerProtocol?
private let requestDismiss: (ContextMenuActionResult) -> Void
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
@ -1423,10 +1466,12 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
}
public init(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
) {
self.context = context
self.getController = getController
self.requestDismiss = requestDismiss
self.requestUpdate = requestUpdate
@ -1534,6 +1579,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
itemContainer.storedScrollingState = currentScrollingState
}
let itemContainer = ItemContainer(
context: self.context,
getController: self.getController,
requestDismiss: self.requestDismiss,
requestUpdate: self.requestUpdate,

View File

@ -8,6 +8,7 @@ import TelegramCore
import SwiftSignalKit
import ReactionSelectionNode
import UndoUI
import AccountContext
private extension ContextControllerTakeViewInfo.ContainingItem {
var contentRect: CGRect {
@ -227,6 +228,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
return self._ready.get()
}
private let context: AccountContext?
private let getController: () -> ContextControllerProtocol?
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void
@ -268,6 +270,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private weak var currentUndoController: ViewController?
init(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void,
@ -275,6 +278,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
source: ContentSource
) {
self.context = context
self.getController = getController
self.requestUpdate = requestUpdate
self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard
@ -308,6 +312,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.actionsContainerNode = ASDisplayNode()
self.actionsStackNode = ContextControllerActionsStackNode(
context: self.context,
getController: getController,
requestDismiss: { result in
requestDismiss(result)
@ -316,6 +321,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
)
self.additionalActionsStackNode = ContextControllerActionsStackNode(
context: self.context,
getController: getController,
requestDismiss: { result in
requestDismiss(result)

View File

@ -9,6 +9,7 @@ import ComponentFlow
import TabSelectorComponent
import PlainButtonComponent
import ComponentDisplayAdapters
import AccountContext
final class ContextSourceContainer: ASDisplayNode {
final class Source {
@ -16,6 +17,7 @@ final class ContextSourceContainer: ASDisplayNode {
let id: AnyHashable
let title: String
let context: AccountContext?
let source: ContextContentSource
let closeActionTitle: String?
let closeAction: (() -> Void)?
@ -42,6 +44,7 @@ final class ContextSourceContainer: ASDisplayNode {
controller: ContextController,
id: AnyHashable,
title: String,
context: AccountContext?,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
closeActionTitle: String? = nil,
@ -50,6 +53,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.controller = controller
self.id = id
self.title = title
self.context = context
self.source = source
self.closeActionTitle = closeActionTitle
self.closeAction = closeAction
@ -65,6 +69,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
context: self.context,
getController: { [weak self] in
guard let self else {
return nil
@ -105,6 +110,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
context: self.context,
getController: { [weak self] in
guard let self else {
return nil
@ -145,6 +151,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
context: self.context,
getController: { [weak self] in
guard let self else {
return nil
@ -188,6 +195,7 @@ final class ContextSourceContainer: ASDisplayNode {
self.contentReady.set(source.controller.ready.get())
let presentationNode = ContextControllerExtractedPresentationNode(
context: self.context,
getController: { [weak self] in
guard let self else {
return nil
@ -373,7 +381,7 @@ final class ContextSourceContainer: ASDisplayNode {
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
}
init(controller: ContextController, configuration: ContextController.Configuration) {
init(controller: ContextController, configuration: ContextController.Configuration, context: AccountContext?) {
self.controller = controller
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
@ -389,6 +397,7 @@ final class ContextSourceContainer: ASDisplayNode {
controller: controller,
id: source.id,
title: source.title,
context: context,
source: source.source,
items: source.items,
closeActionTitle: source.closeActionTitle,

View File

@ -80,6 +80,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
self.actionsStackNode = ContextControllerActionsStackNode(
context: nil,
getController: { [weak controller] in
return controller
},

View File

@ -304,7 +304,7 @@ private struct FolderInviteLinkListControllerState: Equatable {
var isSaving: Bool = false
}
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [EnginePeer.Id], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController {
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, title filterTitle: ChatFolderTitle, allPeerIds: [EnginePeer.Id], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void, presentController parentPresentController: ((ViewController) -> Void)?) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
let _ = pushControllerImpl
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
@ -699,10 +699,11 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
//TODO:release
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries(
presentationData: presentationData,
state: state,
title: filterTitle,
title: filterTitle.text,
allPeers: allPeers
), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges)

View File

@ -28,7 +28,7 @@ public enum ItemListSectionHeaderActivityIndicator {
case left
case right
fileprivate var hasActivity: Bool {
public var hasActivity: Bool {
switch self {
case .left, .right:
return true

View File

@ -230,9 +230,37 @@ public struct ChatListFilterData: Equatable, Hashable {
}
}
public struct ChatFolderTitle: Codable, Equatable {
public let text: String
public let entities: [MessageTextEntity]
public var enableAnimations: Bool
public init(text: String, entities: [MessageTextEntity], enableAnimations: Bool) {
self.text = text
self.entities = entities
self.enableAnimations = enableAnimations
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.text = try container.decode(String.self, forKey: "text")
self.entities = try container.decode([MessageTextEntity].self, forKey: "entities")
self.enableAnimations = try container.decodeIfPresent(Bool.self, forKey: "enableAnimations") ?? true
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.text, forKey: "text")
try container.encode(self.entities, forKey: "entities")
try container.encode(self.enableAnimations, forKey: "enableAnimations")
}
}
public enum ChatListFilter: Codable, Equatable {
case allChats
case filter(id: Int32, title: String, emoticon: String?, data: ChatListFilterData)
case filter(id: Int32, title: ChatFolderTitle, emoticon: String?, data: ChatListFilterData)
public var id: Int32 {
switch self {
@ -251,7 +279,14 @@ public enum ChatListFilter: Codable, Equatable {
self = .allChats
} else {
let id = try container.decode(Int32.self, forKey: "id")
let title = try container.decode(String.self, forKey: "title")
let title: ChatFolderTitle
if let titleWithEntities = try container.decodeIfPresent(ChatFolderTitle.self, forKey: "titleWithEntities") {
title = titleWithEntities
} else {
title = ChatFolderTitle(text: try container.decode(String.self, forKey: "title"), entities: [], enableAnimations: true)
}
let emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon")
let data = ChatListFilterData(
@ -284,7 +319,7 @@ public enum ChatListFilter: Codable, Equatable {
try container.encode(type, forKey: "t")
try container.encode(id, forKey: "id")
try container.encode(title, forKey: "title")
try container.encode(title, forKey: "titleWithEntities")
try container.encodeIfPresent(emoticon, forKey: "emoticon")
try container.encode(data.isShared, forKey: "isShared")
@ -309,7 +344,7 @@ extension ChatListFilter {
case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers):
self = .filter(
id: id,
title: title,
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
emoticon: emoticon,
data: ChatListFilterData(
isShared: false,
@ -359,7 +394,7 @@ extension ChatListFilter {
case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers):
self = .filter(
id: id,
title: title,
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
emoticon: emoticon,
data: ChatListFilterData(
isShared: true,
@ -400,9 +435,9 @@ extension ChatListFilter {
func apiFilter(transaction: Transaction) -> Api.DialogFilter? {
switch self {
case .allChats:
return nil
case let .filter(id, title, emoticon, data):
case .allChats:
return nil
case let .filter(id, title, emoticon, data):
if data.isShared {
var flags: Int32 = 0
if emoticon != nil {
@ -411,7 +446,7 @@ extension ChatListFilter {
if data.color != nil {
flags |= 1 << 27
}
return .dialogFilterChatlist(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return .dialogFilterChatlist(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
@ -437,7 +472,7 @@ extension ChatListFilter {
if data.color != nil {
flags |= 1 << 27
}
return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return .dialogFilter(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
@ -1099,12 +1134,12 @@ func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersS
}
public struct ChatListFeaturedFilter: Codable, Equatable {
public var title: String
public var title: ChatFolderTitle
public var description: String
public var data: ChatListFilterData
fileprivate init(
title: String,
title: ChatFolderTitle,
description: String,
data: ChatListFilterData
) {
@ -1116,7 +1151,11 @@ public struct ChatListFeaturedFilter: Codable, Equatable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.title = try container.decode(String.self, forKey: "title")
if let title = try container.decodeIfPresent(ChatFolderTitle.self, forKey: "titleWithEntities") {
self.title = title
} else {
self.title = ChatFolderTitle(text: try container.decode(String.self, forKey: "title"), entities: [], enableAnimations: true)
}
self.description = try container.decode(String.self, forKey: "description")
self.data = ChatListFilterData(
isShared: false,
@ -1137,7 +1176,7 @@ public struct ChatListFeaturedFilter: Codable, Equatable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.title, forKey: "title")
try container.encode(self.title, forKey: "titleWithEntities")
try container.encode(self.description, forKey: "description")
try container.encode(self.data.categories.rawValue, forKey: "categories")
try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted")

View File

@ -244,14 +244,14 @@ public enum CheckChatFolderLinkError {
public final class ChatFolderLinkContents {
public let localFilterId: Int32?
public let title: String?
public let title: ChatFolderTitle?
public let peers: [EnginePeer]
public let alreadyMemberPeerIds: Set<EnginePeer.Id>
public let memberCounts: [EnginePeer.Id: Int]
public init(
localFilterId: Int32?,
title: String?,
title: ChatFolderTitle?,
peers: [EnginePeer],
alreadyMemberPeerIds: Set<EnginePeer.Id>,
memberCounts: [EnginePeer.Id: Int]
@ -301,7 +301,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
}
}
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
return ChatFolderLinkContents(localFilterId: nil, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
case let .chatlistInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
var memberCounts: [PeerId: Int] = [:]
@ -317,7 +317,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
let currentFilters = _internal_currentChatListFilters(transaction: transaction)
var currentFilterTitle: String?
var currentFilterTitle: ChatFolderTitle?
if let index = currentFilters.firstIndex(where: { $0.id == filterId }) {
switch currentFilters[index] {
case let .filter(_, title, _, _):
@ -367,10 +367,10 @@ public enum JoinChatFolderLinkError {
public final class JoinChatFolderResult {
public let folderId: Int32
public let title: String
public let title: ChatFolderTitle
public let newChatCount: Int
public init(folderId: Int32, title: String, newChatCount: Int) {
public init(folderId: Int32, title: ChatFolderTitle, newChatCount: Int) {
self.folderId = folderId
self.title = title
self.newChatCount = newChatCount
@ -378,25 +378,6 @@ public final class JoinChatFolderResult {
}
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
/*#if DEBUG
if "".isEmpty {
return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in
return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false)
}
|> castError(JoinChatFolderLinkError.self)
|> mapToSignal { appConfiguration, isPremium -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> in
let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false)
let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true)
if isPremium {
return .fail(.tooManyChannelsInAccount(limit: userPremiumLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount))
} else {
return .fail(.tooManyChannelsInAccount(limit: userDefaultLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount))
}
}
}
#endif*/
return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in
var newChatCount = 0
for peerId in peerIds {
@ -522,7 +503,7 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
public final class ChatFolderUpdates: Equatable {
public let folderId: Int32
fileprivate let title: String
fileprivate let title: ChatFolderTitle
fileprivate let missingPeers: [EnginePeer]
fileprivate let memberCounts: [EnginePeer.Id: Int]
@ -536,7 +517,7 @@ public final class ChatFolderUpdates: Equatable {
fileprivate init(
folderId: Int32,
title: String,
title: ChatFolderTitle,
missingPeers: [EnginePeer],
memberCounts: [EnginePeer.Id: Int]
) {
@ -647,7 +628,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
struct InternalData: Equatable {
var title: String
var title: ChatFolderTitle
var peerIds: [EnginePeer.Id]
var memberCounts: [EnginePeer.Id: Int]
}

View File

@ -100,7 +100,7 @@ public extension TelegramEngine {
case same
case archived
case unarchived
case folder(id: Int32, title: String)
case folder(id: Int32, title: ChatFolderTitle)
}
final class Peers {

View File

@ -6,6 +6,7 @@ import ComponentFlow
import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
final class BadgeComponent: Component {
let fillColor: UIColor
@ -85,13 +86,13 @@ final class BadgeComponent: Component {
final class ChatFolderLinkHeaderComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let title: String
let title: ChatFolderTitle
let badge: String?
init(
theme: PresentationTheme,
strings: PresentationStrings,
title: String,
title: ChatFolderTitle,
badge: String?
) {
self.theme = theme
@ -212,9 +213,10 @@ final class ChatFolderLinkHeaderComponent: Component {
}
contentWidth += spacing
//TODO:release
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)),
component: AnyComponent(Text(text: component.title.text, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)

View File

@ -439,7 +439,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
component: AnyComponent(ChatFolderLinkHeaderComponent(
theme: environment.theme,
strings: environment.strings,
title: component.linkContents?.title ?? "Folder",
title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true),
badge: topBadge
)),
environment: {},
@ -469,7 +469,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
text = environment.strings.FolderLinkPreview_TextAddFolder
} else {
let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount))
text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title ?? "").string
//TODO:release
text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title?.text ?? "").string
}
} else {
text = " "
@ -981,7 +982,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems)))
disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId]))
let folderTitle = linkContents.title ?? ""
//TODO:release
let folderTitle = linkContents.title?.text ?? ""
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
@ -1110,7 +1112,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
if isUpdates {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
} else if result.newChatCount != 0 {
let animationBackgroundColor: UIColor
if presentationData.theme.overallDarkAppearance {
@ -1118,7 +1121,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
} else {
animationBackgroundColor = UIColor(rgb: 0x474747)
}
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
} else {
let animationBackgroundColor: UIColor
if presentationData.theme.overallDarkAppearance {
@ -1126,7 +1130,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
} else {
animationBackgroundColor = UIColor(rgb: 0x474747)
}
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
//TODO:release
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title.text).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
}
})
}
@ -1305,6 +1310,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
})
let navigationController = controller.navigationController
//TODO:release
controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
}))

View File

@ -231,20 +231,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
let emoji: ChatTextInputTextCustomEmojiAttribute
let cache: AnimationCache
let renderer: MultiAnimationRenderer
let unique: Bool
let placeholderColor: UIColor
let loopCount: Int?
let pointSize: CGSize
let pixelSize: CGSize
init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, loopCount: Int?, pointSize: CGSize, pixelSize: CGSize) {
init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, loopCount: Int?, pointSize: CGSize, pixelSize: CGSize) {
self.context = context
self.userLocation = userLocation
self.emoji = emoji
self.cache = cache
self.renderer = renderer
self.unique = unique
self.placeholderColor = placeholderColor
self.loopCount = loopCount
self.pointSize = pointSize
@ -373,6 +371,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private var currentLoopCount: Int = 0
public var isUnique: Bool = false
private var isInHierarchyValue: Bool = false
public var isVisibleForAnimations: Bool = false {
didSet {
@ -440,13 +440,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) {
let scale = min(2.0, UIScreenScale)
self.isUnique = unique
self.arguments = Arguments(
context: context,
userLocation: userLocation,
emoji: emoji,
cache: cache,
renderer: renderer,
unique: unique,
placeholderColor: placeholderColor,
loopCount: loopCount,
pointSize: pointSize,
@ -670,7 +671,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if attemptSynchronousLoad {
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) {
}
self.loadAnimation()
@ -694,19 +694,27 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
let keyframeOnly = arguments.pixelSize.width >= 120.0
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil))
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: self.isUnique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil))
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
}
if self.file?.fileId == file.fileId {
return
}
self.file = file
self.updateFile(attemptSynchronousLoad: attemptSynchronousLoad)
}
private func updateFile(attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
}
guard let file = self.file else {
return
}
self.loadDisposable?.dispose()
if attemptSynchronousLoad {
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) {
@ -757,6 +765,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
}
public func reloadAnimation() {
self.updateFile(attemptSynchronousLoad: false)
}
private func loadAnimation() {
guard let arguments = self.arguments else {
return
@ -768,13 +780,15 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
let isTemplate = file.isCustomTemplateEmoji
self.disposable?.dispose()
let context = arguments.context
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
let keyframeOnly = arguments.pixelSize.width >= 120.0
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: self.isUnique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
} else {
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: self.isUnique, size: arguments.pixelSize, fetch: { options in
let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else {
return
@ -846,6 +860,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
public final class EmojiTextAttachmentView: UIView {
public let contentLayer: InlineStickerItemLayer
public var isUnique: Bool = false {
didSet {
if self.isActive != oldValue {
self.contentLayer.isUnique = self.isUnique
}
}
}
public var isActive: Bool = true {
didSet {
if self.isActive != oldValue {
@ -867,8 +888,8 @@ public final class EmojiTextAttachmentView: UIView {
)
}
public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) {
self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize)
public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize) {
self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, pointSize: pointSize)
super.init(frame: CGRect())
@ -889,6 +910,10 @@ public final class EmojiTextAttachmentView: UIView {
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height))
}
public func resetToFirstFrame() {
self.contentLayer.reloadAnimation()
}
}
public final class CustomEmojiContainerView: UIView {

View File

@ -79,7 +79,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu
f(.dismissWithoutContent)
}
})
let actionNode = ContextControllerActionsListActionItemNode(getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
let actionNode = ContextControllerActionsListActionItemNode(context: nil, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
actionNodes.append(actionNode)
if actionNodes.count != item.packs.count {
let separatorNode = ASDisplayNode()

View File

@ -190,7 +190,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
if params.hasFilters {
self._ready.set(.never())
self.tabContainerNode = ChatListFilterTabContainerNode()
self.tabContainerNode = ChatListFilterTabContainerNode(context: self.context)
self.reloadFilters()
self.peerSelectionNode.mainContainerNode?.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in

View File

@ -24,6 +24,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
"//submodules/TextInputMenu",
"//submodules/ObjCRuntimeUtils",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -16,6 +16,7 @@ import ImageTransparency
import ChatInputTextNode
import TextInputMenu
import ObjCRuntimeUtils
import MultilineTextComponent
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
public var enableInputClicksWhenVisible: Bool {
@ -128,10 +129,12 @@ public final class TextFieldComponent: Component {
public let insets: UIEdgeInsets
public let hideKeyboard: Bool
public let customInputView: UIView?
public let placeholder: NSAttributedString?
public let resetText: NSAttributedString?
public let assumeIsEditing: Bool
public let isOneLineWhenUnfocused: Bool
public let characterLimit: Int?
public let enableInlineAnimations: Bool
public let emptyLineHandling: EmptyLineHandling
public let formatMenuAvailability: FormatMenuAvailability
public let returnKeyType: UIReturnKeyType
@ -152,10 +155,12 @@ public final class TextFieldComponent: Component {
insets: UIEdgeInsets,
hideKeyboard: Bool,
customInputView: UIView?,
placeholder: NSAttributedString? = nil,
resetText: NSAttributedString?,
assumeIsEditing: Bool = false,
isOneLineWhenUnfocused: Bool,
characterLimit: Int? = nil,
enableInlineAnimations: Bool = true,
emptyLineHandling: EmptyLineHandling = .allowed,
formatMenuAvailability: FormatMenuAvailability,
returnKeyType: UIReturnKeyType = .default,
@ -175,10 +180,12 @@ public final class TextFieldComponent: Component {
self.insets = insets
self.hideKeyboard = hideKeyboard
self.customInputView = customInputView
self.placeholder = placeholder
self.resetText = resetText
self.assumeIsEditing = assumeIsEditing
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
self.characterLimit = characterLimit
self.enableInlineAnimations = enableInlineAnimations
self.emptyLineHandling = emptyLineHandling
self.formatMenuAvailability = formatMenuAvailability
self.returnKeyType = returnKeyType
@ -220,6 +227,9 @@ public final class TextFieldComponent: Component {
if lhs.customInputView !== rhs.customInputView {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.resetText != rhs.resetText {
return false
}
@ -232,6 +242,9 @@ public final class TextFieldComponent: Component {
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
@ -261,6 +274,7 @@ public final class TextFieldComponent: Component {
}
public final class View: UIView, UIScrollViewDelegate, ChatInputTextNodeDelegate {
private var placeholder: ComponentView<Empty>?
private let textView: ChatInputTextView
private let inputMenu: TextInputMenu
@ -1164,6 +1178,18 @@ public final class TextFieldComponent: Component {
}
customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects)
for (_, emojiView) in customEmojiContainerView.emojiLayers {
if let emojiView = emojiView as? EmojiTextAttachmentView {
if emojiView.isActive != component.enableInlineAnimations {
emojiView.isUnique = !component.enableInlineAnimations
emojiView.isActive = component.enableInlineAnimations
if !emojiView.isActive {
emojiView.resetToFirstFrame()
}
}
}
}
} else if let customEmojiContainerView = self.customEmojiContainerView {
customEmojiContainerView.removeFromSuperview()
self.customEmojiContainerView = nil
@ -1341,6 +1367,40 @@ public final class TextFieldComponent: Component {
self.textView.updateLayout(size: textFrame.size)
self.textView.panGestureRecognizer.isEnabled = isEditing
if let placeholderValue = component.placeholder {
var placeholderTransition = transition
let placeholder: ComponentView<Empty>
if let current = self.placeholder {
placeholder = current
} else {
placeholderTransition = placeholderTransition.withAnimation(.none)
placeholder = ComponentView()
self.placeholder = placeholder
}
let placeholderSize = placeholder.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(placeholderValue)
)),
environment: {},
containerSize: textFrame.size
)
let placeholderFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.minY + floor((textFrame.height - placeholderSize.height) * 0.5) - 1.0), size: placeholderSize)
if let placeholderView = placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
self.insertSubview(placeholderView, belowSubview: self.textView)
}
placeholderTransition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
placeholderView.isHidden = self.textView.textStorage.length != 0
}
} else if let placeholder = self.placeholder {
self.placeholder = nil
placeholder.view?.removeFromSuperview()
}
self.updateEmojiSuggestion(transition: .immediate)
if refreshScrolling {

View File

@ -2334,8 +2334,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, [])
releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, [])
case let .folder(_, title):
swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title)._tuple
releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title)._tuple
//TODO:release
swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title.text)._tuple
releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title.text)._tuple
}
if expandProgress < 0.1 {

View File

@ -133,7 +133,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
var chatListFilter: ChatListFilter?
if chatSelection.onlyUsers {
chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData(
chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData(
isShared: false,
hasSharedLinks: false,
categories: [.contacts, .nonContacts],
@ -153,7 +153,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
categories.remove(.bots)
}
chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData(
chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData(
isShared: false,
hasSharedLinks: false,
categories: categories,