Various improvements

This commit is contained in:
Ilya Laktyushin 2024-10-22 14:09:47 +04:00
parent df249f5302
commit 04b25d7152
21 changed files with 516 additions and 186 deletions

View File

@ -11127,6 +11127,9 @@ Sorry for the inconvenience.";
"Chat.BottomSearchPanel.MessageCount_1" = "message";
"Chat.BottomSearchPanel.MessageCount_any" = "messages";
"Chat.BottomSearchPanel.StoryCount_1" = "story";
"Chat.BottomSearchPanel.StoryCount_any" = "stories";
"Chat.BottomSearchPanel.DisplayModeFormat" = "Show as %@";
"Chat.BottomSearchPanel.DisplayModeChat" = "Chat";
"Chat.BottomSearchPanel.DisplayModeList" = "List";
@ -12397,6 +12400,12 @@ Sorry for the inconvenience.";
"HashtagSearch.Stories_any" = "%@ Stories";
"HashtagSearch.LocalStoriesFound" = "%1$@ in %2$@";
"HashtagSearch.Posts_1" = "%@ Message";
"HashtagSearch.Posts_any" = "%@ Messages";
"HashtagSearch.FoundInfoFormat" = "View %1$@ with %2$@";
"HashtagSearch.FoundStories" = "stories";
"HashtagSearch.FoundPosts" = "posts";
"Stars.BotRevenue.Title" = "Stars Balance";
"Stars.BotRevenue.Revenue.Title" = "Stars Received";
"Stars.BotRevenue.Proceeds.Title" = "Rewards Overview";

View File

@ -941,7 +941,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStorageManagementController(context: AccountContext) -> ViewController
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController
func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController

View File

@ -1031,14 +1031,18 @@ public protocol ChatController: ViewController {
var visibleContextController: ViewController? { get }
var contentContainerNode: ASDisplayNode { get }
var searching: ValuePromise<Bool> { get }
var searchResultsCount: ValuePromise<Int32> { get }
var externalSearchResultsCount: Int32? { get set }
var alwaysShowSearchResultsAsList: Bool { get set }
var includeSavedPeersInSearchResults: Bool { get set }
var showListEmptyResults: Bool { get set }
func beginMessageSearch(_ query: String)
func updatePresentationMode(_ mode: ChatControllerPresentationMode)
func beginMessageSearch(_ query: String)
func displayPromoAnnouncement(text: String)
func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition)

View File

@ -253,6 +253,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
var updatedItems: [ContextMenuItem] = []
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c, _ in
c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
})))
updatedItems.append(.separator)
for filter in filters {
if case let .filter(_, title, _, data) = filter {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
@ -338,16 +345,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
})))
}
}
updatedItems.append(.separator)
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c, _ in
c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
})))
c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true)
})))
items.append(.separator)
}
}

View File

@ -28,6 +28,10 @@ swift_library(
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View File

@ -25,6 +25,8 @@ public final class HashtagSearchController: TelegramBaseController {
private let query: String
let mode: Mode
let publicPosts: Bool
let stories: Bool
let forceDark: Bool
private var transitionDisposable: Disposable?
private let openMessageFromSearchDisposable = MetaDisposable()
@ -39,17 +41,23 @@ public final class HashtagSearchController: TelegramBaseController {
return self.displayNode as! HashtagSearchControllerNode
}
public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false) {
public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false, stories: Bool = false, forceDark: Bool = false) {
self.context = context
self.peer = peer
self.query = query
self.mode = mode
self.publicPosts = publicPosts
self.stories = stories
self.forceDark = forceDark
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if forceDark {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
self.presentationData = presentationData
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none)
@ -69,6 +77,11 @@ public final class HashtagSearchController: TelegramBaseController {
let previousTheme = self.presentationData.theme
let previousStrings = self.presentationData.strings
var presentationData = presentationData
if forceDark {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
self.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {

View File

@ -9,6 +9,8 @@ import AccountContext
import ChatListUI
import SegmentedControlNode
import ChatListSearchItemHeader
import PeerInfoVisualMediaPaneNode
import UIKitRuntimeUtils
final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
private let context: AccountContext
@ -30,6 +32,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
private let isSearching = Promise<Bool>()
private var isSearchingDisposable: Disposable?
private var searchResultsCount: Int32 = 0
private var searchResultsCountDisposable: Disposable?
private let clippingNode: ASDisplayNode
private let containerNode: ASDisplayNode
let currentController: ChatController?
@ -39,10 +44,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
let globalController: ChatController?
let globalChatContents: HashtagSearchGlobalChatContents?
private var globalStorySearchContext: SearchStoryListContext?
private var globalStorySearchDisposable = MetaDisposable()
private var globalStorySearchState: StoryListContext.State?
private var globalStorySearchComponentView: ComponentView<Empty>?
private var storySearchContext: SearchStoryListContext?
private var storySearchDisposable = MetaDisposable()
private var storySearchState: StoryListContext.State?
private var storySearchComponentView: ComponentView<Empty>?
private var storySearchPaneNode: PeerInfoStoryPaneNode?
private var isDisplayingStories = false
private var panRecognizer: InteractiveTransitionGestureRecognizer?
@ -57,8 +65,14 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.navigationBar = navigationBar
self.isCashtag = query.hasPrefix("$")
self.presentationData = controller.presentationData
self.isDisplayingStories = controller.stories
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
var controllerParams: ChatControllerParams?
if controller.forceDark {
controllerParams = ChatControllerParams(forcedTheme: defaultDarkColorPresentationTheme, forcedWallpaper: defaultBuiltinWallpaper(data: .default, colors: defaultDarkWallpaperGradientColors.map(\.rgb), intensity: -34))
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
@ -78,7 +92,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
let navigationController = controller.navigationController as? NavigationController
if let peer, controller.mode != .noChat {
self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil)
self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: controllerParams)
self.currentController?.alwaysShowSearchResultsAsList = true
self.currentController?.showListEmptyResults = true
self.currentController?.customNavigationController = navigationController
@ -89,7 +103,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
if let _ = peer, controller.mode != .chatOnly {
let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false)
self.myChatContents = myChatContents
self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: nil)
self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: controllerParams)
self.myController?.alwaysShowSearchResultsAsList = true
self.myController?.showListEmptyResults = true
self.myController?.customNavigationController = navigationController
@ -100,7 +114,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true)
self.globalChatContents = globalChatContents
self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: nil)
self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: controllerParams)
self.globalController?.alwaysShowSearchResultsAsList = true
self.globalController?.showListEmptyResults = true
self.globalController?.customNavigationController = navigationController
@ -182,7 +196,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
self.addSubnode(self.shimmerNode)
if !self.isDisplayingStories {
self.addSubnode(self.shimmerNode)
}
self.searchContentNode.setQueryUpdated { [weak self] query in
self?.searchQueryPromise.set(query)
@ -227,13 +243,25 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
}
})
if let currentController = self.currentController {
self.searchResultsCountDisposable = (currentController.searchResultsCount.get()
|> deliverOnMainQueue).start(next: { [weak self] searchResultsCount in
guard let self else {
return
}
self.searchResultsCount = searchResultsCount
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
})
}
self.updateStorySearch()
}
deinit {
self.searchQueryDisposable?.dispose()
self.isSearchingDisposable?.dispose()
self.globalStorySearchDisposable.dispose()
self.searchResultsCountDisposable?.dispose()
self.storySearchDisposable.dispose()
}
private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections {
@ -373,29 +401,30 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
}
private func updateStorySearch() {
self.globalStorySearchState = nil
self.globalStorySearchDisposable.set(nil)
self.globalStorySearchContext = nil
self.storySearchState = nil
self.storySearchDisposable.set(nil)
self.storySearchContext = nil
if !self.query.isEmpty {
var peerId: EnginePeer.Id?
if self.controller?.mode == .chatOnly {
peerId = self.peer?.id
}
let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query))
self.globalStorySearchDisposable.set((globalStorySearchContext.state
let storySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query))
self.storySearchDisposable.set((storySearchContext.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
return
}
if state.totalCount > 0 {
self.globalStorySearchState = state
self.storySearchState = state
} else {
self.globalStorySearchState = nil
self.storySearchState = nil
self.currentController?.externalSearchResultsCount = nil
}
self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut))
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
}))
self.globalStorySearchContext = globalStorySearchContext
self.storySearchContext = storySearchContext
}
}
@ -420,6 +449,37 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
}
}
private func animateContentOut() {
guard let controller = self.currentController else {
return
}
controller.contentContainerNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.3, removeOnCompletion: false)
if let blurFilter = makeBlurFilter() {
blurFilter.setValue(30.0 as NSNumber, forKey: "inputRadius")
controller.contentContainerNode.layer.filters = [blurFilter]
controller.contentContainerNode.layer.animate(from: 0.0 as NSNumber, to: 30.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false)
}
}
private func animateContentIn() {
guard let controller = self.currentController else {
return
}
controller.contentContainerNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
if let blurFilter = makeBlurFilter() {
blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius")
controller.contentContainerNode.layer.filters = [blurFilter]
controller.contentContainerNode.layer.animate(from: 30.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak controller] completed in
guard let controller, completed else {
return
}
controller.contentContainerNode.layer.filters = []
})
}
}
func requestUpdate(transition: ContainedViewLayoutTransition) {
if let (layout, navigationHeight) = self.containerLayout {
let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition)
@ -440,45 +500,69 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.insertSubnode(self.clippingNode, at: 0)
}
var storyParentController: ViewController?
if self.controller?.mode == .chatOnly {
storyParentController = self.currentController
} else {
storyParentController = self.globalController
}
var currentTopInset: CGFloat = 0.0
var globalTopInset: CGFloat = 0.0
if let state = self.globalStorySearchState {
var parentController: ViewController?
if self.controller?.mode == .chatOnly {
parentController = self.currentController
var panelSearchState: StoryResultsPanelComponent.SearchState?
if let storySearchState = self.storySearchState {
if self.isDisplayingStories {
if self.searchResultsCount > 0 {
panelSearchState = .messages(self.searchResultsCount)
}
} else {
parentController = self.globalController
panelSearchState = .stories(storySearchState)
}
if let parentController {
}
if self.isDisplayingStories {
if let storySearchState = self.storySearchState {
self.currentController?.externalSearchResultsCount = Int32(storySearchState.totalCount)
} else {
self.currentController?.externalSearchResultsCount = nil
}
} else {
self.currentController?.externalSearchResultsCount = nil
}
if let panelSearchState {
if let storyParentController {
let componentView: ComponentView<Empty>
var panelTransition = ComponentTransition(transition)
if let current = self.globalStorySearchComponentView {
if let current = self.storySearchComponentView {
componentView = current
} else {
panelTransition = .immediate
componentView = ComponentView()
self.globalStorySearchComponentView = componentView
self.storySearchComponentView = componentView
}
let panelSize = componentView.update(
transition: .immediate,
transition: panelTransition,
component: AnyComponent(StoryResultsPanelComponent(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
query: self.query,
peer: self.controller?.mode == .chatOnly ? self.peer : nil,
state: state,
state: panelSearchState,
sideInset: layout.safeInsets.left,
action: { [weak self] in
guard let self else {
return
}
var peer: EnginePeer?
if self.controller?.mode == .chatOnly {
peer = self.peer
self.isDisplayingStories = !self.isDisplayingStories
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
} else {
let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(nil, self.query), listContext: self.storySearchContext)
self.controller?.push(searchController)
}
let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(peer, self.query), listContext: self.globalStorySearchContext)
self.controller?.push(searchController)
}
)),
environment: {},
@ -487,8 +571,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize)
if let view = componentView.view {
if view.superview == nil {
parentController.view.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true)
storyParentController.view.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
panelTransition.setFrame(view: view, frame: panelFrame)
}
@ -498,9 +582,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
globalTopInset += panelSize.height
}
}
} else if let globalStorySearchComponentView = self.globalStorySearchComponentView {
globalStorySearchComponentView.view?.removeFromSuperview()
self.globalStorySearchComponentView = nil
} else if let storySearchComponentView = self.storySearchComponentView {
storySearchComponentView.view?.removeFromSuperview()
self.storySearchComponentView = nil
}
if let controller = self.currentController {
@ -548,6 +632,75 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
}
}
if self.isDisplayingStories, let peer = self.peer, let storySearchContext = self.storySearchContext {
let storySearchPaneNode: PeerInfoStoryPaneNode
var paneTransition = transition
if let current = self.storySearchPaneNode {
storySearchPaneNode = current
} else {
storySearchPaneNode = PeerInfoStoryPaneNode(
context: self.context,
scope: .search(peerId: peer.id, query: self.query),
captureProtected: false,
isProfileEmbedded: false,
canManageStories: false,
navigationController: { [weak self] in
guard let self else {
return nil
}
return self.controller?.navigationController as? NavigationController
},
listContext: storySearchContext
)
self.storySearchPaneNode = storySearchPaneNode
if let storySearchView = self.storySearchComponentView?.view {
storySearchView.superview?.insertSubview(storySearchPaneNode.view, belowSubview: storySearchView)
} else {
storyParentController?.view.addSubview(storySearchPaneNode.view)
}
paneTransition = .immediate
if transition.isAnimated {
storySearchPaneNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
storySearchPaneNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.animateContentOut()
}
}
var bottomInset: CGFloat = 0.0
if case .regular = layout.metrics.widthClass {
bottomInset += 49.0
} else {
bottomInset += 45.0
}
bottomInset += layout.intrinsicInsets.bottom
storySearchPaneNode.update(
size: layout.size,
topInset: navigationBarHeight,
sideInset: layout.safeInsets.left,
bottomInset: 0.0,
deviceMetrics: layout.deviceMetrics,
visibleHeight: layout.size.height - currentTopInset,
isScrollingLockedAtTop: false,
expandProgress: 1.0,
navigationHeight: 0.0,
presentationData: self.presentationData,
synchronous: false,
transition: paneTransition
)
storySearchPaneNode.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
paneTransition.updateFrame(node: storySearchPaneNode, frame: CGRect(origin: CGPoint(x: 0.0, y: currentTopInset), size: CGSize(width: layout.size.width, height: layout.size.height - bottomInset - currentTopInset)))
} else if let storySearchPaneNode = self.storySearchPaneNode {
self.storySearchPaneNode = nil
storySearchPaneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
storySearchPaneNode.view.removeFromSuperview()
})
storySearchPaneNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.animateContentIn()
}
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: .zero, size: layout.size))
let containerPosition: CGFloat = -layout.size.width * CGFloat(self.searchContentNode.selectedIndex) - self.panTransitionFraction * layout.size.width
@ -559,7 +712,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg
self.shimmerNode.update(context: self.context, size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height), presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, key: .chats, hasSelection: false, transition: transition)
if isFirstTime {
self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode)
if self.shimmerNode.supernode != nil {
self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode)
} else {
self.insertSubnode(self.recentListNode, aboveSubnode: self.clippingNode)
}
}
transition.updateFrame(node: self.recentListNode, frame: CGRect(origin: .zero, size: layout.size))

View File

@ -7,14 +7,20 @@ import MultilineTextComponent
import BundleIconComponent
import StorySetIndicatorComponent
import AccountContext
import AnimatedTextComponent
import BlurredBackgroundComponent
final class StoryResultsPanelComponent: CombinedComponent {
enum SearchState: Equatable {
case stories(StoryListContext.State)
case messages(Int32)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let query: String
let peer: EnginePeer?
let state: StoryListContext.State
let state: SearchState
let sideInset: CGFloat
let action: () -> Void
@ -24,7 +30,7 @@ final class StoryResultsPanelComponent: CombinedComponent {
strings: PresentationStrings,
query: String,
peer: EnginePeer?,
state: StoryListContext.State,
state: SearchState,
sideInset: CGFloat,
action: @escaping () -> Void
) {
@ -61,10 +67,11 @@ final class StoryResultsPanelComponent: CombinedComponent {
}
static var body: Body {
let background = Child(Rectangle.self)
let background = Child(BlurredBackgroundComponent.self)
let avatars = Child(StorySetIndicatorComponent.self)
let titlePrefix = Child(AnimatedTextComponent.self)
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let text = Child(AnimatedTextComponent.self)
let arrow = Child(BundleIconComponent.self)
let separator = Child(Rectangle.self)
let button = Child(Button.self)
@ -74,39 +81,47 @@ final class StoryResultsPanelComponent: CombinedComponent {
let spacing: CGFloat = 3.0
let textLeftInset: CGFloat = 81.0 + component.sideInset
var textLeftInset: CGFloat = 16.0 + component.sideInset
let textTopInset: CGFloat = 9.0
var existingPeerIds = Set<EnginePeer.Id>()
var items: [StorySetIndicatorComponent.Item] = []
for item in component.state.items {
guard let peer = item.peer, !existingPeerIds.contains(peer.id) else {
continue
switch component.state {
case let .stories(state):
for item in state.items {
guard let peer = item.peer, !existingPeerIds.contains(peer.id) || component.peer != nil else {
continue
}
existingPeerIds.insert(peer.id)
items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer))
}
existingPeerIds.insert(peer.id)
items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer))
textLeftInset += 65.0
default:
break
}
let avatars = avatars.update(
component: StorySetIndicatorComponent(
context: component.context,
strings: component.strings,
items: Array(items.prefix(3)),
displayAvatars: true,
hasUnseen: true,
hasUnseenPrivate: false,
totalCount: 0,
theme: component.theme,
action: {}
),
availableSize: context.availableSize,
transition: .immediate
)
var titlePrefixString: [AnimatedTextComponent.Item] = []
let titleString: NSAttributedString
var textString: [AnimatedTextComponent.Item] = []
if let peer = component.peer, let username = peer.addressName {
let storiesString = component.strings.HashtagSearch_Stories(Int32(component.state.totalCount))
let fullString = component.strings.HashtagSearch_LocalStoriesFound(storiesString, "@\(username)")
let entityType: String
switch component.state {
case let .messages(count):
titlePrefixString = [AnimatedTextComponent.Item(
id: "text",
isUnbreakable: true,
content: .text(component.strings.HashtagSearch_Posts(count))
)]
entityType = component.strings.HashtagSearch_FoundPosts
case let .stories(state):
titlePrefixString = [AnimatedTextComponent.Item(
id: "text",
isUnbreakable: true,
content: .text(component.strings.HashtagSearch_Stories(Int32(state.totalCount)))
)]
entityType = component.strings.HashtagSearch_FoundStories
}
let fullString = component.strings.HashtagSearch_LocalStoriesFound("", "@\(username)")
titleString = NSMutableAttributedString(
string: fullString.string,
font: Font.semibold(15.0),
@ -116,13 +131,48 @@ final class StoryResultsPanelComponent: CombinedComponent {
if let lastRange = fullString.ranges.last?.range {
(titleString as? NSMutableAttributedString)?.addAttribute(NSAttributedString.Key.foregroundColor, value: component.theme.rootController.navigationBar.accentTextColor, range: lastRange)
}
textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat(
".",
"."
), id: "info", mapping: [
0: .text(entityType),
1: .text(component.query)
])
} else {
titleString = NSAttributedString(
string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)),
font: Font.semibold(15.0),
textColor: component.theme.rootController.navigationBar.primaryTextColor,
paragraphAlignment: .natural
if case let .stories(state) = component.state {
titleString = NSAttributedString(
string: component.strings.HashtagSearch_StoriesFound(Int32(state.totalCount)),
font: Font.semibold(15.0),
textColor: component.theme.rootController.navigationBar.primaryTextColor,
paragraphAlignment: .natural
)
} else {
titleString = NSAttributedString()
}
textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat(
".",
"."
), id: "info", mapping: [
0: .text(component.strings.HashtagSearch_FoundStories),
1: .text(component.query)
])
}
var titlePrefixOffset: CGFloat = 0.0
var titlePrefixChild: _UpdatedChildComponent?
if !titlePrefixString.isEmpty {
let titlePrefix = titlePrefix.update(
component: AnimatedTextComponent(
font: Font.semibold(15.0),
color: component.theme.rootController.navigationBar.primaryTextColor,
items: titlePrefixString,
noDelay: true
),
availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0, height: context.availableSize.height),
transition: context.transition
)
titlePrefixOffset = titlePrefix.size.width + 1.0
titlePrefixChild = titlePrefix
}
let title = title.update(
@ -131,23 +181,19 @@ final class StoryResultsPanelComponent: CombinedComponent {
horizontalAlignment: .natural,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0 - titlePrefixOffset, height: CGFloat.greatestFiniteMagnitude),
transition: context.transition
)
let text = text.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string,
font: Font.regular(14.0),
textColor: component.theme.rootController.navigationBar.secondaryTextColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .natural,
maximumNumberOfLines: 1
component: AnimatedTextComponent(
font: Font.regular(14.0),
color: component.theme.rootController.navigationBar.secondaryTextColor,
items: textString,
noDelay: true
),
availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height),
transition: .immediate
transition: context.transition
)
let arrow = arrow.update(
@ -162,7 +208,7 @@ final class StoryResultsPanelComponent: CombinedComponent {
let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0)
let background = background.update(
component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor),
component: BlurredBackgroundComponent(color: component.theme.rootController.navigationBar.blurredBackgroundColor),
availableSize: size,
transition: .immediate
)
@ -190,12 +236,37 @@ final class StoryResultsPanelComponent: CombinedComponent {
.position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0))
)
context.add(avatars
.position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0))
)
if !items.isEmpty {
let avatars = avatars.update(
component: StorySetIndicatorComponent(
context: component.context,
strings: component.strings,
items: Array(items.prefix(3)),
displayAvatars: component.peer == nil,
hasUnseen: true,
hasUnseenPrivate: false,
totalCount: 0,
theme: component.theme,
action: {}
),
availableSize: context.availableSize,
transition: .immediate
)
context.add(avatars
.position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if let titlePrefixChild {
context.add(titlePrefixChild
.position(CGPoint(x: textLeftInset + titlePrefixChild.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
}
context.add(title
.position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
.position(CGPoint(x: textLeftInset + titlePrefixOffset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text

View File

@ -565,7 +565,7 @@ public func legacyAttachmentMenu(
carouselItemView?.underlyingViews = underlyingViews
if editMediaOptions == nil {
if editMediaOptions == nil && !addingMedia {
for i in 0 ..< min(20, recentlyUsedInlineBots.count) {
let peer = recentlyUsedInlineBots[i]
let addressName = peer.addressName

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
public final class AnimatedTextComponent: Component {
public struct Item: Equatable {
@ -24,15 +25,18 @@ public final class AnimatedTextComponent: Component {
public let font: UIFont
public let color: UIColor
public let items: [Item]
public let noDelay: Bool
public init(
font: UIFont,
color: UIColor,
items: [Item]
items: [Item],
noDelay: Bool = false
) {
self.font = font
self.color = color
self.items = items
self.noDelay = noDelay
}
public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool {
@ -45,6 +49,9 @@ public final class AnimatedTextComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.noDelay != rhs.noDelay {
return false
}
return true
}
@ -157,10 +164,12 @@ public final class AnimatedTextComponent: Component {
if animateIn, !transition.animation.isImmediate {
var delayWidth: Double = 0.0
if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth
} else {
firstDelayWidth = size.width
if !component.noDelay {
if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth
} else {
firstDelayWidth = size.width
}
}
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
@ -220,3 +229,33 @@ public final class AnimatedTextComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public extension AnimatedTextComponent {
static func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] {
var textItems: [AnimatedTextComponent.Item] = []
var previousIndex = 0
let nsString = string.string as NSString
for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) {
if range.range.lowerBound > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex)))))
}
if let value = mapping[range.index] {
let isUnbreakable: Bool
switch value {
case .text:
isUnbreakable = true
case .number:
isUnbreakable = false
}
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value))
}
previousIndex = range.range.upperBound
}
if nsString.length > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex)))))
}
return textItems
}
}

View File

@ -1025,7 +1025,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError>
let profileGiftsContext: ProfileGiftsContext?
if case .user = kind {
profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId)
if isMyProfile || userPeerId != context.account.peerId {
profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId)
} else {
profileGiftsContext = nil
}
premiumGiftOptions = .single([])
|> then(
context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
@ -1314,7 +1318,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.stories, at: 0)
}
if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData {
if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData, peerView.peerId != context.account.peerId {
if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 {
availablePanes?.insert(.gifts, at: hasStories ? 1 : 0)
}

View File

@ -1770,7 +1770,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if case .peer = self.scope {
let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
|> deliverOnMainQueue).start(next: { [weak self] count in
guard let strongSelf = self else {
return
}
@ -2420,7 +2420,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.updateDisposable.dispose()
self.mapDisposable?.dispose()
}
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
let listSource = self.listSource
return Signal { subscriber in
@ -2862,6 +2862,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
continue
}
var authorPeer = item.peer
var isReorderable = false
switch self.scope {
case .botPreview:
@ -2870,16 +2871,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if id == self.context.account.peerId {
isReorderable = state.pinnedIds.contains(item.storyItem.id)
}
case let .search(peerId, _):
if peerId != nil {
authorPeer = nil
}
default:
break
}
mappedItems.append(VisualMediaItem(
index: mappedItems.count,
peer: peerReference,
storyId: item.id,
story: item.storyItem,
authorPeer: item.peer,
authorPeer: authorPeer,
isPinned: state.pinnedIds.contains(item.storyItem.id),
localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue,
isReorderable: isReorderable
@ -4104,7 +4109,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
if self.isProfileEmbedded, case .botPreview = self.scope {
self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
self.view.backgroundColor = .clear
if case let .search(peerId, _) = self.scope, peerId != nil {
} else {
self.view.backgroundColor = .clear
}
}
}
}

View File

@ -2922,7 +2922,7 @@ final class StoryItemSetContainerSendMessage {
let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(nil, hashtag), listContext: nil)
navigationController.pushViewController(searchController)
} else {
let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true)
let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, stories: true, forceDark: true)
navigationController.pushViewController(searchController)
}
}

View File

@ -330,6 +330,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let startingBot = ValuePromise<Bool>(false, ignoreRepeated: true)
let unblockingPeer = ValuePromise<Bool>(false, ignoreRepeated: true)
public let searching = ValuePromise<Bool>(false, ignoreRepeated: true)
public let searchResultsCount = ValuePromise<Int32>(0, ignoreRepeated: true)
let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>()
let loadingMessage = Promise<ChatLoadingMessageSubject?>(nil)
let performingInlineSearch = ValuePromise<Bool>(false, ignoreRepeated: true)
@ -629,6 +630,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
public var externalSearchResultsCount: Int32? {
didSet {
if let panelNode = self.chatDisplayNode.inputPanelNode as? ChatTagSearchInputPanelNode {
panelNode.externalSearchResultsCount = self.externalSearchResultsCount
}
}
}
public var includeSavedPeersInSearchResults: Bool = false {
didSet {
self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults
@ -9697,7 +9706,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if case let .customChatContents(contents) = self.subject, case let .hashTagSearch(publicPostsValue) = contents.kind {
publicPosts = publicPostsValue
}
let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: publicPosts)
let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: peerName == nil && publicPosts)
self.effectiveNavigationController?.pushViewController(searchController)
}
}))
@ -10972,4 +10981,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return true
}
}
public var contentContainerNode: ASDisplayNode {
return self.chatDisplayNode.contentContainerNode
}
}

View File

@ -513,7 +513,7 @@ extension ChatControllerImpl {
title = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount))
}
let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
let textItems = AnimatedTextComponent.extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
0: .number(self.currentSendStarsUndoCount, minDigits: 1),
1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount)))
])
@ -536,31 +536,3 @@ extension ChatControllerImpl {
}
}
}
private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] {
var textItems: [AnimatedTextComponent.Item] = []
var previousIndex = 0
let nsString = string.string as NSString
for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) {
if range.range.lowerBound > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex)))))
}
if let value = mapping[range.index] {
let isUnbreakable: Bool
switch value {
case .text:
isUnbreakable = true
case .number:
isUnbreakable = false
}
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value))
}
previousIndex = range.range.upperBound
}
if nsString.length > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex)))))
}
return textItems
}

View File

@ -77,6 +77,7 @@ extension ChatControllerImpl {
if queryIsEmpty {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
self.searchResult.set(.single(nil))
if let data = interfaceState.search {
@ -104,6 +105,7 @@ extension ChatControllerImpl {
guard let strongSelf = self else {
return
}
strongSelf.searchResultsCount.set(results.totalCount)
var navigateIndex: MessageIndex?
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search {
@ -152,6 +154,7 @@ extension ChatControllerImpl {
guard let strongSelf = self else {
return
}
strongSelf.searchResultsCount.set(results.totalCount)
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
if let data = current.search, let previousResultsState = data.resultsState {
let messageIndices = results.messages.map({ $0.index }).sorted()
@ -167,11 +170,13 @@ extension ChatControllerImpl {
}))
} else {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
}
}
} else {
self.searching.set(false)
self.searchResultsCount.set(0)
self.searchDisposable?.set(nil)
if let data = interfaceState.search {

View File

@ -19,34 +19,6 @@ import AnimatedTextComponent
private let labelFont = Font.regular(15.0)
private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] {
var textItems: [AnimatedTextComponent.Item] = []
var previousIndex = 0
let nsString = string.string as NSString
for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) {
if range.range.lowerBound > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex)))))
}
if let value = mapping[range.index] {
let isUnbreakable: Bool
switch value {
case .text:
isUnbreakable = true
case .number:
isUnbreakable = false
}
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value))
}
previousIndex = range.range.upperBound
}
if nsString.length > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex)))))
}
return textItems
}
final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
private struct Params: Equatable {
var width: CGFloat
@ -100,6 +72,14 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
private var totalMessageCount: Int?
private var totalMessageCountDisposable: Disposable?
public var externalSearchResultsCount: Int32? {
didSet {
if let params = self.currentLayout?.params {
let _ = self.update(params: params, transition: .spring(duration: 0.4))
}
}
}
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
didSet {
}
@ -223,7 +203,15 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
var canChangeListMode = false
var resultsTextString: [AnimatedTextComponent.Item] = []
if let results = params.interfaceState.search?.resultsState {
if let externalSearchResultsCount = self.externalSearchResultsCount {
let value = presentationStringsFormattedNumber(externalSearchResultsCount, params.interfaceState.dateTimeFormat.groupingSeparator)
let suffix = params.interfaceState.strings.Chat_BottomSearchPanel_StoryCount(externalSearchResultsCount)
resultsTextString = [AnimatedTextComponent.Item(
id: "stories",
isUnbreakable: true,
content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string)
)]
} else if let results = params.interfaceState.search?.resultsState {
let displayTotalCount = results.completed ? results.messageIndices.count : Int(results.totalCount)
if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) {
canChangeListMode = true
@ -237,7 +225,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string)
)]
} else if params.interfaceState.displayHistoryFilterAsList {
resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(
resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(
".",
"."
), id: "total_count", mapping: [
@ -247,7 +235,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
} else {
let adjustedIndex = results.messageIndices.count - 1 - index
resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM(
resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM(
".",
"."
), id: "position", mapping: [
@ -263,7 +251,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
} else if let count = self.tagMessageCount?.count ?? self.totalMessageCount {
canChangeListMode = count != 0
resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(
resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(
".",
"."
), id: "total_count", mapping: [
@ -282,7 +270,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
}
var modeButtonTitle: [AnimatedTextComponent.Item] = []
modeButtonTitle = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [
modeButtonTitle = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [
0: params.interfaceState.displayHistoryFilterAsList ? .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeChat) : .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeList)
])
@ -346,7 +334,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
var nextLeftX: CGFloat = 16.0
if !self.alwaysShowTotalMessagesCount {
if !self.alwaysShowTotalMessagesCount && self.externalSearchResultsCount == nil {
nextLeftX = 12.0
let calendarButtonSize = self.calendarButton.update(
transition: .immediate,
@ -372,12 +360,28 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode {
if let calendarButtonView = self.calendarButton.view {
if calendarButtonView.superview == nil {
self.view.addSubview(calendarButtonView)
if !transition.animation.isImmediate {
calendarButtonView.alpha = 1.0
transition.animateAlpha(view: calendarButtonView, from: 0.0, to: 1.0)
transition.animateScale(view: calendarButtonView, from: 0.01, to: 1.0)
}
}
transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame)
}
nextLeftX += calendarButtonSize.width + 8.0
} else if let calendarButtonView = self.calendarButton.view {
calendarButtonView.removeFromSuperview()
if transition.animation.isImmediate {
calendarButtonView.removeFromSuperview()
} else {
transition.setAlpha(view: calendarButtonView, alpha: 0.0, completion: { finished in
if finished {
calendarButtonView.removeFromSuperview()
}
calendarButtonView.alpha = 1.0
})
transition.animateScale(view: calendarButtonView, from: 1.0, to: 0.01)
}
}
if displaySearchMembers {

View File

@ -1924,8 +1924,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return inputPanelNode
}
public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController {
return HashtagSearchController(context: context, peer: peer, query: query, mode: all ? .noChat : .generic)
public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController {
return HashtagSearchController(context: context, peer: peer, query: query, mode: stories ? .chatOnly : .generic, stories: stories, forceDark: forceDark)
}
public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController {

View File

@ -9,23 +9,27 @@ public struct ChatTranslationState: Codable {
enum CodingKeys: String, CodingKey {
case baseLang
case fromLang
case timestamp
case toLang
case isEnabled
}
public let baseLang: String
public let fromLang: String
public let timestamp: Int32?
public let toLang: String?
public let isEnabled: Bool
public init(
baseLang: String,
fromLang: String,
timestamp: Int32?,
toLang: String?,
isEnabled: Bool
) {
self.baseLang = baseLang
self.fromLang = fromLang
self.timestamp = timestamp
self.toLang = toLang
self.isEnabled = isEnabled
}
@ -35,6 +39,7 @@ public struct ChatTranslationState: Codable {
self.baseLang = try container.decode(String.self, forKey: .baseLang)
self.fromLang = try container.decode(String.self, forKey: .fromLang)
self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp)
self.toLang = try container.decodeIfPresent(String.self, forKey: .toLang)
self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled)
}
@ -44,6 +49,7 @@ public struct ChatTranslationState: Codable {
try container.encode(self.baseLang, forKey: .baseLang)
try container.encode(self.fromLang, forKey: .fromLang)
try container.encodeIfPresent(self.timestamp, forKey: .timestamp)
try container.encodeIfPresent(self.toLang, forKey: .toLang)
try container.encode(self.isEnabled, forKey: .isEnabled)
}
@ -52,6 +58,7 @@ public struct ChatTranslationState: Codable {
return ChatTranslationState(
baseLang: self.baseLang,
fromLang: self.fromLang,
timestamp: self.timestamp,
toLang: toLang,
isEnabled: self.isEnabled
)
@ -61,6 +68,7 @@ public struct ChatTranslationState: Codable {
return ChatTranslationState(
baseLang: self.baseLang,
fromLang: self.fromLang,
timestamp: self.timestamp,
toLang: self.toLang,
isEnabled: isEnabled
)
@ -191,7 +199,8 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
return cachedChatTranslationState(engine: context.engine, peerId: peerId)
|> mapToSignal { cached in
if let cached, cached.baseLang == baseLang {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 {
if !dontTranslateLanguages.contains(cached.fromLang) {
return .single(cached)
} else {
@ -277,7 +286,13 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
if loggingEnabled {
Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)")
}
let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false)
let state = ChatTranslationState(
baseLang: baseLang,
fromLang: fromLang,
timestamp: currentTime,
toLang: cached?.toLang,
isEnabled: cached?.isEnabled ?? false
)
let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start()
if !dontTranslateLanguages.contains(fromLang) {
return state

View File

@ -587,6 +587,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
private var updateWebViewWhenStable = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let previousLayout = self.validLayout?.0
@ -605,6 +607,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset)))
if !webView.frame.width.isZero && webView.frame != frame {
self.updateWebViewWhenStable = true
}
var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
@ -635,6 +640,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let controller = self.controller {
webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition)
if self.updateWebViewWhenStable && !controller.isContainerPanning() {
self.updateWebViewWhenStable = false
webView.setNeedsLayout()
}
}
if layout.intrinsicInsets.bottom > 44.0 {