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/Components/MultilineTextComponent",
|
||||||
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
|
||||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||||
|
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||||
|
"//submodules/ComposePollUI",
|
||||||
|
"//submodules/ChatPresentationInterfaceState",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -221,9 +221,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||||||
}
|
}
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).startStandalone(completed: {
|
|> deliverOnMainQueue).startStandalone(completed: {
|
||||||
c?.dismiss(completion: {
|
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
|
return false
|
||||||
}), in: .current)
|
}), in: .current)
|
||||||
})
|
})
|
||||||
@ -273,7 +274,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
let filterType = chatListFilterType(data)
|
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
|
let imageName: String
|
||||||
switch filterType {
|
switch filterType {
|
||||||
case .generic:
|
case .generic:
|
||||||
@ -337,8 +339,8 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||||||
}
|
}
|
||||||
return filters
|
return filters
|
||||||
}).startStandalone()
|
}).startStandalone()
|
||||||
|
//TODO:release
|
||||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
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
|
return false
|
||||||
}), in: .current)
|
}), in: .current)
|
||||||
})
|
})
|
||||||
|
@ -246,7 +246,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.tabsNode = SparseNode()
|
self.tabsNode = SparseNode()
|
||||||
self.tabContainerNode = ChatListFilterTabContainerNode()
|
self.tabContainerNode = ChatListFilterTabContainerNode(context: context)
|
||||||
self.tabsNode.addSubnode(self.tabContainerNode)
|
self.tabsNode.addSubnode(self.tabContainerNode)
|
||||||
|
|
||||||
super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource)
|
super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource)
|
||||||
@ -1809,10 +1809,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO:release
|
||||||
let iconColor: UIColor = .white
|
let iconColor: UIColor = .white
|
||||||
let overlayController: UndoOverlayController
|
let overlayController: UndoOverlayController
|
||||||
if !filterPeersAreMuted.areMuted {
|
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: [
|
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
|
||||||
"Middle.Group 1.Fill 1": iconColor,
|
"Middle.Group 1.Fill 1": iconColor,
|
||||||
"Top.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
|
"Line.Group 1.Stroke 1": iconColor
|
||||||
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
||||||
} else {
|
} 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: [
|
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
|
||||||
"Middle.Group 1.Fill 1": iconColor,
|
"Middle.Group 1.Fill 1": iconColor,
|
||||||
"Top.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 presentationData = self.presentationData
|
||||||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||||
@ -3962,7 +3964,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
context: self.context,
|
context: self.context,
|
||||||
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
|
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
|
||||||
contents: ChatFolderLinkContents(
|
contents: ChatFolderLinkContents(
|
||||||
localFilterId: filterId, title: title,
|
localFilterId: filterId,
|
||||||
|
title: title,
|
||||||
peers: [],
|
peers: [],
|
||||||
alreadyMemberPeerIds: Set(),
|
alreadyMemberPeerIds: Set(),
|
||||||
memberCounts: [:]
|
memberCounts: [:]
|
||||||
@ -5928,7 +5931,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
|
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
|
let imageName: String
|
||||||
if isDisabled {
|
if isDisabled {
|
||||||
imageName = "Chat/Context Menu/Lock"
|
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)
|
strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,12 @@ import ContextUI
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import UndoUI
|
import UndoUI
|
||||||
import PeerNameColorItem
|
import PeerNameColorItem
|
||||||
|
import EntityKeyboard
|
||||||
|
import ComposePollUI
|
||||||
|
import ChatEntityKeyboardInputNode
|
||||||
|
import ComponentFlow
|
||||||
|
import ChatPresentationInterfaceState
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
|
||||||
private enum FilterSection: Int32, Hashable {
|
private enum FilterSection: Int32, Hashable {
|
||||||
case include
|
case include
|
||||||
@ -28,6 +34,8 @@ private enum FilterSection: Int32, Hashable {
|
|||||||
private final class ChatListFilterPresetControllerArguments {
|
private final class ChatListFilterPresetControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
||||||
|
let updateName: (ChatFolderTitle) -> Void
|
||||||
|
let toggleNameAnimations: () -> Void
|
||||||
let openAddIncludePeer: () -> Void
|
let openAddIncludePeer: () -> Void
|
||||||
let openAddExcludePeer: () -> Void
|
let openAddExcludePeer: () -> Void
|
||||||
let deleteIncludePeer: (EnginePeer.Id) -> Void
|
let deleteIncludePeer: (EnginePeer.Id) -> Void
|
||||||
@ -49,6 +57,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
||||||
|
updateName: @escaping (ChatFolderTitle) -> Void,
|
||||||
|
toggleNameAnimations: @escaping () -> Void,
|
||||||
openAddIncludePeer: @escaping () -> Void,
|
openAddIncludePeer: @escaping () -> Void,
|
||||||
openAddExcludePeer: @escaping () -> Void,
|
openAddExcludePeer: @escaping () -> Void,
|
||||||
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
|
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||||
@ -69,6 +79,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.updateState = updateState
|
self.updateState = updateState
|
||||||
|
self.updateName = updateName
|
||||||
|
self.toggleNameAnimations = toggleNameAnimations
|
||||||
self.openAddIncludePeer = openAddIncludePeer
|
self.openAddIncludePeer = openAddIncludePeer
|
||||||
self.openAddExcludePeer = openAddExcludePeer
|
self.openAddExcludePeer = openAddExcludePeer
|
||||||
self.deleteIncludePeer = deleteIncludePeer
|
self.deleteIncludePeer = deleteIncludePeer
|
||||||
@ -216,8 +228,8 @@ private enum ChatListFilterRevealedItemId: Equatable {
|
|||||||
|
|
||||||
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||||
case screenHeader
|
case screenHeader
|
||||||
case nameHeader(String)
|
case nameHeader(title: String, enableAnimations: Bool)
|
||||||
case name(placeholder: String, value: String)
|
case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool)
|
||||||
case includePeersHeader(String)
|
case includePeersHeader(String)
|
||||||
case addIncludePeer(title: String)
|
case addIncludePeer(title: String)
|
||||||
case includeCategory(index: Int, category: ChatListFilterIncludeCategory, title: String, isRevealed: Bool)
|
case includeCategory(index: Int, category: ChatListFilterIncludeCategory, title: String, isRevealed: Bool)
|
||||||
@ -234,7 +246,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
|||||||
case inviteLinkCreate(hasLinks: Bool)
|
case inviteLinkCreate(hasLinks: Bool)
|
||||||
case inviteLink(Int, ExportedChatFolderLink)
|
case inviteLink(Int, ExportedChatFolderLink)
|
||||||
case inviteLinkInfo(text: String)
|
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 tagColor(colors: PeerNameColors, currentColor: PeerNameColor?, isPremium: Bool)
|
||||||
case tagColorFooter
|
case tagColorFooter
|
||||||
|
|
||||||
@ -362,21 +374,36 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
|||||||
switch self {
|
switch self {
|
||||||
case .screenHeader:
|
case .screenHeader:
|
||||||
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section)
|
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section)
|
||||||
case let .nameHeader(title):
|
case let .nameHeader(title, enableAnimations):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
//TODO:localize
|
||||||
case let .name(placeholder, value):
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: {
|
||||||
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.toggleNameAnimations()
|
||||||
arguments.updateState { current in
|
}, sectionId: self.section)
|
||||||
var state = current
|
case let .name(placeholder, value, inputMode, enableAnimations):
|
||||||
state.name = value
|
return ItemListFilterTitleInputItem(
|
||||||
state.changedName = true
|
context: arguments.context,
|
||||||
return state
|
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):
|
case .includePeersHeader(let text), .excludePeersHeader(let text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case .includePeerInfo(let text), .excludePeerInfo(let text):
|
case .includePeerInfo(let text), .excludePeerInfo(let text):
|
||||||
@ -462,13 +489,13 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
|||||||
arguments.expandSection(.exclude)
|
arguments.expandSection(.exclude)
|
||||||
})
|
})
|
||||||
case let .tagColorHeader(name, color, isPremium):
|
case let .tagColorHeader(name, color, isPremium):
|
||||||
var badge: String?
|
var badge: ChatFolderTitle?
|
||||||
var badgeStyle: ItemListSectionHeaderItem.BadgeStyle?
|
var badgeStyle: ChatListFilterTagSectionHeaderItem.BadgeStyle?
|
||||||
var accessoryText: ItemListSectionHeaderAccessoryText?
|
var accessoryText: ItemListSectionHeaderAccessoryText?
|
||||||
if isPremium {
|
if isPremium {
|
||||||
if let color {
|
if let color {
|
||||||
badge = name.uppercased()
|
badge = ChatFolderTitle(text: name.text.uppercased(), entities: name.entities, enableAnimations: name.enableAnimations)
|
||||||
badgeStyle = ItemListSectionHeaderItem.BadgeStyle(
|
badgeStyle = ChatListFilterTagSectionHeaderItem.BadgeStyle(
|
||||||
background: color.main.withMultipliedAlpha(0.1),
|
background: color.main.withMultipliedAlpha(0.1),
|
||||||
foreground: color.main
|
foreground: color.main
|
||||||
)
|
)
|
||||||
@ -478,7 +505,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
|||||||
} else if color != nil {
|
} else if color != nil {
|
||||||
accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelPremiumExpired, color: .generic)
|
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):
|
case let .tagColor(colors, color, isPremium):
|
||||||
return PeerNameColorItem(
|
return PeerNameColorItem(
|
||||||
theme: presentationData.theme,
|
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 {
|
private struct ChatListFilterPresetControllerState: Equatable {
|
||||||
var name: String
|
var name: ChatFolderTitle
|
||||||
var changedName: Bool
|
var changedName: Bool
|
||||||
|
var nameInputMode: ListComposePollOptionComponent.InputMode = .keyboard
|
||||||
var color: PeerNameColor?
|
var color: PeerNameColor?
|
||||||
var colorUpdated: Bool = false
|
var colorUpdated: Bool = false
|
||||||
var includeCategories: ChatListFilterPeerCategories
|
var includeCategories: ChatListFilterPeerCategories
|
||||||
@ -534,7 +593,7 @@ private struct ChatListFilterPresetControllerState: Equatable {
|
|||||||
var expandedSections: Set<FilterSection>
|
var expandedSections: Set<FilterSection>
|
||||||
|
|
||||||
var isComplete: Bool {
|
var isComplete: Bool {
|
||||||
if self.name.isEmpty {
|
if self.name.text.isEmpty {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,8 +624,8 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
|
|||||||
entries.append(.screenHeader)
|
entries.append(.screenHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.append(.nameHeader(presentationData.strings.ChatListFolder_NameSectionHeader))
|
entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations))
|
||||||
entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name))
|
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))
|
entries.append(.includePeersHeader(presentationData.strings.ChatListFolder_IncludedSectionHeader))
|
||||||
if includePeers.count < limit {
|
if includePeers.count < limit {
|
||||||
@ -648,6 +707,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
|
|||||||
resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance)
|
resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium))
|
entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium))
|
||||||
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium))
|
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium))
|
||||||
entries.append(.tagColorFooter)
|
entries.append(.tagColorFooter)
|
||||||
@ -1015,11 +1075,11 @@ func chatListFilterType(_ data: ChatListFilterData) -> ChatListFilterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension ChatListFilter {
|
private extension ChatListFilter {
|
||||||
var title: String {
|
var title: ChatFolderTitle {
|
||||||
if case let .filter(_, title, _, _) = self {
|
if case let .filter(_, title, _, _) = self {
|
||||||
return title
|
return title
|
||||||
} else {
|
} 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 {
|
func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController {
|
||||||
let initialName: String
|
let initialName: ChatFolderTitle
|
||||||
if let initialPreset {
|
if let initialPreset {
|
||||||
initialName = initialPreset.title
|
initialName = initialPreset.title
|
||||||
} else {
|
} 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
|
initialState.colorUpdated = true
|
||||||
|
|
||||||
let updatedCurrentPreset: Signal<ChatListFilter?, NoError>
|
let updatedCurrentPreset: Signal<ChatListFilter?, NoError>
|
||||||
@ -1061,6 +1445,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
updatedCurrentPreset = .single(nil)
|
updatedCurrentPreset = .single(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var withController: (((ChatListFilterPresetController) -> Void) -> Void)?
|
||||||
|
|
||||||
let stateValue = Atomic(value: initialState)
|
let stateValue = Atomic(value: initialState)
|
||||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
|
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in
|
||||||
@ -1076,24 +1462,29 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
case .generic:
|
case .generic:
|
||||||
state.name = initialName
|
state.name = initialName
|
||||||
case .unmuted:
|
case .unmuted:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameNonMuted
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonMuted, entities: [], enableAnimations: true)
|
||||||
case .unread:
|
case .unread:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameUnread
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameUnread, entities: [], enableAnimations: true)
|
||||||
case .channels:
|
case .channels:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameChannels
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameChannels, entities: [], enableAnimations: true)
|
||||||
case .groups:
|
case .groups:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameGroups
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameGroups, entities: [], enableAnimations: true)
|
||||||
case .bots:
|
case .bots:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameBots
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameBots, entities: [], enableAnimations: true)
|
||||||
case .contacts:
|
case .contacts:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameContacts
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameContacts, entities: [], enableAnimations: true)
|
||||||
case .nonContacts:
|
case .nonContacts:
|
||||||
state.name = presentationData.strings.ChatListFolder_NameNonContacts
|
state.name = ChatFolderTitle(text: presentationData.strings.ChatListFolder_NameNonContacts, entities: [], enableAnimations: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state
|
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
|
var skipStateAnimation = false
|
||||||
|
|
||||||
@ -1180,6 +1571,30 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
updateState: { f in
|
updateState: { f in
|
||||||
updateState(f)
|
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: {
|
openAddIncludePeer: {
|
||||||
let _ = combineLatest(
|
let _ = combineLatest(
|
||||||
queue: Queue.mainQueue(),
|
queue: Queue.mainQueue(),
|
||||||
@ -1714,7 +2129,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
actionsDisposable.dispose()
|
actionsDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = ItemListController(context: context, state: signal)
|
let controller = ChatListFilterPresetController(context: context, state: signal)
|
||||||
controller.navigationPresentation = .modal
|
controller.navigationPresentation = .modal
|
||||||
presentControllerImpl = { [weak controller] c, d in
|
presentControllerImpl = { [weak controller] c, d in
|
||||||
controller?.present(c, in: .window(.root), with: d)
|
controller?.present(c, in: .window(.root), with: d)
|
||||||
@ -1741,6 +2156,21 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
}
|
}
|
||||||
controller.view.endEditing(true)
|
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
|
controller.attemptNavigation = { _ in
|
||||||
if let attemptNavigationImpl {
|
if let attemptNavigationImpl {
|
||||||
attemptNavigationImpl({ value in
|
attemptNavigationImpl({ value in
|
||||||
@ -1812,7 +2242,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
return controller
|
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 {
|
if peerIds.isEmpty {
|
||||||
completed()
|
completed()
|
||||||
return
|
return
|
||||||
|
@ -289,7 +289,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
|
|||||||
}
|
}
|
||||||
if case let .filter(_, title, _, _) = filter {
|
if case let .filter(_, title, _, _) = filter {
|
||||||
folderCount += 1
|
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 {
|
if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount {
|
||||||
entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection))
|
entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection))
|
||||||
for filter in filteredSuggestedFilters {
|
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 {
|
if filters.isEmpty {
|
||||||
entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder))
|
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
|
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||||
var filters = filters
|
var filters = filters
|
||||||
let id = context.engine.peers.generateNewChatListFilterId(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
|
return filters
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
|
@ -4,6 +4,8 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import TextNodeWithEntities
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
|
private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
|
||||||
private let pressed: () -> Void
|
private let pressed: () -> Void
|
||||||
@ -55,6 +57,7 @@ private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class ItemNode: ASDisplayNode {
|
private final class ItemNode: ASDisplayNode {
|
||||||
|
private let context: AccountContext
|
||||||
private let pressed: (Bool) -> Void
|
private let pressed: (Bool) -> Void
|
||||||
private let requestedDeletion: () -> Void
|
private let requestedDeletion: () -> Void
|
||||||
|
|
||||||
@ -63,11 +66,11 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
private let extractedBackgroundNode: ASImageNode
|
private let extractedBackgroundNode: ASImageNode
|
||||||
private let titleContainer: ASDisplayNode
|
private let titleContainer: ASDisplayNode
|
||||||
private let titleNode: ImmediateTextNode
|
private let titleNode: ImmediateTextNodeWithEntities
|
||||||
private let titleActiveNode: ImmediateTextNode
|
private let titleActiveNode: ImmediateTextNodeWithEntities
|
||||||
private let shortTitleContainer: ASDisplayNode
|
private let shortTitleContainer: ASDisplayNode
|
||||||
private let shortTitleNode: ImmediateTextNode
|
private let shortTitleNode: ImmediateTextNodeWithEntities
|
||||||
private let shortTitleActiveNode: ImmediateTextNode
|
private let shortTitleActiveNode: ImmediateTextNodeWithEntities
|
||||||
private let badgeContainerNode: ASDisplayNode
|
private let badgeContainerNode: ASDisplayNode
|
||||||
private let badgeTextNode: ImmediateTextNode
|
private let badgeTextNode: ImmediateTextNode
|
||||||
private let badgeBackgroundActiveNode: ASImageNode
|
private let badgeBackgroundActiveNode: ASImageNode
|
||||||
@ -84,11 +87,12 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
private var isDisabled: Bool = false
|
private var isDisabled: Bool = false
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
private var currentTitle: (String, String)?
|
private var currentTitle: (ChatFolderTitle, ChatFolderTitle)?
|
||||||
|
|
||||||
private var pointerInteraction: PointerInteraction?
|
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.pressed = pressed
|
||||||
self.requestedDeletion = requestedDeletion
|
self.requestedDeletion = requestedDeletion
|
||||||
|
|
||||||
@ -102,23 +106,23 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.titleContainer = ASDisplayNode()
|
self.titleContainer = ASDisplayNode()
|
||||||
|
|
||||||
self.titleNode = ImmediateTextNode()
|
self.titleNode = ImmediateTextNodeWithEntities()
|
||||||
self.titleNode.displaysAsynchronously = false
|
self.titleNode.displaysAsynchronously = false
|
||||||
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
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.displaysAsynchronously = false
|
||||||
self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
||||||
self.titleActiveNode.alpha = 0.0
|
self.titleActiveNode.alpha = 0.0
|
||||||
|
|
||||||
self.shortTitleContainer = ASDisplayNode()
|
self.shortTitleContainer = ASDisplayNode()
|
||||||
|
|
||||||
self.shortTitleNode = ImmediateTextNode()
|
self.shortTitleNode = ImmediateTextNodeWithEntities()
|
||||||
self.shortTitleNode.displaysAsynchronously = false
|
self.shortTitleNode.displaysAsynchronously = false
|
||||||
self.shortTitleNode.alpha = 0.0
|
self.shortTitleNode.alpha = 0.0
|
||||||
self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 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.displaysAsynchronously = false
|
||||||
self.shortTitleActiveNode.alpha = 0.0
|
self.shortTitleActiveNode.alpha = 0.0
|
||||||
self.shortTitleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 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)
|
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.isEditing = isEditing
|
||||||
self.isDisabled = isDisabled
|
self.isDisabled = isDisabled
|
||||||
|
|
||||||
@ -221,7 +225,7 @@ private final class ItemNode: ASDisplayNode {
|
|||||||
self.unreadCount = unreadCount
|
self.unreadCount = unreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
self.buttonNode.accessibilityLabel = title
|
self.buttonNode.accessibilityLabel = title.text
|
||||||
if unreadCount > 0 {
|
if unreadCount > 0 {
|
||||||
if self.buttonNode.accessibilityValue == nil || unreadCountUpdated {
|
if self.buttonNode.accessibilityValue == nil || unreadCountUpdated {
|
||||||
self.buttonNode.accessibilityValue = strings.VoiceOver_Chat_UnreadMessages(Int32(unreadCount))
|
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.shortTitleNode, alpha: deselectionAlpha)
|
||||||
transition.updateAlpha(node: self.shortTitleActiveNode, alpha: selectionAlpha)
|
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 {
|
if themeUpdated || titleUpdated {
|
||||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
//TODO:release
|
||||||
self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor)
|
self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||||
self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, 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.shortTitleActiveNode.attributedText = NSAttributedString(string: shortTitle, 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 {
|
if unreadCount != 0 {
|
||||||
@ -467,7 +491,7 @@ public struct ChatListFilterTabEntryUnreadCount: Equatable {
|
|||||||
|
|
||||||
public enum ChatListFilterTabEntry: Equatable {
|
public enum ChatListFilterTabEntry: Equatable {
|
||||||
case all(unreadCount: Int)
|
case all(unreadCount: Int)
|
||||||
case filter(id: Int32, text: String, unread: ChatListFilterTabEntryUnreadCount)
|
case filter(id: Int32, text: ChatFolderTitle, unread: ChatListFilterTabEntryUnreadCount)
|
||||||
|
|
||||||
public var id: ChatListFilterTabEntryId {
|
public var id: ChatListFilterTabEntryId {
|
||||||
switch self {
|
switch self {
|
||||||
@ -478,19 +502,19 @@ public enum ChatListFilterTabEntry: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func title(strings: PresentationStrings) -> String {
|
func title(strings: PresentationStrings) -> ChatFolderTitle {
|
||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
return strings.ChatList_Tabs_AllChats
|
return ChatFolderTitle(text: strings.ChatList_Tabs_AllChats, entities: [], enableAnimations: true)
|
||||||
case let .filter(_, text, _):
|
case let .filter(_, text, _):
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shortTitle(strings: PresentationStrings) -> String {
|
func shortTitle(strings: PresentationStrings) -> ChatFolderTitle {
|
||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
return strings.ChatList_Tabs_All
|
return ChatFolderTitle(text: strings.ChatList_Tabs_All, entities: [], enableAnimations: true)
|
||||||
case let .filter(_, text, _):
|
case let .filter(_, text, _):
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
@ -498,6 +522,7 @@ public enum ChatListFilterTabEntry: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||||
|
private let context: AccountContext
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
private let selectedLineNode: ASImageNode
|
private let selectedLineNode: ASImageNode
|
||||||
private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:]
|
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.scrollNode = ASScrollNode()
|
||||||
|
|
||||||
self.selectedLineNode = ASImageNode()
|
self.selectedLineNode = ASImageNode()
|
||||||
@ -778,7 +804,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
itemNodeTransition = .immediate
|
itemNodeTransition = .immediate
|
||||||
wasAdded = true
|
wasAdded = true
|
||||||
itemNode = ItemNode(pressed: { [weak self] disabled in
|
itemNode = ItemNode(context: self.context, pressed: { [weak self] disabled in
|
||||||
self?.tabSelected?(filter.id, disabled)
|
self?.tabSelected?(filter.id, disabled)
|
||||||
}, requestedDeletion: { [weak self] in
|
}, requestedDeletion: { [weak self] in
|
||||||
self?.tabRequestedDeletion?(filter.id)
|
self?.tabRequestedDeletion?(filter.id)
|
||||||
@ -831,7 +857,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
|||||||
selectionFraction = 0.0
|
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] = []
|
var removeKeys: [ChatListFilterTabEntryId] = []
|
||||||
for (id, _) in self.itemNodes {
|
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 {
|
if !result.isEmpty {
|
||||||
result.append(", ")
|
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 {
|
if data.color != nil {
|
||||||
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
|
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) {
|
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(
|
result.append(ChatListItemContent.Tag(
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title.text,
|
||||||
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
|
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -650,6 +650,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
), animated: !transition.animation.isImmediate)
|
), animated: !transition.animation.isImmediate)
|
||||||
} else {
|
} else {
|
||||||
actionsStackNode = ContextControllerActionsStackNode(
|
actionsStackNode = ContextControllerActionsStackNode(
|
||||||
|
context: component.context,
|
||||||
getController: {
|
getController: {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -655,7 +655,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: self.resetPollText.flatMap { resetText in
|
resetText: self.resetPollText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: resetText)
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag,
|
||||||
characterLimit: component.initialData.maxPollTextLength,
|
characterLimit: component.initialData.maxPollTextLength,
|
||||||
@ -750,7 +750,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: pollOption.resetText.flatMap { resetText in
|
resetText: pollOption.resetText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: resetText)
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
|
||||||
characterLimit: component.initialData.maxPollOptionLength,
|
characterLimit: component.initialData.maxPollOptionLength,
|
||||||
@ -1139,7 +1139,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: self.resetQuizAnswerText.flatMap { resetText in
|
resetText: self.resetQuizAnswerText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: resetText)
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag,
|
||||||
characterLimit: component.initialData.maxPollTextLength,
|
characterLimit: component.initialData.maxPollTextLength,
|
||||||
|
@ -16,9 +16,9 @@ import SwiftSignalKit
|
|||||||
|
|
||||||
public final class ListComposePollOptionComponent: Component {
|
public final class ListComposePollOptionComponent: Component {
|
||||||
public final class ResetText: Equatable {
|
public final class ResetText: Equatable {
|
||||||
public let value: String
|
public let value: NSAttributedString
|
||||||
|
|
||||||
public init(value: String) {
|
public init(value: NSAttributedString) {
|
||||||
self.value = value
|
self.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +72,11 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let theme: PresentationTheme
|
public let theme: PresentationTheme
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
|
public let placeholder: NSAttributedString?
|
||||||
public let resetText: ResetText?
|
public let resetText: ResetText?
|
||||||
public let assumeIsEditing: Bool
|
public let assumeIsEditing: Bool
|
||||||
public let characterLimit: Int?
|
public let characterLimit: Int?
|
||||||
|
public let enableInlineAnimations: Bool
|
||||||
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
|
public let emptyLineHandling: TextFieldComponent.EmptyLineHandling
|
||||||
public let returnKeyAction: (() -> Void)?
|
public let returnKeyAction: (() -> Void)?
|
||||||
public let backspaceKeyAction: (() -> Void)?
|
public let backspaceKeyAction: (() -> Void)?
|
||||||
@ -88,9 +90,11 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
|
placeholder: NSAttributedString? = nil,
|
||||||
resetText: ResetText? = nil,
|
resetText: ResetText? = nil,
|
||||||
assumeIsEditing: Bool = false,
|
assumeIsEditing: Bool = false,
|
||||||
characterLimit: Int,
|
characterLimit: Int,
|
||||||
|
enableInlineAnimations: Bool = true,
|
||||||
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
|
emptyLineHandling: TextFieldComponent.EmptyLineHandling,
|
||||||
returnKeyAction: (() -> Void)?,
|
returnKeyAction: (() -> Void)?,
|
||||||
backspaceKeyAction: (() -> Void)?,
|
backspaceKeyAction: (() -> Void)?,
|
||||||
@ -103,9 +107,11 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.placeholder = placeholder
|
||||||
self.resetText = resetText
|
self.resetText = resetText
|
||||||
self.assumeIsEditing = assumeIsEditing
|
self.assumeIsEditing = assumeIsEditing
|
||||||
self.characterLimit = characterLimit
|
self.characterLimit = characterLimit
|
||||||
|
self.enableInlineAnimations = enableInlineAnimations
|
||||||
self.emptyLineHandling = emptyLineHandling
|
self.emptyLineHandling = emptyLineHandling
|
||||||
self.returnKeyAction = returnKeyAction
|
self.returnKeyAction = returnKeyAction
|
||||||
self.backspaceKeyAction = backspaceKeyAction
|
self.backspaceKeyAction = backspaceKeyAction
|
||||||
@ -128,6 +134,9 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
if lhs.strings !== rhs.strings {
|
if lhs.strings !== rhs.strings {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.placeholder != rhs.placeholder {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.resetText != rhs.resetText {
|
if lhs.resetText != rhs.resetText {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -137,6 +146,9 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
if lhs.characterLimit != rhs.characterLimit {
|
if lhs.characterLimit != rhs.characterLimit {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||||
return false
|
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? {
|
public var textFieldView: TextFieldComponent.View? {
|
||||||
return self.textField.view as? 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),
|
insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0),
|
||||||
hideKeyboard: component.inputMode == .emoji,
|
hideKeyboard: component.inputMode == .emoji,
|
||||||
customInputView: nil,
|
customInputView: nil,
|
||||||
|
placeholder: component.placeholder,
|
||||||
resetText: component.resetText.flatMap { resetText in
|
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,
|
isOneLineWhenUnfocused: false,
|
||||||
characterLimit: component.characterLimit,
|
characterLimit: component.characterLimit,
|
||||||
|
enableInlineAnimations: component.enableInlineAnimations,
|
||||||
emptyLineHandling: component.emptyLineHandling,
|
emptyLineHandling: component.emptyLineHandling,
|
||||||
formatMenuAvailability: .none,
|
formatMenuAvailability: .none,
|
||||||
returnKeyType: .next,
|
returnKeyType: .next,
|
||||||
|
@ -125,6 +125,8 @@ public final class ContextMenuActionItem {
|
|||||||
|
|
||||||
public let id: AnyHashable?
|
public let id: AnyHashable?
|
||||||
public let text: String
|
public let text: String
|
||||||
|
public let entities: [MessageTextEntity]
|
||||||
|
public let enableEntityAnimations: Bool
|
||||||
public let textColor: ContextMenuActionItemTextColor
|
public let textColor: ContextMenuActionItemTextColor
|
||||||
public let textFont: ContextMenuActionItemFont
|
public let textFont: ContextMenuActionItemFont
|
||||||
public let textLayout: ContextMenuActionItemTextLayout
|
public let textLayout: ContextMenuActionItemTextLayout
|
||||||
@ -143,6 +145,8 @@ public final class ContextMenuActionItem {
|
|||||||
convenience public init(
|
convenience public init(
|
||||||
id: AnyHashable? = nil,
|
id: AnyHashable? = nil,
|
||||||
text: String,
|
text: String,
|
||||||
|
entities: [MessageTextEntity] = [],
|
||||||
|
enableEntityAnimations: Bool = true,
|
||||||
textColor: ContextMenuActionItemTextColor = .primary,
|
textColor: ContextMenuActionItemTextColor = .primary,
|
||||||
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
||||||
textFont: ContextMenuActionItemFont = .regular,
|
textFont: ContextMenuActionItemFont = .regular,
|
||||||
@ -161,6 +165,8 @@ public final class ContextMenuActionItem {
|
|||||||
self.init(
|
self.init(
|
||||||
id: id,
|
id: id,
|
||||||
text: text,
|
text: text,
|
||||||
|
entities: entities,
|
||||||
|
enableEntityAnimations: enableEntityAnimations,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
textLayout: textLayout,
|
textLayout: textLayout,
|
||||||
textFont: textFont,
|
textFont: textFont,
|
||||||
@ -185,6 +191,8 @@ public final class ContextMenuActionItem {
|
|||||||
public init(
|
public init(
|
||||||
id: AnyHashable? = nil,
|
id: AnyHashable? = nil,
|
||||||
text: String,
|
text: String,
|
||||||
|
entities: [MessageTextEntity] = [],
|
||||||
|
enableEntityAnimations: Bool = true,
|
||||||
textColor: ContextMenuActionItemTextColor = .primary,
|
textColor: ContextMenuActionItemTextColor = .primary,
|
||||||
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
|
||||||
textFont: ContextMenuActionItemFont = .regular,
|
textFont: ContextMenuActionItemFont = .regular,
|
||||||
@ -202,6 +210,8 @@ public final class ContextMenuActionItem {
|
|||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.entities = entities
|
||||||
|
self.enableEntityAnimations = enableEntityAnimations
|
||||||
self.textColor = textColor
|
self.textColor = textColor
|
||||||
self.textFont = textFont
|
self.textFont = textFont
|
||||||
self.textLayout = textLayout
|
self.textLayout = textLayout
|
||||||
@ -258,6 +268,7 @@ func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) ->
|
|||||||
|
|
||||||
final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate {
|
final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate {
|
||||||
private weak var controller: ContextController?
|
private weak var controller: ContextController?
|
||||||
|
private let context: AccountContext?
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
|
|
||||||
private let configuration: ContextController.Configuration
|
private let configuration: ContextController.Configuration
|
||||||
@ -324,6 +335,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
controller: ContextController,
|
controller: ContextController,
|
||||||
|
context: AccountContext?,
|
||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
configuration: ContextController.Configuration,
|
configuration: ContextController.Configuration,
|
||||||
beginDismiss: @escaping (ContextMenuActionResult) -> Void,
|
beginDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
@ -333,6 +345,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
|
|||||||
attemptTransitionControllerIntoNavigation: @escaping () -> Void
|
attemptTransitionControllerIntoNavigation: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.beginDismiss = beginDismiss
|
self.beginDismiss = beginDismiss
|
||||||
@ -704,7 +717,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let controller = self.controller {
|
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.contentReady.set(sourceContainer.ready.get())
|
||||||
self.itemsReady.set(.single(true))
|
self.itemsReady.set(.single(true))
|
||||||
self.sourceContainer = sourceContainer
|
self.sourceContainer = sourceContainer
|
||||||
@ -2437,6 +2450,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let context: AccountContext?
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private let configuration: ContextController.Configuration
|
private let configuration: ContextController.Configuration
|
||||||
|
|
||||||
@ -2491,8 +2505,9 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
|
|
||||||
public var getOverlayViews: (() -> [UIView])?
|
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(
|
self.init(
|
||||||
|
context: context,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
configuration: ContextController.Configuration(
|
configuration: ContextController.Configuration(
|
||||||
sources: [ContextController.Source(
|
sources: [ContextController.Source(
|
||||||
@ -2511,6 +2526,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
}
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
context: AccountContext? = nil,
|
||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
configuration: ContextController.Configuration,
|
configuration: ContextController.Configuration,
|
||||||
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
|
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
|
||||||
@ -2518,6 +2534,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
workaroundUseLegacyImplementation: Bool = false,
|
workaroundUseLegacyImplementation: Bool = false,
|
||||||
disableScreenshots: Bool = false
|
disableScreenshots: Bool = false
|
||||||
) {
|
) {
|
||||||
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
self.recognizer = recognizer
|
self.recognizer = recognizer
|
||||||
@ -2586,7 +2603,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
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)
|
self?.dismiss(result: result, completion: nil)
|
||||||
}, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in
|
}, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
@ -15,6 +15,7 @@ import MultiAnimationRenderer
|
|||||||
import AnimationUI
|
import AnimationUI
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import LottieComponent
|
import LottieComponent
|
||||||
|
import TextNodeWithEntities
|
||||||
|
|
||||||
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
|
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
|
||||||
var wantsFullWidth: Bool { get }
|
var wantsFullWidth: Bool { get }
|
||||||
@ -71,6 +72,7 @@ public final class ContextControllerPreviewReaction {
|
|||||||
|
|
||||||
public protocol ContextControllerActionsStackItem: AnyObject {
|
public protocol ContextControllerActionsStackItem: AnyObject {
|
||||||
func node(
|
func node(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
@ -94,13 +96,14 @@ public protocol ContextControllerActionsListItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode {
|
public final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode {
|
||||||
|
private let context: AccountContext?
|
||||||
private let getController: () -> ContextControllerProtocol?
|
private let getController: () -> ContextControllerProtocol?
|
||||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||||
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
||||||
private var item: ContextMenuActionItem
|
private var item: ContextMenuActionItem
|
||||||
|
|
||||||
private let highlightBackgroundNode: ASDisplayNode
|
private let highlightBackgroundNode: ASDisplayNode
|
||||||
private let titleLabelNode: ImmediateTextNode
|
private let titleLabelNode: ImmediateTextNodeWithEntities
|
||||||
private let subtitleNode: ImmediateTextNode
|
private let subtitleNode: ImmediateTextNode
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let additionalIconNode: ASImageNode
|
private let additionalIconNode: ASImageNode
|
||||||
@ -115,11 +118,13 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||||||
private var iconDisposable: Disposable?
|
private var iconDisposable: Disposable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void,
|
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void,
|
||||||
item: ContextMenuActionItem
|
item: ContextMenuActionItem
|
||||||
) {
|
) {
|
||||||
|
self.context = context
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
self.requestDismiss = requestDismiss
|
self.requestDismiss = requestDismiss
|
||||||
self.requestUpdateAction = requestUpdateAction
|
self.requestUpdateAction = requestUpdateAction
|
||||||
@ -130,7 +135,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||||||
self.highlightBackgroundNode.isUserInteractionEnabled = false
|
self.highlightBackgroundNode.isUserInteractionEnabled = false
|
||||||
self.highlightBackgroundNode.alpha = 0.0
|
self.highlightBackgroundNode.alpha = 0.0
|
||||||
|
|
||||||
self.titleLabelNode = ImmediateTextNode()
|
self.titleLabelNode = ImmediateTextNodeWithEntities()
|
||||||
self.titleLabelNode.isAccessibilityElement = false
|
self.titleLabelNode.isAccessibilityElement = false
|
||||||
self.titleLabelNode.displaysAsynchronously = 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 subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
|
||||||
let subtitleColor = presentationData.theme.contextMenu.secondaryColor
|
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?
|
var subtitle: NSAttributedString?
|
||||||
switch self.item.textLayout {
|
switch self.item.textLayout {
|
||||||
case .singleLine:
|
case .singleLine:
|
||||||
@ -296,16 +312,32 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||||||
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.item.parseMarkdown {
|
if self.item.parseMarkdown || !self.item.entities.isEmpty {
|
||||||
let attributedText = parseMarkdownIntoAttributedString(
|
let attributedText: NSAttributedString
|
||||||
self.item.text,
|
if !self.item.entities.isEmpty {
|
||||||
attributes: MarkdownAttributes(
|
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
|
||||||
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
|
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||||
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
|
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range)
|
||||||
link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor),
|
}
|
||||||
linkAttribute: { value in return ("URL", value) }
|
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.attributedText = attributedText
|
||||||
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
|
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
|
||||||
self.titleLabelNode.highlightAttributeAction = { attributes in
|
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 requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||||
private let getController: () -> ContextControllerProtocol?
|
private let getController: () -> ContextControllerProtocol?
|
||||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||||
@ -708,11 +741,13 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
items: [ContextMenuItem]
|
items: [ContextMenuItem]
|
||||||
) {
|
) {
|
||||||
|
self.context = context
|
||||||
self.requestUpdate = requestUpdate
|
self.requestUpdate = requestUpdate
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
self.requestDismiss = requestDismiss
|
self.requestDismiss = requestDismiss
|
||||||
@ -724,6 +759,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
|
|||||||
case let .action(actionItem):
|
case let .action(actionItem):
|
||||||
return Item(
|
return Item(
|
||||||
node: ContextControllerActionsListActionItemNode(
|
node: ContextControllerActionsListActionItemNode(
|
||||||
|
context: context,
|
||||||
getController: getController,
|
getController: getController,
|
||||||
requestDismiss: requestDismiss,
|
requestDismiss: requestDismiss,
|
||||||
requestUpdateAction: { id, action in
|
requestUpdateAction: { id, action in
|
||||||
@ -794,6 +830,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
|
|||||||
|
|
||||||
let addedNode = Item(
|
let addedNode = Item(
|
||||||
node: ContextControllerActionsListActionItemNode(
|
node: ContextControllerActionsListActionItemNode(
|
||||||
|
context: self.context,
|
||||||
getController: self.getController,
|
getController: self.getController,
|
||||||
requestDismiss: self.requestDismiss,
|
requestDismiss: self.requestDismiss,
|
||||||
requestUpdateAction: { [weak self] id, action in
|
requestUpdateAction: { [weak self] id, action in
|
||||||
@ -981,12 +1018,14 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func node(
|
public func node(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||||
) -> ContextControllerActionsStackItemNode {
|
) -> ContextControllerActionsStackItemNode {
|
||||||
return Node(
|
return Node(
|
||||||
|
context: context,
|
||||||
getController: getController,
|
getController: getController,
|
||||||
requestDismiss: requestDismiss,
|
requestDismiss: requestDismiss,
|
||||||
requestUpdate: requestUpdate,
|
requestUpdate: requestUpdate,
|
||||||
@ -1082,6 +1121,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
func node(
|
func node(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
@ -1246,6 +1286,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
private var tipDisposable: Disposable?
|
private var tipDisposable: Disposable?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
@ -1262,6 +1303,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
self.requestUpdate = requestUpdate
|
self.requestUpdate = requestUpdate
|
||||||
self.item = item
|
self.item = item
|
||||||
self.node = item.node(
|
self.node = item.node(
|
||||||
|
context: context,
|
||||||
getController: getController,
|
getController: getController,
|
||||||
requestDismiss: requestDismiss,
|
requestDismiss: requestDismiss,
|
||||||
requestUpdate: requestUpdate,
|
requestUpdate: requestUpdate,
|
||||||
@ -1396,6 +1438,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let context: AccountContext?
|
||||||
private let getController: () -> ContextControllerProtocol?
|
private let getController: () -> ContextControllerProtocol?
|
||||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||||
@ -1423,10 +1466,12 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
|
||||||
) {
|
) {
|
||||||
|
self.context = context
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
self.requestDismiss = requestDismiss
|
self.requestDismiss = requestDismiss
|
||||||
self.requestUpdate = requestUpdate
|
self.requestUpdate = requestUpdate
|
||||||
@ -1534,6 +1579,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
itemContainer.storedScrollingState = currentScrollingState
|
itemContainer.storedScrollingState = currentScrollingState
|
||||||
}
|
}
|
||||||
let itemContainer = ItemContainer(
|
let itemContainer = ItemContainer(
|
||||||
|
context: self.context,
|
||||||
getController: self.getController,
|
getController: self.getController,
|
||||||
requestDismiss: self.requestDismiss,
|
requestDismiss: self.requestDismiss,
|
||||||
requestUpdate: self.requestUpdate,
|
requestUpdate: self.requestUpdate,
|
||||||
|
@ -8,6 +8,7 @@ import TelegramCore
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import ReactionSelectionNode
|
import ReactionSelectionNode
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
private extension ContextControllerTakeViewInfo.ContainingItem {
|
private extension ContextControllerTakeViewInfo.ContainingItem {
|
||||||
var contentRect: CGRect {
|
var contentRect: CGRect {
|
||||||
@ -227,6 +228,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
return self._ready.get()
|
return self._ready.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let context: AccountContext?
|
||||||
private let getController: () -> ContextControllerProtocol?
|
private let getController: () -> ContextControllerProtocol?
|
||||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||||
private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void
|
private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void
|
||||||
@ -268,6 +270,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
private weak var currentUndoController: ViewController?
|
private weak var currentUndoController: ViewController?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
context: AccountContext?,
|
||||||
getController: @escaping () -> ContextControllerProtocol?,
|
getController: @escaping () -> ContextControllerProtocol?,
|
||||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void,
|
requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||||
@ -275,6 +278,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
|
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
|
||||||
source: ContentSource
|
source: ContentSource
|
||||||
) {
|
) {
|
||||||
|
self.context = context
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
self.requestUpdate = requestUpdate
|
self.requestUpdate = requestUpdate
|
||||||
self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard
|
self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard
|
||||||
@ -308,6 +312,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
|
|
||||||
self.actionsContainerNode = ASDisplayNode()
|
self.actionsContainerNode = ASDisplayNode()
|
||||||
self.actionsStackNode = ContextControllerActionsStackNode(
|
self.actionsStackNode = ContextControllerActionsStackNode(
|
||||||
|
context: self.context,
|
||||||
getController: getController,
|
getController: getController,
|
||||||
requestDismiss: { result in
|
requestDismiss: { result in
|
||||||
requestDismiss(result)
|
requestDismiss(result)
|
||||||
@ -316,6 +321,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.additionalActionsStackNode = ContextControllerActionsStackNode(
|
self.additionalActionsStackNode = ContextControllerActionsStackNode(
|
||||||
|
context: self.context,
|
||||||
getController: getController,
|
getController: getController,
|
||||||
requestDismiss: { result in
|
requestDismiss: { result in
|
||||||
requestDismiss(result)
|
requestDismiss(result)
|
||||||
|
@ -9,6 +9,7 @@ import ComponentFlow
|
|||||||
import TabSelectorComponent
|
import TabSelectorComponent
|
||||||
import PlainButtonComponent
|
import PlainButtonComponent
|
||||||
import ComponentDisplayAdapters
|
import ComponentDisplayAdapters
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
final class ContextSourceContainer: ASDisplayNode {
|
final class ContextSourceContainer: ASDisplayNode {
|
||||||
final class Source {
|
final class Source {
|
||||||
@ -16,6 +17,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
|
|
||||||
let id: AnyHashable
|
let id: AnyHashable
|
||||||
let title: String
|
let title: String
|
||||||
|
let context: AccountContext?
|
||||||
let source: ContextContentSource
|
let source: ContextContentSource
|
||||||
let closeActionTitle: String?
|
let closeActionTitle: String?
|
||||||
let closeAction: (() -> Void)?
|
let closeAction: (() -> Void)?
|
||||||
@ -42,6 +44,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
controller: ContextController,
|
controller: ContextController,
|
||||||
id: AnyHashable,
|
id: AnyHashable,
|
||||||
title: String,
|
title: String,
|
||||||
|
context: AccountContext?,
|
||||||
source: ContextContentSource,
|
source: ContextContentSource,
|
||||||
items: Signal<ContextController.Items, NoError>,
|
items: Signal<ContextController.Items, NoError>,
|
||||||
closeActionTitle: String? = nil,
|
closeActionTitle: String? = nil,
|
||||||
@ -50,6 +53,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.context = context
|
||||||
self.source = source
|
self.source = source
|
||||||
self.closeActionTitle = closeActionTitle
|
self.closeActionTitle = closeActionTitle
|
||||||
self.closeAction = closeAction
|
self.closeAction = closeAction
|
||||||
@ -65,6 +69,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
self.contentReady.set(.single(true))
|
self.contentReady.set(.single(true))
|
||||||
|
|
||||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||||
|
context: self.context,
|
||||||
getController: { [weak self] in
|
getController: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return nil
|
return nil
|
||||||
@ -105,6 +110,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
self.contentReady.set(.single(true))
|
self.contentReady.set(.single(true))
|
||||||
|
|
||||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||||
|
context: self.context,
|
||||||
getController: { [weak self] in
|
getController: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return nil
|
return nil
|
||||||
@ -145,6 +151,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
self.contentReady.set(.single(true))
|
self.contentReady.set(.single(true))
|
||||||
|
|
||||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||||
|
context: self.context,
|
||||||
getController: { [weak self] in
|
getController: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return nil
|
return nil
|
||||||
@ -188,6 +195,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
self.contentReady.set(source.controller.ready.get())
|
self.contentReady.set(source.controller.ready.get())
|
||||||
|
|
||||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||||
|
context: self.context,
|
||||||
getController: { [weak self] in
|
getController: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return nil
|
return nil
|
||||||
@ -373,7 +381,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
|
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(controller: ContextController, configuration: ContextController.Configuration) {
|
init(controller: ContextController, configuration: ContextController.Configuration, context: AccountContext?) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
|
||||||
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
||||||
@ -389,6 +397,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
id: source.id,
|
id: source.id,
|
||||||
title: source.title,
|
title: source.title,
|
||||||
|
context: context,
|
||||||
source: source.source,
|
source: source.source,
|
||||||
items: source.items,
|
items: source.items,
|
||||||
closeActionTitle: source.closeActionTitle,
|
closeActionTitle: source.closeActionTitle,
|
||||||
|
@ -80,6 +80,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
|||||||
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
self.actionsStackNode = ContextControllerActionsStackNode(
|
self.actionsStackNode = ContextControllerActionsStackNode(
|
||||||
|
context: nil,
|
||||||
getController: { [weak controller] in
|
getController: { [weak controller] in
|
||||||
return controller
|
return controller
|
||||||
},
|
},
|
||||||
|
@ -304,7 +304,7 @@ private struct FolderInviteLinkListControllerState: Equatable {
|
|||||||
var isSaving: Bool = false
|
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)?
|
var pushControllerImpl: ((ViewController) -> Void)?
|
||||||
let _ = pushControllerImpl
|
let _ = pushControllerImpl
|
||||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
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)
|
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(
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
state: state,
|
state: state,
|
||||||
title: filterTitle,
|
title: filterTitle.text,
|
||||||
allPeers: allPeers
|
allPeers: allPeers
|
||||||
), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges)
|
), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ public enum ItemListSectionHeaderActivityIndicator {
|
|||||||
case left
|
case left
|
||||||
case right
|
case right
|
||||||
|
|
||||||
fileprivate var hasActivity: Bool {
|
public var hasActivity: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .left, .right:
|
case .left, .right:
|
||||||
return true
|
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 {
|
public enum ChatListFilter: Codable, Equatable {
|
||||||
case allChats
|
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 {
|
public var id: Int32 {
|
||||||
switch self {
|
switch self {
|
||||||
@ -251,7 +279,14 @@ public enum ChatListFilter: Codable, Equatable {
|
|||||||
self = .allChats
|
self = .allChats
|
||||||
} else {
|
} else {
|
||||||
let id = try container.decode(Int32.self, forKey: "id")
|
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 emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon")
|
||||||
|
|
||||||
let data = ChatListFilterData(
|
let data = ChatListFilterData(
|
||||||
@ -284,7 +319,7 @@ public enum ChatListFilter: Codable, Equatable {
|
|||||||
try container.encode(type, forKey: "t")
|
try container.encode(type, forKey: "t")
|
||||||
|
|
||||||
try container.encode(id, forKey: "id")
|
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.encodeIfPresent(emoticon, forKey: "emoticon")
|
||||||
|
|
||||||
try container.encode(data.isShared, forKey: "isShared")
|
try container.encode(data.isShared, forKey: "isShared")
|
||||||
@ -309,7 +344,7 @@ extension ChatListFilter {
|
|||||||
case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers):
|
case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers):
|
||||||
self = .filter(
|
self = .filter(
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
|
||||||
emoticon: emoticon,
|
emoticon: emoticon,
|
||||||
data: ChatListFilterData(
|
data: ChatListFilterData(
|
||||||
isShared: false,
|
isShared: false,
|
||||||
@ -359,7 +394,7 @@ extension ChatListFilter {
|
|||||||
case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers):
|
case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers):
|
||||||
self = .filter(
|
self = .filter(
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
|
||||||
emoticon: emoticon,
|
emoticon: emoticon,
|
||||||
data: ChatListFilterData(
|
data: ChatListFilterData(
|
||||||
isShared: true,
|
isShared: true,
|
||||||
@ -400,9 +435,9 @@ extension ChatListFilter {
|
|||||||
|
|
||||||
func apiFilter(transaction: Transaction) -> Api.DialogFilter? {
|
func apiFilter(transaction: Transaction) -> Api.DialogFilter? {
|
||||||
switch self {
|
switch self {
|
||||||
case .allChats:
|
case .allChats:
|
||||||
return nil
|
return nil
|
||||||
case let .filter(id, title, emoticon, data):
|
case let .filter(id, title, emoticon, data):
|
||||||
if data.isShared {
|
if data.isShared {
|
||||||
var flags: Int32 = 0
|
var flags: Int32 = 0
|
||||||
if emoticon != nil {
|
if emoticon != nil {
|
||||||
@ -411,7 +446,7 @@ extension ChatListFilter {
|
|||||||
if data.color != nil {
|
if data.color != nil {
|
||||||
flags |= 1 << 27
|
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)
|
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||||
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
|
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
|
||||||
if data.includePeers.pinnedPeers.contains(peerId) {
|
if data.includePeers.pinnedPeers.contains(peerId) {
|
||||||
@ -437,7 +472,7 @@ extension ChatListFilter {
|
|||||||
if data.color != nil {
|
if data.color != nil {
|
||||||
flags |= 1 << 27
|
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)
|
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||||
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
|
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
|
||||||
if data.includePeers.pinnedPeers.contains(peerId) {
|
if data.includePeers.pinnedPeers.contains(peerId) {
|
||||||
@ -1099,12 +1134,12 @@ func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersS
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatListFeaturedFilter: Codable, Equatable {
|
public struct ChatListFeaturedFilter: Codable, Equatable {
|
||||||
public var title: String
|
public var title: ChatFolderTitle
|
||||||
public var description: String
|
public var description: String
|
||||||
public var data: ChatListFilterData
|
public var data: ChatListFilterData
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
title: String,
|
title: ChatFolderTitle,
|
||||||
description: String,
|
description: String,
|
||||||
data: ChatListFilterData
|
data: ChatListFilterData
|
||||||
) {
|
) {
|
||||||
@ -1116,7 +1151,11 @@ public struct ChatListFeaturedFilter: Codable, Equatable {
|
|||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
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.description = try container.decode(String.self, forKey: "description")
|
||||||
self.data = ChatListFilterData(
|
self.data = ChatListFilterData(
|
||||||
isShared: false,
|
isShared: false,
|
||||||
@ -1137,7 +1176,7 @@ public struct ChatListFeaturedFilter: Codable, Equatable {
|
|||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
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.description, forKey: "description")
|
||||||
try container.encode(self.data.categories.rawValue, forKey: "categories")
|
try container.encode(self.data.categories.rawValue, forKey: "categories")
|
||||||
try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted")
|
try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted")
|
||||||
|
@ -244,14 +244,14 @@ public enum CheckChatFolderLinkError {
|
|||||||
|
|
||||||
public final class ChatFolderLinkContents {
|
public final class ChatFolderLinkContents {
|
||||||
public let localFilterId: Int32?
|
public let localFilterId: Int32?
|
||||||
public let title: String?
|
public let title: ChatFolderTitle?
|
||||||
public let peers: [EnginePeer]
|
public let peers: [EnginePeer]
|
||||||
public let alreadyMemberPeerIds: Set<EnginePeer.Id>
|
public let alreadyMemberPeerIds: Set<EnginePeer.Id>
|
||||||
public let memberCounts: [EnginePeer.Id: Int]
|
public let memberCounts: [EnginePeer.Id: Int]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
localFilterId: Int32?,
|
localFilterId: Int32?,
|
||||||
title: String?,
|
title: ChatFolderTitle?,
|
||||||
peers: [EnginePeer],
|
peers: [EnginePeer],
|
||||||
alreadyMemberPeerIds: Set<EnginePeer.Id>,
|
alreadyMemberPeerIds: Set<EnginePeer.Id>,
|
||||||
memberCounts: [EnginePeer.Id: Int]
|
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):
|
case let .chatlistInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
|
||||||
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
|
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
|
||||||
var memberCounts: [PeerId: Int] = [:]
|
var memberCounts: [PeerId: Int] = [:]
|
||||||
@ -317,7 +317,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|
|||||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
||||||
|
|
||||||
let currentFilters = _internal_currentChatListFilters(transaction: transaction)
|
let currentFilters = _internal_currentChatListFilters(transaction: transaction)
|
||||||
var currentFilterTitle: String?
|
var currentFilterTitle: ChatFolderTitle?
|
||||||
if let index = currentFilters.firstIndex(where: { $0.id == filterId }) {
|
if let index = currentFilters.firstIndex(where: { $0.id == filterId }) {
|
||||||
switch currentFilters[index] {
|
switch currentFilters[index] {
|
||||||
case let .filter(_, title, _, _):
|
case let .filter(_, title, _, _):
|
||||||
@ -367,10 +367,10 @@ public enum JoinChatFolderLinkError {
|
|||||||
|
|
||||||
public final class JoinChatFolderResult {
|
public final class JoinChatFolderResult {
|
||||||
public let folderId: Int32
|
public let folderId: Int32
|
||||||
public let title: String
|
public let title: ChatFolderTitle
|
||||||
public let newChatCount: Int
|
public let newChatCount: Int
|
||||||
|
|
||||||
public init(folderId: Int32, title: String, newChatCount: Int) {
|
public init(folderId: Int32, title: ChatFolderTitle, newChatCount: Int) {
|
||||||
self.folderId = folderId
|
self.folderId = folderId
|
||||||
self.title = title
|
self.title = title
|
||||||
self.newChatCount = newChatCount
|
self.newChatCount = newChatCount
|
||||||
@ -378,25 +378,6 @@ public final class JoinChatFolderResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
|
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
|
return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in
|
||||||
var newChatCount = 0
|
var newChatCount = 0
|
||||||
for peerId in peerIds {
|
for peerId in peerIds {
|
||||||
@ -522,7 +503,7 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
|
|||||||
|
|
||||||
public final class ChatFolderUpdates: Equatable {
|
public final class ChatFolderUpdates: Equatable {
|
||||||
public let folderId: Int32
|
public let folderId: Int32
|
||||||
fileprivate let title: String
|
fileprivate let title: ChatFolderTitle
|
||||||
fileprivate let missingPeers: [EnginePeer]
|
fileprivate let missingPeers: [EnginePeer]
|
||||||
fileprivate let memberCounts: [EnginePeer.Id: Int]
|
fileprivate let memberCounts: [EnginePeer.Id: Int]
|
||||||
|
|
||||||
@ -536,7 +517,7 @@ public final class ChatFolderUpdates: Equatable {
|
|||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
folderId: Int32,
|
folderId: Int32,
|
||||||
title: String,
|
title: ChatFolderTitle,
|
||||||
missingPeers: [EnginePeer],
|
missingPeers: [EnginePeer],
|
||||||
memberCounts: [EnginePeer.Id: Int]
|
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> {
|
func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
|
||||||
struct InternalData: Equatable {
|
struct InternalData: Equatable {
|
||||||
var title: String
|
var title: ChatFolderTitle
|
||||||
var peerIds: [EnginePeer.Id]
|
var peerIds: [EnginePeer.Id]
|
||||||
var memberCounts: [EnginePeer.Id: Int]
|
var memberCounts: [EnginePeer.Id: Int]
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ public extension TelegramEngine {
|
|||||||
case same
|
case same
|
||||||
case archived
|
case archived
|
||||||
case unarchived
|
case unarchived
|
||||||
case folder(id: Int32, title: String)
|
case folder(id: Int32, title: ChatFolderTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Peers {
|
final class Peers {
|
||||||
|
@ -6,6 +6,7 @@ import ComponentFlow
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
final class BadgeComponent: Component {
|
final class BadgeComponent: Component {
|
||||||
let fillColor: UIColor
|
let fillColor: UIColor
|
||||||
@ -85,13 +86,13 @@ final class BadgeComponent: Component {
|
|||||||
final class ChatFolderLinkHeaderComponent: Component {
|
final class ChatFolderLinkHeaderComponent: Component {
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
let title: String
|
let title: ChatFolderTitle
|
||||||
let badge: String?
|
let badge: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
title: String,
|
title: ChatFolderTitle,
|
||||||
badge: String?
|
badge: String?
|
||||||
) {
|
) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -212,9 +213,10 @@ final class ChatFolderLinkHeaderComponent: Component {
|
|||||||
}
|
}
|
||||||
contentWidth += spacing
|
contentWidth += spacing
|
||||||
|
|
||||||
|
//TODO:release
|
||||||
let titleSize = self.title.update(
|
let titleSize = self.title.update(
|
||||||
transition: .immediate,
|
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: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||||
)
|
)
|
||||||
|
@ -439,7 +439,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
component: AnyComponent(ChatFolderLinkHeaderComponent(
|
component: AnyComponent(ChatFolderLinkHeaderComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
title: component.linkContents?.title ?? "Folder",
|
title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true),
|
||||||
badge: topBadge
|
badge: topBadge
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -469,7 +469,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
text = environment.strings.FolderLinkPreview_TextAddFolder
|
text = environment.strings.FolderLinkPreview_TextAddFolder
|
||||||
} else {
|
} else {
|
||||||
let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount))
|
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 {
|
} else {
|
||||||
text = " "
|
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.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems)))
|
||||||
disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId]))
|
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 })
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
|
|
||||||
@ -1110,7 +1112,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isUpdates {
|
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 {
|
} else if result.newChatCount != 0 {
|
||||||
let animationBackgroundColor: UIColor
|
let animationBackgroundColor: UIColor
|
||||||
if presentationData.theme.overallDarkAppearance {
|
if presentationData.theme.overallDarkAppearance {
|
||||||
@ -1118,7 +1121,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
animationBackgroundColor = UIColor(rgb: 0x474747)
|
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 {
|
} else {
|
||||||
let animationBackgroundColor: UIColor
|
let animationBackgroundColor: UIColor
|
||||||
if presentationData.theme.overallDarkAppearance {
|
if presentationData.theme.overallDarkAppearance {
|
||||||
@ -1126,7 +1130,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
animationBackgroundColor = UIColor(rgb: 0x474747)
|
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
|
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
|
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))
|
(navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root))
|
||||||
}))
|
}))
|
||||||
|
@ -231,20 +231,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||||
let cache: AnimationCache
|
let cache: AnimationCache
|
||||||
let renderer: MultiAnimationRenderer
|
let renderer: MultiAnimationRenderer
|
||||||
let unique: Bool
|
|
||||||
let placeholderColor: UIColor
|
let placeholderColor: UIColor
|
||||||
let loopCount: Int?
|
let loopCount: Int?
|
||||||
|
|
||||||
let pointSize: CGSize
|
let pointSize: CGSize
|
||||||
let pixelSize: 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.context = context
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
self.emoji = emoji
|
self.emoji = emoji
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.renderer = renderer
|
self.renderer = renderer
|
||||||
self.unique = unique
|
|
||||||
self.placeholderColor = placeholderColor
|
self.placeholderColor = placeholderColor
|
||||||
self.loopCount = loopCount
|
self.loopCount = loopCount
|
||||||
self.pointSize = pointSize
|
self.pointSize = pointSize
|
||||||
@ -373,6 +371,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
|
|
||||||
private var currentLoopCount: Int = 0
|
private var currentLoopCount: Int = 0
|
||||||
|
|
||||||
|
public var isUnique: Bool = false
|
||||||
|
|
||||||
private var isInHierarchyValue: Bool = false
|
private var isInHierarchyValue: Bool = false
|
||||||
public var isVisibleForAnimations: Bool = false {
|
public var isVisibleForAnimations: Bool = false {
|
||||||
didSet {
|
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) {
|
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)
|
let scale = min(2.0, UIScreenScale)
|
||||||
|
|
||||||
|
self.isUnique = unique
|
||||||
|
|
||||||
self.arguments = Arguments(
|
self.arguments = Arguments(
|
||||||
context: context,
|
context: context,
|
||||||
userLocation: userLocation,
|
userLocation: userLocation,
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
renderer: renderer,
|
renderer: renderer,
|
||||||
unique: unique,
|
|
||||||
placeholderColor: placeholderColor,
|
placeholderColor: placeholderColor,
|
||||||
loopCount: loopCount,
|
loopCount: loopCount,
|
||||||
pointSize: pointSize,
|
pointSize: pointSize,
|
||||||
@ -670,7 +671,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
|
|
||||||
if attemptSynchronousLoad {
|
if attemptSynchronousLoad {
|
||||||
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) {
|
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loadAnimation()
|
self.loadAnimation()
|
||||||
@ -694,19 +694,27 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let keyframeOnly = arguments.pixelSize.width >= 120.0
|
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) {
|
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
|
||||||
guard let arguments = self.arguments else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.file?.fileId == file.fileId {
|
if self.file?.fileId == file.fileId {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.file = file
|
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 attemptSynchronousLoad {
|
||||||
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) {
|
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() {
|
private func loadAnimation() {
|
||||||
guard let arguments = self.arguments else {
|
guard let arguments = self.arguments else {
|
||||||
return
|
return
|
||||||
@ -768,13 +780,15 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
|
|
||||||
let isTemplate = file.isCustomTemplateEmoji
|
let isTemplate = file.isCustomTemplateEmoji
|
||||||
|
|
||||||
|
self.disposable?.dispose()
|
||||||
|
|
||||||
let context = arguments.context
|
let context = arguments.context
|
||||||
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
|
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
|
||||||
let keyframeOnly = arguments.pixelSize.width >= 120.0
|
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 {
|
} 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
|
let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in
|
||||||
guard result.complete else {
|
guard result.complete else {
|
||||||
return
|
return
|
||||||
@ -846,6 +860,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||||||
public final class EmojiTextAttachmentView: UIView {
|
public final class EmojiTextAttachmentView: UIView {
|
||||||
public let contentLayer: InlineStickerItemLayer
|
public let contentLayer: InlineStickerItemLayer
|
||||||
|
|
||||||
|
public var isUnique: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if self.isActive != oldValue {
|
||||||
|
self.contentLayer.isUnique = self.isUnique
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
public var isActive: Bool = true {
|
public var isActive: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
if self.isActive != oldValue {
|
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) {
|
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, placeholderColor: placeholderColor, pointSize: pointSize)
|
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())
|
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))
|
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 {
|
public final class CustomEmojiContainerView: UIView {
|
||||||
|
@ -79,7 +79,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu
|
|||||||
f(.dismissWithoutContent)
|
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)
|
actionNodes.append(actionNode)
|
||||||
if actionNodes.count != item.packs.count {
|
if actionNodes.count != item.packs.count {
|
||||||
let separatorNode = ASDisplayNode()
|
let separatorNode = ASDisplayNode()
|
||||||
|
@ -190,7 +190,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
|
|||||||
if params.hasFilters {
|
if params.hasFilters {
|
||||||
self._ready.set(.never())
|
self._ready.set(.never())
|
||||||
|
|
||||||
self.tabContainerNode = ChatListFilterTabContainerNode()
|
self.tabContainerNode = ChatListFilterTabContainerNode(context: self.context)
|
||||||
self.reloadFilters()
|
self.reloadFilters()
|
||||||
|
|
||||||
self.peerSelectionNode.mainContainerNode?.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in
|
self.peerSelectionNode.mainContainerNode?.currentItemFilterUpdated = { [weak self] filter, fraction, transition, force in
|
||||||
|
@ -24,6 +24,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
|
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
|
||||||
"//submodules/TextInputMenu",
|
"//submodules/TextInputMenu",
|
||||||
"//submodules/ObjCRuntimeUtils",
|
"//submodules/ObjCRuntimeUtils",
|
||||||
|
"//submodules/Components/MultilineTextComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -16,6 +16,7 @@ import ImageTransparency
|
|||||||
import ChatInputTextNode
|
import ChatInputTextNode
|
||||||
import TextInputMenu
|
import TextInputMenu
|
||||||
import ObjCRuntimeUtils
|
import ObjCRuntimeUtils
|
||||||
|
import MultilineTextComponent
|
||||||
|
|
||||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||||
public var enableInputClicksWhenVisible: Bool {
|
public var enableInputClicksWhenVisible: Bool {
|
||||||
@ -128,10 +129,12 @@ public final class TextFieldComponent: Component {
|
|||||||
public let insets: UIEdgeInsets
|
public let insets: UIEdgeInsets
|
||||||
public let hideKeyboard: Bool
|
public let hideKeyboard: Bool
|
||||||
public let customInputView: UIView?
|
public let customInputView: UIView?
|
||||||
|
public let placeholder: NSAttributedString?
|
||||||
public let resetText: NSAttributedString?
|
public let resetText: NSAttributedString?
|
||||||
public let assumeIsEditing: Bool
|
public let assumeIsEditing: Bool
|
||||||
public let isOneLineWhenUnfocused: Bool
|
public let isOneLineWhenUnfocused: Bool
|
||||||
public let characterLimit: Int?
|
public let characterLimit: Int?
|
||||||
|
public let enableInlineAnimations: Bool
|
||||||
public let emptyLineHandling: EmptyLineHandling
|
public let emptyLineHandling: EmptyLineHandling
|
||||||
public let formatMenuAvailability: FormatMenuAvailability
|
public let formatMenuAvailability: FormatMenuAvailability
|
||||||
public let returnKeyType: UIReturnKeyType
|
public let returnKeyType: UIReturnKeyType
|
||||||
@ -152,10 +155,12 @@ public final class TextFieldComponent: Component {
|
|||||||
insets: UIEdgeInsets,
|
insets: UIEdgeInsets,
|
||||||
hideKeyboard: Bool,
|
hideKeyboard: Bool,
|
||||||
customInputView: UIView?,
|
customInputView: UIView?,
|
||||||
|
placeholder: NSAttributedString? = nil,
|
||||||
resetText: NSAttributedString?,
|
resetText: NSAttributedString?,
|
||||||
assumeIsEditing: Bool = false,
|
assumeIsEditing: Bool = false,
|
||||||
isOneLineWhenUnfocused: Bool,
|
isOneLineWhenUnfocused: Bool,
|
||||||
characterLimit: Int? = nil,
|
characterLimit: Int? = nil,
|
||||||
|
enableInlineAnimations: Bool = true,
|
||||||
emptyLineHandling: EmptyLineHandling = .allowed,
|
emptyLineHandling: EmptyLineHandling = .allowed,
|
||||||
formatMenuAvailability: FormatMenuAvailability,
|
formatMenuAvailability: FormatMenuAvailability,
|
||||||
returnKeyType: UIReturnKeyType = .default,
|
returnKeyType: UIReturnKeyType = .default,
|
||||||
@ -175,10 +180,12 @@ public final class TextFieldComponent: Component {
|
|||||||
self.insets = insets
|
self.insets = insets
|
||||||
self.hideKeyboard = hideKeyboard
|
self.hideKeyboard = hideKeyboard
|
||||||
self.customInputView = customInputView
|
self.customInputView = customInputView
|
||||||
|
self.placeholder = placeholder
|
||||||
self.resetText = resetText
|
self.resetText = resetText
|
||||||
self.assumeIsEditing = assumeIsEditing
|
self.assumeIsEditing = assumeIsEditing
|
||||||
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
|
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
|
||||||
self.characterLimit = characterLimit
|
self.characterLimit = characterLimit
|
||||||
|
self.enableInlineAnimations = enableInlineAnimations
|
||||||
self.emptyLineHandling = emptyLineHandling
|
self.emptyLineHandling = emptyLineHandling
|
||||||
self.formatMenuAvailability = formatMenuAvailability
|
self.formatMenuAvailability = formatMenuAvailability
|
||||||
self.returnKeyType = returnKeyType
|
self.returnKeyType = returnKeyType
|
||||||
@ -220,6 +227,9 @@ public final class TextFieldComponent: Component {
|
|||||||
if lhs.customInputView !== rhs.customInputView {
|
if lhs.customInputView !== rhs.customInputView {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.placeholder != rhs.placeholder {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.resetText != rhs.resetText {
|
if lhs.resetText != rhs.resetText {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -232,6 +242,9 @@ public final class TextFieldComponent: Component {
|
|||||||
if lhs.characterLimit != rhs.characterLimit {
|
if lhs.characterLimit != rhs.characterLimit {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.enableInlineAnimations != rhs.enableInlineAnimations {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -261,6 +274,7 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class View: UIView, UIScrollViewDelegate, ChatInputTextNodeDelegate {
|
public final class View: UIView, UIScrollViewDelegate, ChatInputTextNodeDelegate {
|
||||||
|
private var placeholder: ComponentView<Empty>?
|
||||||
private let textView: ChatInputTextView
|
private let textView: ChatInputTextView
|
||||||
private let inputMenu: TextInputMenu
|
private let inputMenu: TextInputMenu
|
||||||
|
|
||||||
@ -1164,6 +1178,18 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects)
|
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 {
|
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||||
customEmojiContainerView.removeFromSuperview()
|
customEmojiContainerView.removeFromSuperview()
|
||||||
self.customEmojiContainerView = nil
|
self.customEmojiContainerView = nil
|
||||||
@ -1341,6 +1367,40 @@ public final class TextFieldComponent: Component {
|
|||||||
self.textView.updateLayout(size: textFrame.size)
|
self.textView.updateLayout(size: textFrame.size)
|
||||||
self.textView.panGestureRecognizer.isEnabled = isEditing
|
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)
|
self.updateEmojiSuggestion(transition: .immediate)
|
||||||
|
|
||||||
if refreshScrolling {
|
if refreshScrolling {
|
||||||
|
@ -2334,8 +2334,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, [])
|
swipeText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress, [])
|
||||||
releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, [])
|
releaseText = (self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction, [])
|
||||||
case let .folder(_, title):
|
case let .folder(_, title):
|
||||||
swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title)._tuple
|
//TODO:release
|
||||||
releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title)._tuple
|
swipeText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgress(title.text)._tuple
|
||||||
|
releaseText = self.currentPresentationData.strings.Chat_NextChannelFolderSwipeAction(title.text)._tuple
|
||||||
}
|
}
|
||||||
|
|
||||||
if expandProgress < 0.1 {
|
if expandProgress < 0.1 {
|
||||||
|
@ -133,7 +133,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
|||||||
|
|
||||||
var chatListFilter: ChatListFilter?
|
var chatListFilter: ChatListFilter?
|
||||||
if chatSelection.onlyUsers {
|
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,
|
isShared: false,
|
||||||
hasSharedLinks: false,
|
hasSharedLinks: false,
|
||||||
categories: [.contacts, .nonContacts],
|
categories: [.contacts, .nonContacts],
|
||||||
@ -153,7 +153,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
|||||||
categories.remove(.bots)
|
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,
|
isShared: false,
|
||||||
hasSharedLinks: false,
|
hasSharedLinks: false,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user