mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Chat folder emoji
This commit is contained in:
parent
fe2ebc4e85
commit
e18795980e
@ -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",
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
275
submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift
Normal file
275
submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift
Normal 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() {
|
||||
}
|
||||
}
|
@ -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
|
||||
))
|
||||
}
|
||||
|
@ -650,6 +650,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
), animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
actionsStackNode = ContextControllerActionsStackNode(
|
||||
context: component.context,
|
||||
getController: {
|
||||
return nil
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -80,6 +80,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
self.actionsStackNode = ContextControllerActionsStackNode(
|
||||
context: nil,
|
||||
getController: { [weak controller] in
|
||||
return controller
|
||||
},
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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))
|
||||
}))
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -24,6 +24,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
|
||||
"//submodules/TextInputMenu",
|
||||
"//submodules/ObjCRuntimeUtils",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user