mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Camera and editor improvements
This commit is contained in:
commit
37e7a74e4e
28 changed files with 1478 additions and 250 deletions
|
|
@ -742,6 +742,10 @@ public protocol RecentSessionsController: AnyObject {
|
|||
public protocol AttachmentFileController: AnyObject {
|
||||
}
|
||||
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
func openStoryCamera()
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
|
|
|
|||
|
|
@ -86,8 +86,15 @@ public struct FetchManagerPriorityKey: Comparable {
|
|||
}
|
||||
}
|
||||
|
||||
public enum FetchManagerLocation: Hashable {
|
||||
public enum FetchManagerLocation: Hashable, CustomStringConvertible {
|
||||
case chat(PeerId)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .chat(peerId):
|
||||
return "chat:\(peerId)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FetchManagerForegroundDirection {
|
||||
|
|
|
|||
|
|
@ -47,12 +47,46 @@ import InviteLinksUI
|
|||
import ChatFolderLinkPreviewScreen
|
||||
import StoryContainerScreen
|
||||
import StoryContentComponent
|
||||
import StoryPeerListComponent
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
return false
|
||||
}
|
||||
|
||||
let storiesFraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
|
||||
|
||||
var visibleStoriesProgress = max(0.0, min(1.0, searchNode.expansionProgress))
|
||||
visibleStoriesProgress = (1.0 / storiesFraction) * visibleStoriesProgress
|
||||
visibleStoriesProgress = max(0.0, min(1.0, visibleStoriesProgress))
|
||||
|
||||
let searchFieldHeight: CGFloat = 36.0
|
||||
let searchFraction = searchFieldHeight / searchNode.nominalHeight
|
||||
let visibleSearchProgress = max(0.0, min(1.0, searchNode.expansionProgress) - 1.0 + searchFraction) / searchFraction
|
||||
|
||||
if visibleSearchProgress > 0.0 && visibleSearchProgress < 1.0 {
|
||||
let offset: CGFloat
|
||||
if visibleSearchProgress < 0.6 {
|
||||
offset = navigationBarSearchContentHeight
|
||||
} else {
|
||||
offset = 0.0
|
||||
}
|
||||
let _ = listNode.scrollToOffsetFromTop(offset, animated: true)
|
||||
return true
|
||||
} else if visibleStoriesProgress > 0.0 && visibleStoriesProgress < 1.0 {
|
||||
let offset: CGFloat
|
||||
if visibleStoriesProgress < 0.3 {
|
||||
offset = navigationBarSearchContentHeight + 94.0
|
||||
} else {
|
||||
offset = navigationBarSearchContentHeight
|
||||
}
|
||||
let _ = listNode.scrollToOffsetFromTop(offset, animated: true)
|
||||
return true
|
||||
}
|
||||
|
||||
if "".isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 {
|
||||
let offset: CGFloat
|
||||
if searchNode.expansionProgress < 0.6 {
|
||||
|
|
@ -185,7 +219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
private let navigationSecondaryContentNode: ASDisplayNode
|
||||
private var storyPeerListView: ComponentView<Empty>?
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool, Int32?)?
|
||||
|
||||
|
|
@ -210,7 +243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
|
||||
private var powerSavingMonitoringDisposable: Disposable?
|
||||
|
||||
private var storyListContext: StoryListContext?
|
||||
public private(set) var storyListContext: StoryListContext?
|
||||
private var storyListState: StoryListContext.State?
|
||||
private var storyListStateDisposable: Disposable?
|
||||
|
||||
|
|
@ -444,7 +477,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
tabsIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
if case .chatList(.root) = self.location {
|
||||
self.searchContentNode?.additionalHeight = 94.0
|
||||
}
|
||||
self.navigationBar?.secondaryContentHeight = !tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0
|
||||
|
||||
enum State: Equatable {
|
||||
case empty(hasDownloads: Bool)
|
||||
|
|
@ -2123,12 +2159,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
tabsIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
if case .chatList(.root) = self.location {
|
||||
self.searchContentNode?.additionalHeight = 94.0
|
||||
}
|
||||
|
||||
if wasEmpty != isEmpty || self.storyPeerListView == nil {
|
||||
self.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
} else if let componentView = self.storyPeerListView?.view, !componentView.bounds.isEmpty {
|
||||
self.updateStoryPeerListView(size: componentView.bounds.size, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
if wasEmpty != isEmpty {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
|
||||
self.requestLayout(transition: transition)
|
||||
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
|
||||
} else {
|
||||
self.updateHeaderStories(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).containedViewLayoutTransition)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -2358,33 +2400,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
super.updateNavigationBarLayout(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
self.updateLayout(layout: layout, transition: transition)
|
||||
|
||||
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
|
||||
searchContentNode.updateListVisibleContentOffset(.known(0.0))
|
||||
self.chatListDisplayNode.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStoryPeerListView(size: CGSize, transition: Transition) {
|
||||
guard let storyPeerListView = self.storyPeerListView else {
|
||||
return
|
||||
}
|
||||
let _ = storyPeerListView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
context: self.context,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
state: self.storyListState,
|
||||
peerAction: { [weak self] peer in
|
||||
private func updateHeaderStories(transition: ContainedViewLayoutTransition) {
|
||||
if let searchContentNode = self.searchContentNode, case .chatList(.root) = self.location {
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
componentView.storyPeerAction = { [weak self] peer in
|
||||
guard let self, let storyListContext = self.storyListContext else {
|
||||
return
|
||||
}
|
||||
|
|
@ -2401,8 +2420,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
|
||||
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peer.id) {
|
||||
if let peer, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
|
|
@ -2411,9 +2430,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
}
|
||||
|
||||
var initialFocusedId: AnyHashable?
|
||||
if let peer {
|
||||
initialFocusedId = AnyHashable(peer.id)
|
||||
}
|
||||
|
||||
if !initialContent.contains(where: { slice in
|
||||
return !slice.items.isEmpty
|
||||
}) {
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.openStoryCamera()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
initialFocusedId: AnyHashable(peer.id),
|
||||
initialFocusedId: initialFocusedId,
|
||||
initialContent: initialContent,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { [weak self] peerId in
|
||||
|
|
@ -2421,8 +2455,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
return nil
|
||||
}
|
||||
|
||||
if let storyPeerListView = self.storyPeerListView?.view as? StoryPeerListComponent.View {
|
||||
if let transitionView = storyPeerListView.transitionViewForItem(peerId: peerId) {
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
|
|
@ -2437,10 +2471,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
|
||||
let fraction = 94.0 / (navigationBarSearchContentHeight + 94.0)
|
||||
|
||||
var visibleProgress = max(0.0, min(1.0, searchContentNode.expansionProgress))
|
||||
visibleProgress = (1.0 / fraction) * visibleProgress
|
||||
visibleProgress = max(0.0, min(1.0, visibleProgress))
|
||||
|
||||
componentView.updateStories(offset: visibleProgress, context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, storyListState: self.storyListState, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
self.updateLayout(layout: layout, transition: transition)
|
||||
|
||||
self.updateHeaderStories(transition: transition)
|
||||
|
||||
if let searchContentNode = self.searchContentNode, layout.inVoiceOver != wasInVoiceOver {
|
||||
searchContentNode.updateListVisibleContentOffset(.known(0.0))
|
||||
self.chatListDisplayNode.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
|
|
@ -2457,35 +2514,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
|
||||
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: layout.size.width, height: 46.0)))
|
||||
|
||||
if let storyListState = self.storyListState, !storyListState.itemSets.isEmpty {
|
||||
var storyPeerListTransition = Transition(transition)
|
||||
let storyPeerListView: ComponentView<Empty>
|
||||
if let current = self.storyPeerListView {
|
||||
storyPeerListView = current
|
||||
} else {
|
||||
storyPeerListTransition = .immediate
|
||||
storyPeerListView = ComponentView()
|
||||
self.storyPeerListView = storyPeerListView
|
||||
}
|
||||
let storyListFrame = CGRect(origin: CGPoint(x: 0.0, y: 46.0 - 0.0), size: CGSize(width: layout.size.width, height: self.storyListHeight + 0.0))
|
||||
self.updateStoryPeerListView(size: storyListFrame.size, transition: storyPeerListTransition)
|
||||
if let componentView = storyPeerListView.view {
|
||||
if componentView.superview == nil {
|
||||
componentView.alpha = 0.0
|
||||
self.navigationSecondaryContentNode.view.addSubview(componentView)
|
||||
}
|
||||
storyPeerListTransition.setFrame(view: componentView, frame: storyListFrame)
|
||||
transition.updateAlpha(layer: componentView.layer, alpha: 1.0)
|
||||
}
|
||||
} else if let storyPeerListView = self.storyPeerListView {
|
||||
self.storyPeerListView = nil
|
||||
if let componentView = storyPeerListView.view {
|
||||
transition.updateAlpha(layer: componentView.layer, alpha: 0.0, completion: { [weak componentView] _ in
|
||||
componentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !skipTabContainerUpdate {
|
||||
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
|
@ -2930,7 +2958,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
strongSelf.didSetupTabs = true
|
||||
|
||||
if strongSelf.displayNavigationBar {
|
||||
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + strongSelf.storyListHeight
|
||||
strongSelf.navigationBar?.secondaryContentHeight = (!isEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
if case .chatList(.root) = strongSelf.location {
|
||||
strongSelf.searchContentNode?.additionalHeight = 94.0
|
||||
}
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(strongSelf.navigationSecondaryContentNode, animated: false)
|
||||
}
|
||||
|
||||
|
|
@ -3309,6 +3340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
strongSelf.navigationBar?.secondaryContentHeight = NavigationBar.defaultSecondaryContentHeight
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
||||
}
|
||||
strongSelf.searchContentNode?.additionalHeight = 0.0
|
||||
|
||||
activate(filter != .downloads)
|
||||
|
||||
|
|
@ -3362,7 +3394,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
filterContainerNode = searchContentNode.filterContainerNode
|
||||
|
||||
if let filterContainerNode = filterContainerNode, let snapshotView = filterContainerNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = filterContainerNode.frame
|
||||
snapshotView.frame = filterContainerNode.frame//.offsetBy(dx: self.navigationSecondaryContentNode.frame.minX, dy: self.navigationSecondaryContentNode.frame.minY)
|
||||
filterContainerNode.view.superview?.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
|
|
@ -3379,10 +3411,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
let previousFrame = searchContentNode.placeholderNode.frame
|
||||
if case .chatList(.root) = self.location {
|
||||
searchContentNode.placeholderNode.frame = previousFrame.offsetBy(dx: 0.0, dy: 94.0)
|
||||
}
|
||||
completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
|
||||
searchContentNode.placeholderNode.frame = previousFrame
|
||||
}
|
||||
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0) + self.storyListHeight
|
||||
self.navigationBar?.secondaryContentHeight = (!tabsIsEmpty ? NavigationBar.defaultSecondaryContentHeight : 0.0)
|
||||
if case .chatList(.root) = self.location {
|
||||
self.searchContentNode?.additionalHeight = 94.0
|
||||
}
|
||||
self.navigationBar?.setSecondaryContentNode(self.navigationSecondaryContentNode, animated: false)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
|
|
|
|||
|
|
@ -373,6 +373,10 @@ private final class ChatListContainerItemNode: ASDisplayNode {
|
|||
|
||||
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode)
|
||||
|
||||
if let controller, case .chatList(groupId: .root) = controller.location {
|
||||
self.listNode.scrollHeightTopInset = navigationBarSearchContentHeight + 94.0
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
|
|
@ -1481,7 +1485,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||
private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
|
||||
private(set) var inlineStackContainerNode: ChatListContainerNode?
|
||||
private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
|
||||
private(set) var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
|
||||
var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
var navigationBar: NavigationBar?
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,12 @@ public final class ChatListNode: ListView {
|
|||
private var pollFilterUpdatesDisposable: Disposable?
|
||||
private var chatFilterUpdatesDisposable: Disposable?
|
||||
|
||||
public var scrollHeightTopInset: CGFloat {
|
||||
didSet {
|
||||
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
|
||||
self.context = context
|
||||
self.location = location
|
||||
|
|
@ -1204,12 +1210,14 @@ public final class ChatListNode: ListView {
|
|||
|
||||
self.theme = theme
|
||||
|
||||
self.scrollHeightTopInset = navigationBarSearchContentHeight
|
||||
|
||||
super.init()
|
||||
|
||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||
|
||||
self.keepMinimalScrollHeightWithTopInset = navigationBarSearchContentHeight
|
||||
self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset
|
||||
|
||||
let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in
|
||||
if let strongSelf = self, let activateSearch = strongSelf.activateSearch {
|
||||
|
|
@ -3140,7 +3148,7 @@ public final class ChatListNode: ListView {
|
|||
case let .known(value) where abs(value) < .ulpOfOne:
|
||||
offset = 0.0
|
||||
default:
|
||||
offset = -navigationBarSearchContentHeight
|
||||
offset = -self.scrollHeightTopInset
|
||||
}
|
||||
}
|
||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||
|
|
@ -3153,7 +3161,7 @@ public final class ChatListNode: ListView {
|
|||
|
||||
var isNavigationHidden: Bool {
|
||||
switch self.visibleContentOffset() {
|
||||
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
|
||||
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
|
||||
return false
|
||||
case .none:
|
||||
return false
|
||||
|
|
@ -3165,11 +3173,11 @@ public final class ChatListNode: ListView {
|
|||
var isNavigationInAFinalState: Bool {
|
||||
switch self.visibleContentOffset() {
|
||||
case let .known(value):
|
||||
if value < navigationBarSearchContentHeight - 1.0 {
|
||||
if value < self.scrollHeightTopInset - 1.0 {
|
||||
if abs(value - 0.0) < 1.0 {
|
||||
return true
|
||||
}
|
||||
if abs(value - navigationBarSearchContentHeight) < 1.0 {
|
||||
if abs(value - self.scrollHeightTopInset) < 1.0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -3187,9 +3195,9 @@ public final class ChatListNode: ListView {
|
|||
}
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
switch self.visibleContentOffset() {
|
||||
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
|
||||
case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0:
|
||||
if isNavigationHidden {
|
||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(-self.scrollHeightTopInset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||
}
|
||||
default:
|
||||
if !isNavigationHidden {
|
||||
|
|
|
|||
|
|
@ -1395,7 +1395,6 @@ open class NavigationBar: ASDisplayNode {
|
|||
|
||||
if let customHeaderContentView = self.customHeaderContentView {
|
||||
let headerSize = CGSize(width: size.width, height: nominalHeight)
|
||||
//customHeaderContentView.update(size: headerSize, transition: transition)
|
||||
transition.updateFrame(view: customHeaderContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOrigin), size: headerSize))
|
||||
}
|
||||
|
||||
|
|
@ -1732,12 +1731,15 @@ open class NavigationBar: ASDisplayNode {
|
|||
if let result = self.additionalContentNode.view.hitTest(self.view.convert(point, to: self.additionalContentNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
if self.frame.minY > -10.0, let customHeaderContentView = self.customHeaderContentView, let result = customHeaderContentView.hitTest(self.view.convert(point, to: customHeaderContentView), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,9 @@ private final class FetchManagerCategoryContext {
|
|||
previousPriorityKey = nil
|
||||
let (mediaReference, resourceReference, statsCategory, episode) = takeNew()
|
||||
entry = FetchManagerLocationEntry(id: id, episode: episode, mediaReference: mediaReference, resourceReference: resourceReference, statsCategory: statsCategory)
|
||||
|
||||
Logger.shared.log("FetchManager", "[\(entry.id.location)] Adding entry \(entry.resourceReference.resource.id.stringRepresentation) (\(self.entries.count) in queue)")
|
||||
|
||||
self.entries[id] = entry
|
||||
} else {
|
||||
return
|
||||
|
|
@ -236,7 +239,7 @@ private final class FetchManagerCategoryContext {
|
|||
}
|
||||
activeContext.disposable?.dispose()
|
||||
let postbox = self.postbox
|
||||
Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
|
||||
Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
|
||||
|
||||
var userLocation: MediaResourceUserLocation = .other
|
||||
switch entry.id.location {
|
||||
|
|
@ -401,7 +404,7 @@ private final class FetchManagerCategoryContext {
|
|||
} else if ranges.isEmpty {
|
||||
} else {
|
||||
let postbox = self.postbox
|
||||
Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
|
||||
Logger.shared.log("FetchManager", "[\(entry.id.location)] Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))")
|
||||
|
||||
var userLocation: MediaResourceUserLocation = .other
|
||||
switch entry.id.location {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
|
|||
private var disabledOverlay: ASDisplayNode?
|
||||
|
||||
public private(set) var expansionProgress: CGFloat = 1.0
|
||||
|
||||
public var additionalHeight: CGFloat = 0.0
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
|
||||
|
|
@ -125,7 +127,8 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
|
|||
let (searchBarHeight, searchBarApply) = searchBarNodeLayout(placeholderString, compactPlaceholderString, CGSize(width: baseWidth, height: fieldHeight), visibleProgress, textColor, fillColor, backgroundColor, transition)
|
||||
searchBarApply()
|
||||
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: 8.0 + overscrollProgress * fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight))
|
||||
let _ = overscrollProgress
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: size.height + (1.0 - visibleProgress) * fieldHeight - 8.0 - fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight))
|
||||
transition.updateFrame(node: self.placeholderNode, frame: searchBarFrame)
|
||||
|
||||
self.placeholderHeight = searchBarHeight
|
||||
|
|
@ -151,7 +154,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode {
|
|||
}
|
||||
|
||||
override public var nominalHeight: CGFloat {
|
||||
return navigationBarSearchContentHeight
|
||||
return navigationBarSearchContentHeight + self.additionalHeight
|
||||
}
|
||||
|
||||
override public var mode: NavigationBarContentMode {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import TelegramApi
|
|||
|
||||
public enum EngineStoryInputMedia {
|
||||
case image(dimensions: PixelDimensions, data: Data)
|
||||
case video(dimensions: PixelDimensions, duration: Int, resource: TelegramMediaResource)
|
||||
}
|
||||
|
||||
public struct EngineStoryPrivacy: Equatable {
|
||||
|
|
@ -23,7 +24,7 @@ public struct EngineStoryPrivacy: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
switch media {
|
||||
case let .image(dimensions, data):
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
|
|
@ -101,7 +102,16 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, priva
|
|||
case let .content(content):
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: privacyRules))
|
||||
var flags: Int32 = 0
|
||||
var inputEntities: [Api.MessageEntity]?
|
||||
if let text = text, !text.isEmpty {
|
||||
flags |= (1 << 0)
|
||||
if let entities = entities, !entities.isEmpty {
|
||||
flags |= (1 << 1)
|
||||
inputEntities = apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())
|
||||
}
|
||||
}
|
||||
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: text, entities: inputEntities, privacyRules: privacyRules))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
|
@ -141,6 +151,124 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, priva
|
|||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
case let .video(dimensions, duration, resource):
|
||||
let fileMedia = TelegramMediaFile(
|
||||
fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: MediaId.Id.random(in: MediaId.Id.min ... MediaId.Id.max)),
|
||||
partialReference: nil,
|
||||
resource: resource,
|
||||
previewRepresentations: [],
|
||||
videoThumbnails: [],
|
||||
immediateThumbnailData: nil,
|
||||
mimeType: "video/mp4",
|
||||
size: nil,
|
||||
attributes: [
|
||||
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming)
|
||||
]
|
||||
)
|
||||
|
||||
let contentToUpload = messageContentToUpload(
|
||||
network: account.network,
|
||||
postbox: account.postbox,
|
||||
auxiliaryMethods: account.auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [fileMedia]
|
||||
)
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>
|
||||
switch contentToUpload {
|
||||
case let .immediate(result, _):
|
||||
contentSignal = .single(result)
|
||||
case let .signal(signal, _):
|
||||
contentSignal = signal
|
||||
}
|
||||
|
||||
return contentSignal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var privacyRules: [Api.InputPrivacyRule]
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyRules = [.inputPrivacyValueAllowAll]
|
||||
case .contacts:
|
||||
privacyRules = [.inputPrivacyValueAllowContacts]
|
||||
case .closeFriends:
|
||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||
}
|
||||
var privacyUsers: [Api.InputUser] = []
|
||||
var privacyChats: [Int64] = []
|
||||
for peerId in privacy.additionallyIncludePeers {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
if let _ = peer as? TelegramUser {
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
privacyUsers.append(inputUser)
|
||||
}
|
||||
} else if peer is TelegramGroup || peer is TelegramChannel {
|
||||
privacyChats.append(peer.id.id._internalGetInt64Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !privacyUsers.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
|
||||
}
|
||||
if !privacyChats.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .content(content):
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
return account.network.request(Api.functions.stories.sendStory(flags: 0, media: inputMedia, caption: nil, entities: nil, privacyRules: privacyRules))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStories(stories) = update {
|
||||
switch stories {
|
||||
case .userStories(let userId, let apiStories), .userStoriesShort(let userId, let apiStories, _):
|
||||
if PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) == account.peerId, apiStories.count == 1 {
|
||||
switch apiStories[0] {
|
||||
case let .storyItem(_, _, _, _, _, media, _, _, _):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: fileMedia, to: parsedMedia, postbox: account.postbox, force: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,14 +107,23 @@ public final class StoryListContext {
|
|||
|
||||
public struct State: Equatable {
|
||||
public var itemSets: [PeerItemSet]
|
||||
public var uploadProgress: CGFloat?
|
||||
public var loadMoreToken: LoadMoreToken?
|
||||
|
||||
public init(itemSets: [PeerItemSet], loadMoreToken: LoadMoreToken?) {
|
||||
public init(itemSets: [PeerItemSet], uploadProgress: CGFloat?, loadMoreToken: LoadMoreToken?) {
|
||||
self.itemSets = itemSets
|
||||
self.uploadProgress = uploadProgress
|
||||
self.loadMoreToken = loadMoreToken
|
||||
}
|
||||
}
|
||||
|
||||
private final class UploadContext {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
private let queue: Queue
|
||||
private let account: Account
|
||||
|
|
@ -127,6 +136,12 @@ public final class StoryListContext {
|
|||
private var updatesDisposable: Disposable?
|
||||
private var peerDisposables: [PeerId: Disposable] = [:]
|
||||
|
||||
private var uploadContexts: [UploadContext] = [] {
|
||||
didSet {
|
||||
self.stateValue.uploadProgress = self.uploadContexts.isEmpty ? nil : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private var stateValue: State {
|
||||
didSet {
|
||||
self.state.set(.single(self.stateValue))
|
||||
|
|
@ -139,9 +154,21 @@ public final class StoryListContext {
|
|||
self.account = account
|
||||
self.scope = scope
|
||||
|
||||
self.stateValue = State(itemSets: [], loadMoreToken: LoadMoreToken(value: nil))
|
||||
self.stateValue = State(itemSets: [], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
|
||||
self.state.set(.single(self.stateValue))
|
||||
|
||||
let _ = (account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(account.peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
self.stateValue = State(itemSets: [
|
||||
PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0)
|
||||
], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
|
||||
})
|
||||
|
||||
self.updatesDisposable = (account.stateManager.storyUpdates
|
||||
|> deliverOn(queue)).start(next: { [weak self] updates in
|
||||
if updates.isEmpty {
|
||||
|
|
@ -150,6 +177,11 @@ public final class StoryListContext {
|
|||
|
||||
let _ = account.postbox.transaction({ transaction -> [PeerId: Peer] in
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
|
||||
if let peer = transaction.getPeer(account.peerId) {
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .added(peerId, _):
|
||||
|
|
@ -198,7 +230,21 @@ public final class StoryListContext {
|
|||
if let index = items.firstIndex(where: { $0.id == item.id }) {
|
||||
items.remove(at: index)
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
if peerId == self.account.peerId {
|
||||
items.append(Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: item.media,
|
||||
isSeen: false,
|
||||
seenCount: item.seenCount,
|
||||
seenPeers: item.seenPeers,
|
||||
privacy: item.privacy
|
||||
))
|
||||
} else {
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
items.sort(by: { lhsItem, rhsItem in
|
||||
if lhsItem.timestamp != rhsItem.timestamp {
|
||||
return lhsItem.timestamp < rhsItem.timestamp
|
||||
|
|
@ -263,6 +309,12 @@ public final class StoryListContext {
|
|||
return lhsItem.id > rhsItem.id
|
||||
})
|
||||
|
||||
if !itemSets.contains(where: { $0.peerId == self.account.peerId }) {
|
||||
if let peer = peers[self.account.peerId] {
|
||||
itemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
self.stateValue.itemSets = itemSets
|
||||
})
|
||||
})
|
||||
|
|
@ -398,6 +450,21 @@ public final class StoryListContext {
|
|||
}
|
||||
}
|
||||
|
||||
func upload(media: EngineStoryInputMedia, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy) {
|
||||
let uploadContext = UploadContext()
|
||||
self.uploadContexts.append(uploadContext)
|
||||
uploadContext.disposable.set((_internal_uploadStory(account: self.account, media: media, text: text, entities: entities, privacy: privacy)
|
||||
|> deliverOn(self.queue)).start(next: { _ in
|
||||
}, completed: { [weak self, weak uploadContext] in
|
||||
guard let `self` = self, let uploadContext = uploadContext else {
|
||||
return
|
||||
}
|
||||
if let index = self.uploadContexts.firstIndex(where: { $0 === uploadContext }) {
|
||||
self.uploadContexts.remove(at: index)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func loadMore(refresh: Bool) {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
|
|
@ -528,6 +595,12 @@ public final class StoryListContext {
|
|||
}
|
||||
}
|
||||
|
||||
if !parsedItemSets.contains(where: { $0.peerId == account.peerId }) {
|
||||
if let peer = transaction.getPeer(account.peerId) {
|
||||
parsedItemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), items: [], totalCount: 0), at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) })
|
||||
}
|
||||
}
|
||||
|
|
@ -589,7 +662,7 @@ public final class StoryListContext {
|
|||
return lhsItem.id > rhsItem.id
|
||||
})
|
||||
|
||||
self.stateValue = State(itemSets: itemSets, loadMoreToken: result.1)
|
||||
self.stateValue = State(itemSets: itemSets, uploadProgress: self.stateValue.uploadProgress, loadMoreToken: result.1)
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -639,4 +712,10 @@ public final class StoryListContext {
|
|||
impl.delete(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
public func upload(media: EngineStoryInputMedia, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy) {
|
||||
self.impl.with { impl in
|
||||
impl.upload(media: media, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -577,8 +577,8 @@ public extension TelegramEngine {
|
|||
return StoryListContext(account: self.account, scope: .peer(id))
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, privacy: privacy)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String?, entities: [MessageTextEntity]?, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStory(id: Int64) -> Signal<Never, NoError> {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||
|
||||
let context: AccountContext
|
||||
let camera: Camera
|
||||
let changeMode: ActionSlot<CameraMode>
|
||||
let present: (ViewController) -> Void
|
||||
let push: (ViewController) -> Void
|
||||
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
|
|
@ -73,12 +74,14 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||
init(
|
||||
context: AccountContext,
|
||||
camera: Camera,
|
||||
changeMode: ActionSlot<CameraMode>,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
push: @escaping (ViewController) -> Void,
|
||||
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
) {
|
||||
self.context = context
|
||||
self.camera = camera
|
||||
self.changeMode = changeMode
|
||||
self.present = present
|
||||
self.push = push
|
||||
self.completion = completion
|
||||
|
|
@ -251,6 +254,10 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||
|
||||
let topControlInset: CGFloat = 20.0
|
||||
|
||||
component.changeMode.connect({ [weak state] mode in
|
||||
state?.updateCameraMode(mode)
|
||||
})
|
||||
|
||||
if case .none = state.cameraState.recording {
|
||||
let cancelButton = cancelButton.update(
|
||||
component: CameraButton(
|
||||
|
|
@ -574,11 +581,11 @@ public class CameraScreen: ViewController {
|
|||
fileprivate let camera: Camera
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var changingPositionDisposable: Disposable?
|
||||
|
||||
private let changeMode = ActionSlot<CameraMode>()
|
||||
private let completion = ActionSlot<Signal<CameraScreen.Result, NoError>>()
|
||||
|
||||
private var effectivePreviewView: UIView {
|
||||
|
|
@ -728,18 +735,66 @@ public class CameraScreen: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
private var panStartLocation: CGPoint?
|
||||
private var panTranslation: CGFloat?
|
||||
private var previewInitialPosition: CGPoint?
|
||||
private var controlsInitialPosition: CGPoint?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.panStartLocation = location
|
||||
self.panTranslation = nil
|
||||
self.previewInitialPosition = self.previewContainerView.center
|
||||
self.controlsInitialPosition = self.componentHost.view?.center
|
||||
case .changed:
|
||||
guard let _ = self.panStartLocation else {
|
||||
return
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
if !"".isEmpty {
|
||||
|
||||
} else {
|
||||
if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil {
|
||||
self.changeMode.invoke(translation.x > 0.0 ? .photo : .video)
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
} else if translation.y > 10.0 {
|
||||
self.panTranslation = translation.y
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y)
|
||||
}
|
||||
|
||||
if self.backgroundEffectView.isHidden {
|
||||
self.backgroundEffectView.isHidden = false
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = nil
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||
if velocity.y > 1000.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else {
|
||||
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
transition.setPosition(view: self.previewContainerView, position: previewInitialPosition)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
transition.setPosition(view: view, position: controlsInitialPosition)
|
||||
}
|
||||
if !self.backgroundEffectView.isHidden {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
}, completion: { _ in
|
||||
self.backgroundEffectView.isHidden = true
|
||||
})
|
||||
}
|
||||
}
|
||||
// let translation = location.y - panStartLocation.y
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -903,6 +958,7 @@ public class CameraScreen: ViewController {
|
|||
CameraScreenComponent(
|
||||
context: self.context,
|
||||
camera: self.camera,
|
||||
changeMode: self.changeMode,
|
||||
present: { [weak self] c in
|
||||
self?.controller?.present(c, in: .window(.root))
|
||||
},
|
||||
|
|
@ -984,8 +1040,8 @@ public class CameraScreen: ViewController {
|
|||
guard !self.isDismissed else {
|
||||
return
|
||||
}
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.isDismissed = true
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
if animated {
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
|
|
@ -998,6 +1054,8 @@ public class CameraScreen: ViewController {
|
|||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
||||
if !self.isDismissed {
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ swift_library(
|
|||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/ChatListTitleView",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/AnimationUI",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import TelegramPresentationData
|
|||
import AccountContext
|
||||
import ChatListTitleView
|
||||
import AppBundle
|
||||
import StoryPeerListComponent
|
||||
import TelegramCore
|
||||
|
||||
public final class HeaderNetworkStatusComponent: Component {
|
||||
public enum Content: Equatable {
|
||||
|
|
@ -272,10 +274,13 @@ public final class ChatListHeaderComponent: Component {
|
|||
var backButtonView: BackButtonView?
|
||||
|
||||
let titleOffsetContainer: UIView
|
||||
let titleScaleContainer: UIView
|
||||
let titleTextView: ImmediateTextView
|
||||
var titleContentView: ComponentView<Empty>?
|
||||
var chatListTitleView: ChatListTitleView?
|
||||
|
||||
var contentOffsetFraction: CGFloat = 0.0
|
||||
|
||||
init(
|
||||
backPressed: @escaping () -> Void,
|
||||
openStatusSetup: @escaping (UIView) -> Void,
|
||||
|
|
@ -288,16 +293,18 @@ public final class ChatListHeaderComponent: Component {
|
|||
self.leftButtonOffsetContainer = UIView()
|
||||
self.rightButtonOffsetContainer = UIView()
|
||||
self.titleOffsetContainer = UIView()
|
||||
self.titleScaleContainer = UIView()
|
||||
|
||||
self.titleTextView = ImmediateTextView()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.titleOffsetContainer)
|
||||
self.titleOffsetContainer.addSubview(self.titleScaleContainer)
|
||||
self.addSubview(self.leftButtonOffsetContainer)
|
||||
self.addSubview(self.rightButtonOffsetContainer)
|
||||
|
||||
self.titleOffsetContainer.addSubview(self.titleTextView)
|
||||
self.titleScaleContainer.addSubview(self.titleTextView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -329,10 +336,28 @@ public final class ChatListHeaderComponent: Component {
|
|||
return nil
|
||||
}
|
||||
|
||||
func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: Transition) {
|
||||
if self.contentOffsetFraction == contentOffsetFraction {
|
||||
return
|
||||
}
|
||||
self.contentOffsetFraction = contentOffsetFraction
|
||||
|
||||
let scale = 1.0 * (1.0 - contentOffsetFraction) + 0.001 * contentOffsetFraction
|
||||
let translation = -44.0 * contentOffsetFraction * 0.5
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DTranslate(transform, 0.0, translation, 0.0)
|
||||
transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform)
|
||||
|
||||
transition.setScale(view: self.titleScaleContainer, scale: scale)
|
||||
transition.setAlpha(view: self.titleScaleContainer, alpha: 1.0 - contentOffsetFraction)
|
||||
}
|
||||
|
||||
func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
|
||||
transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
|
||||
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
|
||||
|
||||
if let backButtonView = self.backButtonView {
|
||||
|
|
@ -356,6 +381,7 @@ public final class ChatListHeaderComponent: Component {
|
|||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0))
|
||||
transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size))
|
||||
if let backButtonView = self.backButtonView {
|
||||
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
|
||||
|
|
@ -373,7 +399,45 @@ public final class ChatListHeaderComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
|
||||
transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in
|
||||
})
|
||||
transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
|
||||
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
if let backButtonView = self.backButtonView {
|
||||
transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size))
|
||||
transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0))
|
||||
}
|
||||
|
||||
func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) {
|
||||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0))
|
||||
transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size))
|
||||
if let backButtonView = self.backButtonView {
|
||||
transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0))
|
||||
transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0))
|
||||
|
||||
transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size))
|
||||
transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0))
|
||||
}
|
||||
}
|
||||
|
||||
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, size: CGSize, transition: Transition) {
|
||||
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
|
||||
|
||||
transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size))
|
||||
|
||||
self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||
|
||||
let buttonSpacing: CGFloat = 8.0
|
||||
|
|
@ -530,7 +594,7 @@ public final class ChatListHeaderComponent: Component {
|
|||
)
|
||||
if let titleContentComponentView = titleContentView.view {
|
||||
if titleContentComponentView.superview == nil {
|
||||
self.titleOffsetContainer.addSubview(titleContentComponentView)
|
||||
self.titleScaleContainer.addSubview(titleContentComponentView)
|
||||
}
|
||||
titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize))
|
||||
}
|
||||
|
|
@ -551,7 +615,7 @@ public final class ChatListHeaderComponent: Component {
|
|||
chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer)
|
||||
chatListTitleView.manualLayout = true
|
||||
self.chatListTitleView = chatListTitleView
|
||||
self.titleOffsetContainer.addSubview(chatListTitleView)
|
||||
self.titleScaleContainer.addSubview(chatListTitleView)
|
||||
}
|
||||
|
||||
let chatListTitleContentSize = size
|
||||
|
|
@ -591,6 +655,10 @@ public final class ChatListHeaderComponent: Component {
|
|||
|
||||
private var primaryContentView: ContentView?
|
||||
private var secondaryContentView: ContentView?
|
||||
private var storyOffsetFraction: CGFloat = 0.0
|
||||
|
||||
private var storyPeerList: ComponentView<Empty>?
|
||||
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
||||
|
||||
private var effectiveContentView: ContentView? {
|
||||
return self.secondaryContentView ?? self.primaryContentView
|
||||
|
|
@ -598,6 +666,8 @@ public final class ChatListHeaderComponent: Component {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.storyOffsetFraction = 1.0
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
|
|
@ -643,6 +713,107 @@ public final class ChatListHeaderComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
public func storyPeerListView() -> StoryPeerListComponent.View? {
|
||||
return self.storyPeerList?.view as? StoryPeerListComponent.View
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for subview in self.subviews.reversed() {
|
||||
if !subview.isUserInteractionEnabled || subview.alpha < 0.01 || subview.isHidden {
|
||||
continue
|
||||
}
|
||||
if subview === self.storyPeerList?.view {
|
||||
continue
|
||||
}
|
||||
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if let storyPeerListView = self.storyPeerList?.view {
|
||||
if let result = storyPeerListView.hitTest(self.convert(point, to: storyPeerListView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let defaultResult = super.hitTest(point, with: event)
|
||||
|
||||
if let defaultResult, defaultResult !== self {
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
public func updateStories(offset: CGFloat, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, storyListState: StoryListContext.State?, transition: Transition) {
|
||||
var storyOffsetFraction: CGFloat = 1.0
|
||||
if let storyListState, storyListState.itemSets.count > 1 {
|
||||
storyOffsetFraction = offset
|
||||
}
|
||||
|
||||
self.storyOffsetFraction = storyOffsetFraction
|
||||
|
||||
let storyPeerList: ComponentView<Empty>
|
||||
var storyListTransition = transition
|
||||
if let current = self.storyPeerList {
|
||||
storyPeerList = current
|
||||
} else {
|
||||
storyListTransition = .immediate
|
||||
storyPeerList = ComponentView()
|
||||
self.storyPeerList = storyPeerList
|
||||
}
|
||||
|
||||
if !self.bounds.isEmpty {
|
||||
let _ = storyPeerList.update(
|
||||
transition: storyListTransition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
context: context,
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
state: storyListState,
|
||||
collapseFraction: 1.0 - offset,
|
||||
peerAction: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.storyPeerAction?(peer)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: self.bounds.width, height: 94.0)
|
||||
)
|
||||
if let storyPeerListComponentView = storyPeerList.view {
|
||||
if storyPeerListComponentView.superview == nil {
|
||||
self.addSubview(storyPeerListComponentView)
|
||||
}
|
||||
|
||||
let storyPeerListMinOffset: CGFloat = -7.0
|
||||
let storyPeerListMaxOffset: CGFloat = self.bounds.height + 8.0
|
||||
|
||||
let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - offset) + storyPeerListMaxOffset * offset
|
||||
|
||||
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: storyPeerListPosition), size: CGSize(width: self.bounds.width, height: 94.0)))
|
||||
|
||||
var storyListAlpha: CGFloat = 1.0
|
||||
if let storyListState, storyListState.itemSets.count > 1 {
|
||||
} else {
|
||||
storyListAlpha = offset
|
||||
}
|
||||
storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha)
|
||||
}
|
||||
|
||||
if let primaryContentView = self.primaryContentView {
|
||||
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition)
|
||||
}
|
||||
if let secondaryContentView = self.secondaryContentView {
|
||||
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateContentStoryOffsets(transition: Transition) {
|
||||
}
|
||||
|
||||
func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.state = state
|
||||
|
||||
|
|
@ -681,6 +852,8 @@ public final class ChatListHeaderComponent: Component {
|
|||
}
|
||||
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, size: availableSize, transition: primaryContentTransition)
|
||||
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
|
||||
} else if let primaryContentView = self.primaryContentView {
|
||||
self.primaryContentView = nil
|
||||
primaryContentView.removeFromSuperview()
|
||||
|
|
@ -719,23 +892,42 @@ public final class ChatListHeaderComponent: Component {
|
|||
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, size: availableSize, transition: secondaryContentTransition)
|
||||
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
|
||||
|
||||
if let primaryContentView = self.primaryContentView {
|
||||
if let previousComponent = previousComponent, previousComponent.secondaryContent == nil {
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
if self.storyOffsetFraction < 0.8 {
|
||||
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
} else {
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
if self.storyOffsetFraction < 0.8 {
|
||||
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
} else {
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {})
|
||||
}
|
||||
}
|
||||
} else if let secondaryContentView = self.secondaryContentView {
|
||||
self.secondaryContentView = nil
|
||||
|
||||
if let primaryContentView = self.primaryContentView {
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
|
||||
secondaryContentView?.removeFromSuperview()
|
||||
})
|
||||
if self.storyOffsetFraction < 0.8 {
|
||||
primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
|
||||
secondaryContentView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {})
|
||||
secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in
|
||||
secondaryContentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
secondaryContentView.removeFromSuperview()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,20 @@ fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]],
|
|||
half3 color = texture.sample(samplr, float2(x, y)).rgb;
|
||||
return half4(color, 1.0);
|
||||
}
|
||||
|
||||
fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
|
||||
constant float2 &texCoordScales [[buffer(0)]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]]) {
|
||||
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
|
||||
|
||||
float scaleX = texCoordScales.x;
|
||||
float scaleY = texCoordScales.y;
|
||||
float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX;
|
||||
float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY;
|
||||
if (x < 0 || x > 1 || y < 0 || y > 1) {
|
||||
return 0.0;
|
||||
}
|
||||
half3 color = texture.sample(samplr, float2(x, y)).rgb;
|
||||
half luma = color.r * 0.3 + color.g * 0.59 + color.b * 0.11;
|
||||
return luma;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,22 @@ import Metal
|
|||
import simd
|
||||
import MetalPerformanceShaders
|
||||
|
||||
final class HistogramCalculationPass: RenderPass {
|
||||
fileprivate var histogramInfoBuffer: MTLBuffer?
|
||||
final class HistogramCalculationPass: DefaultRenderPass {
|
||||
fileprivate var cachedTexture: MTLTexture?
|
||||
fileprivate var histogramBuffer: MTLBuffer?
|
||||
fileprivate var calculation: MPSImageHistogram?
|
||||
|
||||
var updated: ((Data) -> Void)?
|
||||
|
||||
func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
override var fragmentShaderFunctionName: String {
|
||||
return "histogramPrepareFragmentShader"
|
||||
}
|
||||
|
||||
override var pixelFormat: MTLPixelFormat {
|
||||
return .r8Unorm
|
||||
}
|
||||
|
||||
override func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
var histogramInfo = MPSImageHistogramInfo(
|
||||
numberOfHistogramEntries: 256,
|
||||
histogramForAlpha: false,
|
||||
|
|
@ -18,22 +27,71 @@ final class HistogramCalculationPass: RenderPass {
|
|||
)
|
||||
|
||||
let calculation = MPSImageHistogram(device: device, histogramInfo: &histogramInfo)
|
||||
calculation.zeroHistogram = false
|
||||
calculation.zeroHistogram = true
|
||||
|
||||
let histogramBufferLength = calculation.histogramSize(forSourceFormat: .bgra8Unorm)
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
|
||||
let bufferLength = calculation.histogramSize(forSourceFormat: .bgra8Unorm)
|
||||
|
||||
if let histogramInfoBuffer = device.makeBuffer(length: bufferLength, options: [.storageModeShared]) {
|
||||
if let histogramBuffer = device.makeBuffer(length: histogramBufferLength + lumaHistogramBufferLength, options: [.storageModeShared]) {
|
||||
self.calculation = calculation
|
||||
self.histogramInfoBuffer = histogramInfoBuffer
|
||||
self.histogramBuffer = histogramBuffer
|
||||
}
|
||||
|
||||
super.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
if let histogramInfoBuffer = self.histogramInfoBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramInfoBuffer, histogramOffset: 0)
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = .r8Unorm
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "lumaTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0)
|
||||
|
||||
let data = Data(bytes: histogramInfoBuffer.contents(), count: histogramInfoBuffer.length)
|
||||
self.updated?(data)
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength)
|
||||
|
||||
let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length)
|
||||
self.updated?(histogramData)
|
||||
}
|
||||
|
||||
return input
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import CoreImage
|
||||
import Metal
|
||||
import Display
|
||||
|
|
@ -11,14 +12,13 @@ final class MediaEditorComposer {
|
|||
private let dimensions: CGSize
|
||||
|
||||
private let ciContext: CIContext?
|
||||
private var textureCache: CVMetalTextureCache?
|
||||
|
||||
private let renderer = MediaEditorRenderer()
|
||||
private let renderChain = MediaEditorRenderChain()
|
||||
|
||||
private var gradientImage: CIImage
|
||||
|
||||
private var textureCache: CVMetalTextureCache?
|
||||
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
init(values: MediaEditorValues, dimensions: CGSize) {
|
||||
|
|
@ -31,7 +31,7 @@ final class MediaEditorComposer {
|
|||
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
self.gradientImage = CIImage(image: image)!
|
||||
self.gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
self.gradientImage = CIImage(color: .black)
|
||||
}
|
||||
|
|
@ -49,7 +49,6 @@ final class MediaEditorComposer {
|
|||
self.renderChain.update(values: self.values)
|
||||
}
|
||||
|
||||
private var processedPixelBuffer: CVPixelBuffer?
|
||||
func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?) -> CVPixelBuffer? {
|
||||
guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else {
|
||||
return nil
|
||||
|
|
@ -74,7 +73,7 @@ final class MediaEditorComposer {
|
|||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
||||
|
||||
if let composition = makeFrameComposition(inputImage: ciImage), let pixelBuffer {
|
||||
if let composition = processImage(inputImage: ciImage), let pixelBuffer {
|
||||
self.ciContext?.render(composition, to: pixelBuffer)
|
||||
|
||||
return pixelBuffer
|
||||
|
|
@ -89,25 +88,45 @@ final class MediaEditorComposer {
|
|||
}
|
||||
}
|
||||
|
||||
func makeFrameComposition(inputImage: CIImage) -> CIImage? {
|
||||
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: self.dimensions)).transformed(by: CGAffineTransform(translationX: -self.dimensions.width / 2.0, y: -self.dimensions.height / 2.0))
|
||||
resultImage = self.gradientImage.composited(over: resultImage)
|
||||
|
||||
var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
|
||||
var cropTransform = CGAffineTransform(translationX: self.values.cropOffset.x, y: self.values.cropOffset.y * -1.0)
|
||||
cropTransform = cropTransform.rotated(by: -self.values.cropRotation)
|
||||
cropTransform = cropTransform.scaledBy(x: self.values.cropScale, y: self.values.cropScale)
|
||||
mediaImage = mediaImage.transformed(by: cropTransform)
|
||||
|
||||
resultImage = mediaImage.composited(over: resultImage)
|
||||
|
||||
resultImage = resultImage.cropped(to: CGRect(origin: CGPoint(x: -self.dimensions.width / 2.0, y: -self.dimensions.height / 2.0), size: self.dimensions))
|
||||
|
||||
return resultImage
|
||||
func processImage(inputImage: CIImage) -> CIImage? {
|
||||
return makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, dimensions: self.dimensions, values: self.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeEditorImageComposition(inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues) -> UIImage? {
|
||||
let inputImage = CIImage(image: inputImage)!
|
||||
let gradientImage: CIImage
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
gradientImage = CIImage(color: .black)
|
||||
}
|
||||
if let ciImage = makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, dimensions: dimensions, values: values) {
|
||||
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
||||
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, dimensions: CGSize, values: MediaEditorValues) -> CIImage? {
|
||||
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
resultImage = gradientImage.composited(over: resultImage)
|
||||
|
||||
var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
|
||||
var cropTransform = CGAffineTransform(translationX: values.cropOffset.x, y: values.cropOffset.y * -1.0)
|
||||
cropTransform = cropTransform.rotated(by: -values.cropRotation)
|
||||
cropTransform = cropTransform.scaledBy(x: values.cropScale, y: values.cropScale)
|
||||
mediaImage = mediaImage.transformed(by: cropTransform)
|
||||
|
||||
resultImage = mediaImage.composited(over: resultImage)
|
||||
|
||||
return resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0))
|
||||
}
|
||||
|
||||
extension CMSampleBuffer {
|
||||
func newSampleBufferWithReplacedImageBuffer(_ imageBuffer: CVImageBuffer) -> CMSampleBuffer? {
|
||||
guard let _ = CMSampleBufferGetImageBuffer(self) else {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
}
|
||||
}
|
||||
|
||||
private var device: MTLDevice?
|
||||
private var commandQueue: MTLCommandQueue?
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentRotation: TextureRotation = .rotate0Degrees
|
||||
|
|
@ -114,12 +115,11 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
self.outputRenderPass.setup(device: device, library: defaultLibrary)
|
||||
}
|
||||
|
||||
private var currentDevice: MTLDevice?
|
||||
func setupForComposer(composer: MediaEditorComposer) {
|
||||
guard let device = composer.device else {
|
||||
return
|
||||
}
|
||||
self.currentDevice = device
|
||||
self.device = device
|
||||
|
||||
let mainBundle = Bundle(for: MediaEditorRenderer.self)
|
||||
guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else {
|
||||
|
|
@ -144,7 +144,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
let device: MTLDevice?
|
||||
if let renderTarget = self.renderTarget {
|
||||
device = renderTarget.mtlDevice
|
||||
} else if let currentDevice = self.currentDevice {
|
||||
} else if let currentDevice = self.device {
|
||||
device = currentDevice
|
||||
} else {
|
||||
device = nil
|
||||
|
|
@ -223,8 +223,9 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
guard let device = self.renderTarget?.mtlDevice else {
|
||||
return nil
|
||||
}
|
||||
let options = [CIImageOption.colorSpace: CGColorSpaceCreateDeviceRGB()]
|
||||
let context = CIContext(mtlDevice: device)
|
||||
guard var ciImage = CIImage(mtlTexture: texture) else {
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: options) else {
|
||||
return nil
|
||||
}
|
||||
let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
|
|
|
|||
|
|
@ -397,6 +397,18 @@ public extension MediaEditorValues {
|
|||
}
|
||||
|
||||
var requiresComposing: Bool {
|
||||
if abs(1.0 - self.cropScale) > 0.0 {
|
||||
return true
|
||||
}
|
||||
if self.cropOffset != .zero {
|
||||
return true
|
||||
}
|
||||
if abs(self.cropRotation) > 0.0 {
|
||||
return true
|
||||
}
|
||||
if self.cropMirroring {
|
||||
return true
|
||||
}
|
||||
if self.hasAdjustments {
|
||||
return true
|
||||
}
|
||||
|
|
@ -483,6 +495,8 @@ public class MediaEditorHistogram: Equatable {
|
|||
var greenValues: [UInt32] = []
|
||||
var maxBlue: UInt32 = 0
|
||||
var blueValues: [UInt32] = []
|
||||
var maxLuma: UInt32 = 0
|
||||
var lumaValues: [UInt32] = []
|
||||
|
||||
data.withUnsafeBytes { pointer in
|
||||
if let red = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self) {
|
||||
|
|
@ -511,9 +525,18 @@ public class MediaEditorHistogram: Equatable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let luma = pointer.baseAddress?.assumingMemoryBound(to: UInt32.self).advanced(by: count * 3) {
|
||||
for i in 0 ..< count {
|
||||
lumaValues.append(luma[i])
|
||||
if luma[i] > maxLuma {
|
||||
maxLuma = luma[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.luminance = HistogramBins(values: [], max: 0)
|
||||
self.luminance = HistogramBins(values: lumaValues, max: maxLuma)
|
||||
self.red = HistogramBins(values: redValues, max: maxRed)
|
||||
self.green = HistogramBins(values: greenValues, max: maxGreen)
|
||||
self.blue = HistogramBins(values: blueValues, max: maxBlue)
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ final class CurvesComponent: Component {
|
|||
switch state.section {
|
||||
case .all:
|
||||
histogramColor = .white
|
||||
histogramBins = component.histogram?.green
|
||||
histogramBins = component.histogram?.luminance
|
||||
case .red:
|
||||
histogramColor = UIColor(rgb: 0xed3d4c)
|
||||
histogramBins = component.histogram?.red
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
muteButtonView.layer.shadowRadius = 2.0
|
||||
muteButtonView.layer.shadowColor = UIColor.black.cgColor
|
||||
muteButtonView.layer.shadowOpacity = 0.25
|
||||
self.addSubview(muteButtonView)
|
||||
//self.addSubview(muteButtonView)
|
||||
}
|
||||
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
|
||||
transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size))
|
||||
|
|
@ -712,7 +712,6 @@ public final class MediaEditorScreen: ViewController {
|
|||
private let stickerPickerInputData = Promise<StickerPickerInputData>()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(controller: MediaEditorScreen) {
|
||||
|
|
@ -1159,8 +1158,8 @@ public final class MediaEditorScreen: ViewController {
|
|||
}
|
||||
|
||||
public enum Result {
|
||||
case image(UIImage)
|
||||
case video(String, UIImage?, MediaEditorValues)
|
||||
case image(UIImage, NSAttributedString?)
|
||||
case video(String, UIImage?, MediaEditorValues, NSAttributedString?)
|
||||
}
|
||||
|
||||
fileprivate let context: AccountContext
|
||||
|
|
@ -1211,14 +1210,20 @@ public final class MediaEditorScreen: ViewController {
|
|||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
|
||||
self.completion(.video("", nil, mediaEditor.values, nil), { [weak self] in
|
||||
self?.node.animateOut(completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
self.completion(.image(image), { [weak self] in
|
||||
self?.node.animateOut(completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
if let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values) {
|
||||
self.completion(.image(resultImage, nil), { [weak self] in
|
||||
self?.node.animateOut(completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1269,14 +1274,22 @@ public final class MediaEditorScreen: ViewController {
|
|||
|
||||
self.exportDisposable = (export.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let _ = self {
|
||||
if let self {
|
||||
if case .completed = status {
|
||||
self.export = nil
|
||||
saveToPhotos(outputPath, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
if let image = mediaEditor.resultImage {
|
||||
let resultImage = makeEditorImageComposition(inputImage: image, dimensions: storyDimensions, values: mediaEditor.values)
|
||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||
saveToPhotos(outputPath, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
private let bottomGradientView: UIView
|
||||
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
private let vibrancyPlaceholder = ComponentView<Empty>()
|
||||
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
|
|
@ -238,6 +239,17 @@ public final class MessageInputPanelComponent: Component {
|
|||
)
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.placeholder,
|
||||
font: Font.regular(17.0),
|
||||
color: UIColor(rgb: 0xffffff, alpha: 0.3)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
|
||||
let _ = self.vibrancyPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.placeholder,
|
||||
|
|
@ -271,10 +283,18 @@ public final class MessageInputPanelComponent: Component {
|
|||
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
|
||||
}
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: floor((fieldFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
|
||||
if vibrancyPlaceholderView.superview == nil {
|
||||
vibrancyPlaceholderView.layer.anchorPoint = CGPoint()
|
||||
self.vibrancyEffectView.contentView.addSubview(vibrancyPlaceholderView)
|
||||
}
|
||||
transition.setPosition(view: vibrancyPlaceholderView, position: placeholderFrame.origin)
|
||||
vibrancyPlaceholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
placeholderView.layer.anchorPoint = CGPoint()
|
||||
self.vibrancyEffectView.contentView.addSubview(placeholderView)
|
||||
self.fieldBackgroundView.addSubview(placeholderView)
|
||||
}
|
||||
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
|
||||
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
|
|
@ -455,8 +475,9 @@ public final class MessageInputPanelComponent: Component {
|
|||
|
||||
self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing || component.style == .editor ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
||||
transition.setAlpha(view: self.fieldBackgroundView, alpha: hasMediaRecording ? 0.0 : 1.0)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
placeholderView.isHidden = self.textFieldExternalState.hasText
|
||||
if let placeholder = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
|
||||
placeholder.isHidden = self.textFieldExternalState.hasText
|
||||
vibrancyPlaceholderView.isHidden = placeholder.isHidden
|
||||
}
|
||||
|
||||
component.externalState.isEditing = self.textFieldExternalState.isEditing
|
||||
|
|
|
|||
|
|
@ -687,10 +687,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||
|
||||
self.currentSliceDisposable?.dispose()
|
||||
if let focusedItemId = self.focusedItemId {
|
||||
if let item = self.currentSlice?.items.first(where: { $0.id == focusedItemId }) {
|
||||
item.markAsSeen?()
|
||||
}
|
||||
|
||||
self.currentSliceDisposable = (component.initialItemSlice.update(
|
||||
component.initialItemSlice,
|
||||
focusedItemId
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public enum StoryChatContent {
|
|||
position: items.count,
|
||||
component: AnyComponent(StoryItemContentComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
item: item
|
||||
)),
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
|
|
@ -57,7 +58,7 @@ public enum StoryChatContent {
|
|||
var sliceFocusedItemId: AnyHashable?
|
||||
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) {
|
||||
sliceFocusedItemId = AnyHashable(focusItem)
|
||||
} else if itemSet.peerId != context.account.peerId {
|
||||
} else {
|
||||
if let id = itemSet.items.first(where: { !$0.isSeen })?.id {
|
||||
sliceFocusedItemId = AnyHashable(id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ final class StoryItemContentComponent: Component {
|
|||
typealias EnvironmentType = StoryContentItem.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let item: StoryListContext.Item
|
||||
|
||||
init(context: AccountContext, item: StoryListContext.Item) {
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, item: StoryListContext.Item) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.item = item
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +29,9 @@ final class StoryItemContentComponent: Component {
|
|||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
|
|
@ -99,6 +104,9 @@ final class StoryItemContentComponent: Component {
|
|||
private var currentProgressTimerValue: Double = 0.0
|
||||
private var videoProgressDisposable: Disposable?
|
||||
|
||||
private var markedAsSeen: Bool = false
|
||||
private var contentLoaded: Bool = false
|
||||
|
||||
private var videoPlaybackStatus: MediaPlayerStatus?
|
||||
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
|
@ -186,7 +194,7 @@ final class StoryItemContentComponent: Component {
|
|||
|
||||
private func updateIsProgressPaused() {
|
||||
if let videoNode = self.videoNode {
|
||||
if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy {
|
||||
if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy {
|
||||
videoNode.play()
|
||||
} else {
|
||||
videoNode.pause()
|
||||
|
|
@ -198,7 +206,7 @@ final class StoryItemContentComponent: Component {
|
|||
}
|
||||
|
||||
private func updateProgressTimer() {
|
||||
let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy
|
||||
let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||
|
||||
if needsTimer {
|
||||
if self.currentProgressTimer == nil {
|
||||
|
|
@ -206,13 +214,20 @@ final class StoryItemContentComponent: Component {
|
|||
timeout: 1.0 / 60.0,
|
||||
repeat: true,
|
||||
completion: { [weak self] in
|
||||
guard let self, !self.isProgressPaused, self.hierarchyTrackingLayer.isInHierarchy else {
|
||||
guard let self, !self.isProgressPaused, self.contentLoaded, self.hierarchyTrackingLayer.isInHierarchy else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.videoNode != nil {
|
||||
if case .file = self.currentMessageMedia {
|
||||
self.updateVideoPlaybackProgress()
|
||||
} else {
|
||||
if !self.markedAsSeen {
|
||||
self.markedAsSeen = true
|
||||
if let component = self.component {
|
||||
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG && false
|
||||
let currentProgressTimerLimit: Double = 5 * 60.0
|
||||
#else
|
||||
|
|
@ -273,6 +288,15 @@ final class StoryItemContentComponent: Component {
|
|||
progress = min(1.0, progress)
|
||||
|
||||
currentProgress = progress
|
||||
|
||||
if isPlaying {
|
||||
if !self.markedAsSeen {
|
||||
self.markedAsSeen = true
|
||||
if let component = self.component {
|
||||
let _ = component.context.engine.messages.markStoryAsSeen(peerId: component.peerId, id: component.item.id).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clippedProgress = max(0.0, min(1.0, currentProgress))
|
||||
|
|
@ -325,6 +349,8 @@ final class StoryItemContentComponent: Component {
|
|||
}
|
||||
}
|
||||
case let .file(file):
|
||||
self.contentLoaded = true
|
||||
|
||||
signal = chatMessageVideo(
|
||||
postbox: component.context.account.postbox,
|
||||
userLocation: .other,
|
||||
|
|
@ -362,7 +388,15 @@ final class StoryItemContentComponent: Component {
|
|||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
if let fetchSignal {
|
||||
self.fetchDisposable = fetchSignal.start()
|
||||
self.fetchDisposable = (fetchSignal |> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.contentLoaded {
|
||||
self.contentLoaded = true
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +442,8 @@ final class StoryItemContentComponent: Component {
|
|||
})
|
||||
}
|
||||
}
|
||||
self.updateProgressTimer()
|
||||
|
||||
self.updateIsProgressPaused()
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,22 @@ public final class StoryPeerListComponent: Component {
|
|||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let state: StoryListContext.State?
|
||||
public let peerAction: (EnginePeer) -> Void
|
||||
public let collapseFraction: CGFloat
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
state: StoryListContext.State?,
|
||||
peerAction: @escaping (EnginePeer) -> Void
|
||||
collapseFraction: CGFloat,
|
||||
peerAction: @escaping (EnginePeer?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.state = state
|
||||
self.collapseFraction = collapseFraction
|
||||
self.peerAction = peerAction
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +46,9 @@ public final class StoryPeerListComponent: Component {
|
|||
if lhs.state != rhs.state {
|
||||
return false
|
||||
}
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +96,7 @@ public final class StoryPeerListComponent: Component {
|
|||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate {
|
||||
private let collapsedButton: HighlightableButton
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
|
@ -98,10 +105,14 @@ public final class StoryPeerListComponent: Component {
|
|||
private var sortedItemSets: [StoryListContext.PeerItemSet] = []
|
||||
private var visibleItems: [EnginePeer.Id: VisibleItem] = [:]
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryPeerListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.collapsedButton = HighlightableButton()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.delaysContentTouches = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
|
|
@ -116,13 +127,43 @@ public final class StoryPeerListComponent: Component {
|
|||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
self.addSubview(self.collapsedButton)
|
||||
|
||||
self.collapsedButton.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.alpha = 0.6
|
||||
} else {
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.25, completion: { [weak self] finished in
|
||||
guard let self, finished else {
|
||||
return
|
||||
}
|
||||
self.layer.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
}
|
||||
self.collapsedButton.addTarget(self, action: #selector(self.collapsedButtonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func collapsedButtonPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.peerAction(nil)
|
||||
}
|
||||
|
||||
public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? {
|
||||
if self.collapsedButton.isUserInteractionEnabled {
|
||||
return nil
|
||||
}
|
||||
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
|
||||
return itemView.transitionView()
|
||||
}
|
||||
|
|
@ -131,21 +172,106 @@ public final class StoryPeerListComponent: Component {
|
|||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
self.updateScrolling(transition: .immediate, keepVisibleUntilCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
private func updateScrolling(transition: Transition, keepVisibleUntilCompletion: Bool) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var hasStories: Bool = false
|
||||
if let state = component.state, state.itemSets.count > 1 {
|
||||
hasStories = true
|
||||
}
|
||||
|
||||
let titleSpacing: CGFloat = 8.0
|
||||
|
||||
let titleText: String
|
||||
let storyCount = self.sortedItemSets.count - 1
|
||||
if storyCount <= 0 {
|
||||
titleText = "No Stories"
|
||||
} else {
|
||||
if storyCount == 1 {
|
||||
titleText = "1 Story"
|
||||
} else {
|
||||
titleText = "\(storyCount) Stories"
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerSize.width, height: 100.0)
|
||||
)
|
||||
|
||||
let collapseStartIndex = 1
|
||||
|
||||
let collapsedItemWidth: CGFloat = 24.0
|
||||
let collapsedItemDistance: CGFloat = 14.0
|
||||
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItemSets.count - collapseStartIndex, 3))
|
||||
var collapsedContentWidth: CGFloat = 0.0
|
||||
if collapsedItemCount > 0 {
|
||||
collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
|
||||
collapsedContentWidth += titleSpacing
|
||||
}
|
||||
|
||||
let collapseEndIndex = collapseStartIndex + Int(collapsedItemCount)
|
||||
let _ = collapseEndIndex
|
||||
|
||||
let titleOffset = collapsedContentWidth
|
||||
collapsedContentWidth += titleSize.width
|
||||
|
||||
let collapsedContentOrigin: CGFloat
|
||||
let collapsedItemOffsetY: CGFloat
|
||||
let itemScale: CGFloat
|
||||
if hasStories {
|
||||
collapsedContentOrigin = floor((itemLayout.containerSize.width - collapsedContentWidth) * 0.5)
|
||||
itemScale = 1.0
|
||||
collapsedItemOffsetY = 0.0
|
||||
} else {
|
||||
collapsedContentOrigin = itemLayout.frame(at: 0).minX + 30.0
|
||||
itemScale = 1.0//1.0 * (1.0 - component.collapseFraction) + 0.001 * component.collapseFraction
|
||||
collapsedItemOffsetY = 16.0
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: component.collapseFraction * (collapsedContentOrigin + titleOffset) + (1.0 - component.collapseFraction) * (itemLayout.containerSize.width), y: 19.0/* * component.collapseFraction + (1.0 - component.collapseFraction) * (-40.0)*/), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
self.scrollView.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY))
|
||||
transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
|
||||
|
||||
var titleAlpha: CGFloat = pow(component.collapseFraction, 1.5)
|
||||
if !hasStories {
|
||||
titleAlpha = 0.0
|
||||
}
|
||||
transition.setAlpha(view: titleView, alpha: titleAlpha)
|
||||
|
||||
transition.setScale(view: titleView, scale: (component.collapseFraction) * 1.0 + (1.0 - component.collapseFraction) * 0.001)
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds
|
||||
|
||||
var validIds: [EnginePeer.Id] = []
|
||||
for i in 0 ..< self.sortedItemSets.count {
|
||||
let itemSet = self.sortedItemSets[i]
|
||||
guard let peer = itemSet.peer else {
|
||||
continue
|
||||
}
|
||||
|
||||
let regularItemFrame = itemLayout.frame(at: i)
|
||||
if !visibleBounds.intersects(regularItemFrame) {
|
||||
/*if keepVisibleUntilCompletion && self.visibleItems[itemSet.peerId] != nil {
|
||||
} else {*/
|
||||
continue
|
||||
//}
|
||||
}
|
||||
|
||||
validIds.append(itemSet.peerId)
|
||||
|
||||
let visibleItem: VisibleItem
|
||||
|
|
@ -159,14 +285,53 @@ public final class StoryPeerListComponent: Component {
|
|||
}
|
||||
|
||||
var hasUnseen = false
|
||||
if peer.id != component.context.account.peerId {
|
||||
for item in itemSet.items {
|
||||
if !item.isSeen {
|
||||
hasUnseen = true
|
||||
}
|
||||
let hasItems = !itemSet.items.isEmpty
|
||||
var itemProgress: CGFloat?
|
||||
if peer.id == component.context.account.peerId {
|
||||
itemProgress = component.state?.uploadProgress
|
||||
//itemProgress = 0.0
|
||||
}
|
||||
|
||||
for item in itemSet.items {
|
||||
if !item.isSeen {
|
||||
hasUnseen = true
|
||||
}
|
||||
}
|
||||
|
||||
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
|
||||
|
||||
let itemFrame = regularItemFrame.interpolate(to: collapsedItemFrame, amount: component.collapseFraction)
|
||||
|
||||
var leftItemFrame: CGRect?
|
||||
var rightItemFrame: CGRect?
|
||||
|
||||
var itemAlpha: CGFloat = 1.0
|
||||
|
||||
if i >= collapseStartIndex && i <= (collapseStartIndex + 2) {
|
||||
if i != collapseStartIndex {
|
||||
let regularLeftItemFrame = itemLayout.frame(at: i - 1)
|
||||
let collapsedLeftItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularLeftItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularLeftItemFrame.height))
|
||||
leftItemFrame = regularLeftItemFrame.interpolate(to: collapsedLeftItemFrame, amount: component.collapseFraction)
|
||||
}
|
||||
if i != collapseStartIndex + 2 {
|
||||
let regularRightItemFrame = itemLayout.frame(at: i - 1)
|
||||
let collapsedRightItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularRightItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularRightItemFrame.height))
|
||||
rightItemFrame = regularRightItemFrame.interpolate(to: collapsedRightItemFrame, amount: component.collapseFraction)
|
||||
}
|
||||
} else {
|
||||
itemAlpha = pow(1.0 - component.collapseFraction, 1.5)
|
||||
}
|
||||
|
||||
var leftNeighborDistance: CGFloat?
|
||||
var rightNeighborDistance: CGFloat?
|
||||
|
||||
if let leftItemFrame {
|
||||
leftNeighborDistance = abs(leftItemFrame.midX - itemFrame.midX)
|
||||
}
|
||||
if let rightItemFrame {
|
||||
rightNeighborDistance = abs(rightItemFrame.midX - itemFrame.midX)
|
||||
}
|
||||
|
||||
let _ = visibleItem.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StoryPeerListItemComponent(
|
||||
|
|
@ -175,19 +340,26 @@ public final class StoryPeerListComponent: Component {
|
|||
strings: component.strings,
|
||||
peer: peer,
|
||||
hasUnseen: hasUnseen,
|
||||
hasItems: hasItems,
|
||||
progress: itemProgress,
|
||||
collapseFraction: component.collapseFraction,
|
||||
collapsedWidth: collapsedItemWidth,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
action: component.peerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
)
|
||||
|
||||
let itemFrame = itemLayout.frame(at: i)
|
||||
|
||||
if let itemView = visibleItem.view.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
itemTransition.setAlpha(view: itemView, alpha: itemAlpha)
|
||||
itemTransition.setScale(view: itemView, scale: itemScale)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,13 +368,21 @@ public final class StoryPeerListComponent: Component {
|
|||
if !validIds.contains(id) {
|
||||
removedIds.append(id)
|
||||
if let itemView = visibleItem.view.view {
|
||||
itemView.removeFromSuperview()
|
||||
if keepVisibleUntilCompletion && !transition.animation.isImmediate {
|
||||
transition.attachAnimation(view: itemView, id: "keep", completion: { [weak itemView] _ in
|
||||
itemView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removedIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.containerSize.height - 8.0)))
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
|
@ -210,19 +390,41 @@ public final class StoryPeerListComponent: Component {
|
|||
}
|
||||
|
||||
func update(component: StoryPeerListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if self.component != nil {
|
||||
if component.collapseFraction != 0.0 && self.scrollView.bounds.minX != 0.0 {
|
||||
self.ignoreScrolling = true
|
||||
|
||||
let scrollingDistance = self.scrollView.bounds.minX
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)
|
||||
let tempTransition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
self.updateScrolling(transition: tempTransition, keepVisibleUntilCompletion: true)
|
||||
tempTransition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: scrollingDistance, y: 0.0), to: CGPoint(), additive: true)
|
||||
|
||||
self.ignoreScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.collapsedButton.isUserInteractionEnabled = component.collapseFraction >= 1.0 - .ulpOfOne
|
||||
|
||||
self.sortedItemSets.removeAll(keepingCapacity: true)
|
||||
if let state = component.state {
|
||||
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
|
||||
self.sortedItemSets.append(state.itemSets[myIndex])
|
||||
}
|
||||
for itemSet in state.itemSets {
|
||||
if itemSet.peerId == component.context.account.peerId {
|
||||
continue
|
||||
for i in 0 ..< 4 {
|
||||
for itemSet in state.itemSets {
|
||||
if itemSet.peerId == component.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
self.sortedItemSets.append(itemSet)
|
||||
} else {
|
||||
self.sortedItemSets.append(StoryListContext.PeerItemSet(peerId: EnginePeer.Id(namespace: itemSet.peerId.namespace, id: EnginePeer.Id.Id._internalFromInt64Value(itemSet.peerId.id._internalGetInt64Value() + Int64(i))), peer: itemSet.peer, items: itemSet.items, totalCount: itemSet.totalCount))
|
||||
}
|
||||
}
|
||||
self.sortedItemSets.append(itemSet)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +445,7 @@ public final class StoryPeerListComponent: Component {
|
|||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,137 @@ import SwiftSignalKit
|
|||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
|
||||
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
|
||||
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
|
||||
let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)
|
||||
if distance > radius * 2.0 || distance == 0.0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let x1 = center.x
|
||||
let y1 = center.y
|
||||
let x2 = otherCenter.x
|
||||
let y2 = otherCenter.y
|
||||
let r1 = radius
|
||||
let r2 = radius
|
||||
let R = distance
|
||||
|
||||
let ix1: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1)
|
||||
let ix2: CGFloat = 0.5 * (x1 + x2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (x2 - x1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (y2 - y1)
|
||||
|
||||
let iy1: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) + 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2)
|
||||
let iy2: CGFloat = 0.5 * (y1 + y2) + (pow(r1, 2.0) - pow(r2, 2.0)) / (2 * pow(R, 2.0)) * (y2 - y1) - 0.5 * sqrt(2.0 * (pow(r1, 2.0) + pow(r2, 2.0)) / pow(R, 2.0) - pow((pow(r1, 2.0) - pow(r2, 2.0)), 2.0) / pow(R, 4.0) - 1) * (x1 - x2)
|
||||
|
||||
var v1 = CGPoint(x: ix1 - center.x, y: iy1 - center.y)
|
||||
let length1 = sqrt(v1.x * v1.x + v1.y * v1.y)
|
||||
v1.x /= length1
|
||||
v1.y /= length1
|
||||
|
||||
var v2 = CGPoint(x: ix2 - center.x, y: iy2 - center.y)
|
||||
let length2 = sqrt(v2.x * v2.x + v2.y * v2.y)
|
||||
v2.x /= length2
|
||||
v2.y /= length2
|
||||
|
||||
var point1Angle = atan(v1.y / v1.x)
|
||||
var point2Angle = atan(v2.y / v2.x)
|
||||
|
||||
if distanceVector.x < 0.0 {
|
||||
point1Angle += CGFloat.pi
|
||||
point2Angle += CGFloat.pi
|
||||
}
|
||||
|
||||
return (point1Angle, point2Angle)
|
||||
}
|
||||
|
||||
private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat) -> CGPath {
|
||||
let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
|
||||
let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
|
||||
|
||||
let path = CGMutablePath()
|
||||
|
||||
if let leftAngles, let rightAngles {
|
||||
path.addArc(center: center, radius: radius, startAngle: leftAngles.point1Angle, endAngle: rightAngles.point2Angle, clockwise: true)
|
||||
|
||||
path.move(to: CGPoint(x: center.x + cos(rightAngles.point1Angle) * radius, y: center.y + sin(rightAngles.point1Angle) * radius))
|
||||
path.addArc(center: center, radius: radius, startAngle: rightAngles.point1Angle, endAngle: leftAngles.point2Angle, clockwise: true)
|
||||
} else if let angles = leftAngles ?? rightAngles {
|
||||
path.addArc(center: center, radius: radius, startAngle: angles.point1Angle, endAngle: angles.point2Angle, clockwise: true)
|
||||
} else {
|
||||
path.addEllipse(in: CGRect(origin: CGPoint(x: center.x - radius, y: center.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private final class StoryProgressLayer: SimpleShapeLayer {
|
||||
private struct Params: Equatable {
|
||||
var size: CGSize
|
||||
var lineWidth: CGFloat
|
||||
}
|
||||
|
||||
private var currentParams: Params?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.fillColor = UIColor.white.cgColor
|
||||
self.fillRule = .evenOdd
|
||||
|
||||
self.fillColor = nil
|
||||
self.strokeColor = UIColor.white.cgColor
|
||||
self.lineWidth = 2.0
|
||||
self.lineCap = .round
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize, lineWidth: CGFloat) {
|
||||
let params = Params(
|
||||
size: size,
|
||||
lineWidth: lineWidth
|
||||
)
|
||||
if self.currentParams == params {
|
||||
return
|
||||
}
|
||||
self.currentParams = params
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: size.width * 0.5 - lineWidth * 0.5, startAngle: 0.0, endAngle: CGFloat.pi * 0.25, clockwise: false)
|
||||
|
||||
self.path = path
|
||||
|
||||
if self.animation(forKey: "rotation") == nil {
|
||||
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
basicAnimation.duration = 2.0
|
||||
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
||||
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
|
||||
basicAnimation.repeatCount = Float.infinity
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
self.add(basicAnimation, forKey: "rotation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoryPeerListItemComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let peer: EnginePeer
|
||||
public let hasUnseen: Bool
|
||||
public let hasItems: Bool
|
||||
public let progress: CGFloat?
|
||||
public let collapseFraction: CGFloat
|
||||
public let collapsedWidth: CGFloat
|
||||
public let leftNeighborDistance: CGFloat?
|
||||
public let rightNeighborDistance: CGFloat?
|
||||
public let action: (EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
|
|
@ -24,6 +149,12 @@ public final class StoryPeerListItemComponent: Component {
|
|||
strings: PresentationStrings,
|
||||
peer: EnginePeer,
|
||||
hasUnseen: Bool,
|
||||
hasItems: Bool,
|
||||
progress: CGFloat?,
|
||||
collapseFraction: CGFloat,
|
||||
collapsedWidth: CGFloat,
|
||||
leftNeighborDistance: CGFloat?,
|
||||
rightNeighborDistance: CGFloat?,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
|
|
@ -31,6 +162,12 @@ public final class StoryPeerListItemComponent: Component {
|
|||
self.strings = strings
|
||||
self.peer = peer
|
||||
self.hasUnseen = hasUnseen
|
||||
self.hasItems = hasItems
|
||||
self.progress = progress
|
||||
self.collapseFraction = collapseFraction
|
||||
self.collapsedWidth = collapsedWidth
|
||||
self.leftNeighborDistance = leftNeighborDistance
|
||||
self.rightNeighborDistance = rightNeighborDistance
|
||||
self.action = action
|
||||
}
|
||||
|
||||
|
|
@ -50,24 +187,70 @@ public final class StoryPeerListItemComponent: Component {
|
|||
if lhs.hasUnseen != rhs.hasUnseen {
|
||||
return false
|
||||
}
|
||||
if lhs.hasItems != rhs.hasItems {
|
||||
return false
|
||||
}
|
||||
if lhs.progress != rhs.progress {
|
||||
return false
|
||||
}
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.collapsedWidth != rhs.collapsedWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.leftNeighborDistance != rhs.leftNeighborDistance {
|
||||
return false
|
||||
}
|
||||
if lhs.rightNeighborDistance != rhs.rightNeighborDistance {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: HighlightTrackingButton {
|
||||
private let avatarContainer: UIView
|
||||
private var avatarNode: AvatarNode?
|
||||
private let indicatorCircleView: UIImageView
|
||||
private var avatarAddBadgeView: UIImageView?
|
||||
private let avatarShapeLayer: SimpleShapeLayer
|
||||
private let indicatorMaskLayer: SimpleLayer
|
||||
private let indicatorColorLayer: SimpleGradientLayer
|
||||
private var progressLayer: StoryProgressLayer?
|
||||
private let indicatorShapeLayer: SimpleShapeLayer
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryPeerListItemComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.indicatorCircleView = UIImageView()
|
||||
self.indicatorCircleView.isUserInteractionEnabled = false
|
||||
self.avatarContainer = UIView()
|
||||
self.avatarContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.avatarShapeLayer = SimpleShapeLayer()
|
||||
|
||||
self.indicatorColorLayer = SimpleGradientLayer()
|
||||
self.indicatorColorLayer.type = .axial
|
||||
self.indicatorColorLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
self.indicatorColorLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
|
||||
|
||||
self.indicatorMaskLayer = SimpleLayer()
|
||||
self.indicatorShapeLayer = SimpleShapeLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.indicatorCircleView)
|
||||
self.addSubview(self.avatarContainer)
|
||||
|
||||
self.layer.addSublayer(self.indicatorColorLayer)
|
||||
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
|
||||
self.indicatorColorLayer.mask = self.indicatorMaskLayer
|
||||
|
||||
self.avatarShapeLayer.fillColor = UIColor.white.cgColor
|
||||
self.avatarShapeLayer.fillRule = .evenOdd
|
||||
|
||||
self.indicatorShapeLayer.fillColor = nil
|
||||
self.indicatorShapeLayer.strokeColor = UIColor.white.cgColor
|
||||
self.indicatorShapeLayer.lineWidth = 2.0
|
||||
self.indicatorShapeLayer.lineCap = .round
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
|
|
@ -100,70 +283,159 @@ public final class StoryPeerListItemComponent: Component {
|
|||
}
|
||||
|
||||
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let hadUnseen = self.component?.hasUnseen ?? false
|
||||
let hadUnseen = self.component?.hasUnseen
|
||||
let hadProgress = self.component?.progress != nil
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let effectiveWidth: CGFloat = (1.0 - component.collapseFraction) * availableSize.width + component.collapseFraction * component.collapsedWidth
|
||||
|
||||
let effectiveScale: CGFloat = 1.0 * (1.0 - component.collapseFraction) + (24.0 / 52.0) * component.collapseFraction
|
||||
|
||||
let avatarNode: AvatarNode
|
||||
if let current = self.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
self.avatarNode = avatarNode
|
||||
avatarNode.layer.mask = self.avatarShapeLayer
|
||||
avatarNode.isUserInteractionEnabled = false
|
||||
self.addSubview(avatarNode.view)
|
||||
self.avatarContainer.addSubview(avatarNode.view)
|
||||
}
|
||||
|
||||
let avatarSize = CGSize(width: 52.0, height: 52.0)
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5), y: 4.0), size: avatarSize)
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: 4.0), size: avatarSize)
|
||||
|
||||
transition.setFrame(view: avatarNode.view, frame: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
|
||||
let indicatorFrame = avatarFrame.insetBy(dx: -4.0, dy: -4.0)
|
||||
|
||||
let indicatorLineWidth: CGFloat = 2.0 * (1.0 - component.collapseFraction) + (1.33 * (1.0 / effectiveScale)) * (component.collapseFraction)
|
||||
|
||||
avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: component.peer
|
||||
)
|
||||
avatarNode.updateSize(size: avatarSize)
|
||||
transition.setFrame(view: avatarNode.view, frame: avatarFrame)
|
||||
transition.setPosition(view: self.avatarContainer, position: avatarFrame.center)
|
||||
transition.setBounds(view: self.avatarContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
|
||||
if component.peer.id == component.context.account.peerId && !component.hasUnseen {
|
||||
self.indicatorCircleView.image = nil
|
||||
} else if self.indicatorCircleView.image == nil || hadUnseen != component.hasUnseen {
|
||||
self.indicatorCircleView.image = generateImage(indicatorFrame.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
context.setLineWidth(lineWidth)
|
||||
context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
|
||||
context.replacePathWithStrokedPath()
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let colors: [CGColor]
|
||||
|
||||
if component.hasUnseen {
|
||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
||||
}
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
let scaledAvatarSize = effectiveScale * (avatarSize.width + 4.0 - indicatorLineWidth * 2.0)
|
||||
|
||||
transition.setScale(view: self.avatarContainer, scale: scaledAvatarSize / avatarSize.width)
|
||||
|
||||
if component.peer.id == component.context.account.peerId && !component.hasItems {
|
||||
self.indicatorColorLayer.isHidden = true
|
||||
|
||||
let avatarAddBadgeView: UIImageView
|
||||
var avatarAddBadgeTransition = transition
|
||||
if let current = self.avatarAddBadgeView {
|
||||
avatarAddBadgeView = current
|
||||
} else {
|
||||
avatarAddBadgeTransition = .immediate
|
||||
avatarAddBadgeView = UIImageView()
|
||||
self.avatarAddBadgeView = avatarAddBadgeView
|
||||
self.avatarContainer.addSubview(avatarAddBadgeView)
|
||||
}
|
||||
let badgeSize = CGSize(width: 16.0, height: 16.0)
|
||||
if avatarAddBadgeView.image == nil || themeUpdated {
|
||||
avatarAddBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(component.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setStrokeColor(component.theme.list.itemCheckColors.foregroundColor.cgColor)
|
||||
context.setLineWidth(UIScreenPixel * 3.0)
|
||||
context.setLineCap(.round)
|
||||
|
||||
let lineSize: CGFloat = 9.0 + UIScreenPixel
|
||||
|
||||
context.move(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5))
|
||||
context.addLine(to: CGPoint(x: size.width * 0.5, y: (size.height - lineSize) * 0.5 + lineSize))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: (size.width - lineSize) * 0.5, y: size.height * 0.5))
|
||||
context.addLine(to: CGPoint(x: (size.width - lineSize) * 0.5 + lineSize, y: size.height * 0.5))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
avatarAddBadgeTransition.setFrame(view: avatarAddBadgeView, frame: CGRect(origin: CGPoint(x: avatarFrame.width - 1.0 - badgeSize.width, y: avatarFrame.height - 1.0 - badgeSize.height), size: badgeSize))
|
||||
} else {
|
||||
if indicatorColorLayer.isHidden {
|
||||
self.indicatorColorLayer.isHidden = false
|
||||
}
|
||||
|
||||
if let avatarAddBadgeView = self.avatarAddBadgeView {
|
||||
self.avatarAddBadgeView = nil
|
||||
avatarAddBadgeView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: self.indicatorCircleView, frame: indicatorFrame)
|
||||
|
||||
if hadUnseen != component.hasUnseen || hadProgress != (component.progress != nil) {
|
||||
let locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor]
|
||||
|
||||
if component.hasUnseen || component.progress != nil {
|
||||
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
|
||||
}
|
||||
|
||||
self.indicatorColorLayer.locations = locations.map { $0 as NSNumber }
|
||||
self.indicatorColorLayer.colors = colors
|
||||
}
|
||||
|
||||
transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.center)
|
||||
transition.setBounds(layer: self.indicatorColorLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
|
||||
transition.setPosition(layer: self.indicatorShapeLayer, position: CGPoint(x: indicatorFrame.width * 0.5, y: indicatorFrame.height * 0.5))
|
||||
transition.setBounds(layer: self.indicatorShapeLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
|
||||
transition.setScale(layer: self.indicatorColorLayer, scale: effectiveScale)
|
||||
|
||||
let indicatorCenter = CGRect(origin: CGPoint(), size: indicatorFrame.size).center
|
||||
|
||||
var mappedLeftCenter: CGPoint?
|
||||
var mappedRightCenter: CGPoint?
|
||||
|
||||
if let leftNeighborDistance = component.leftNeighborDistance {
|
||||
mappedLeftCenter = CGPoint(x: indicatorCenter.x - leftNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y)
|
||||
}
|
||||
if let rightNeighborDistance = component.rightNeighborDistance {
|
||||
mappedRightCenter = CGPoint(x: indicatorCenter.x + rightNeighborDistance * (1.0 / effectiveScale), y: indicatorCenter.y)
|
||||
}
|
||||
|
||||
let avatarPath = CGMutablePath()
|
||||
avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -1.0, dy: -1.0))
|
||||
if component.peer.id == component.context.account.peerId && !component.hasItems {
|
||||
let cutoutSize: CGFloat = 18.0 + UIScreenPixel * 2.0
|
||||
avatarPath.addEllipse(in: CGRect(origin: CGPoint(x: avatarSize.width - cutoutSize + UIScreenPixel, y: avatarSize.height - cutoutSize + UIScreenPixel), size: CGSize(width: cutoutSize, height: cutoutSize)))
|
||||
} else if let mappedRightCenter {
|
||||
avatarPath.addEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: -indicatorLineWidth, dy: -indicatorLineWidth).offsetBy(dx: abs(mappedRightCenter.x - indicatorCenter.x), dy: 0.0))
|
||||
}
|
||||
self.avatarShapeLayer.path = avatarPath
|
||||
|
||||
self.indicatorShapeLayer.path = calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorFrame.width * 0.5 - indicatorLineWidth * 0.5)
|
||||
|
||||
//TODO:localize
|
||||
let titleString: String
|
||||
if component.peer.id == component.context.account.peerId {
|
||||
if let _ = component.progress {
|
||||
titleString = "Uploading..."
|
||||
} else {
|
||||
titleString = "My story"
|
||||
}
|
||||
} else {
|
||||
titleString = component.peer.compactDisplayTitle
|
||||
}
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.peer.id == component.context.account.peerId ? "My story" : component.peer.compactDisplayTitle, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||
component: AnyComponent(Text(text: titleString, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + 4.0, height: 100.0)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: indicatorFrame.maxY + 3.0), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.25, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 3.0) * effectiveScale), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint()
|
||||
|
|
@ -171,7 +443,40 @@ public final class StoryPeerListItemComponent: Component {
|
|||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
transition.setBounds(view: titleView, bounds: CGRect(origin: CGPoint(), size: titleFrame.size))
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
transition.setScale(view: titleView, scale: effectiveScale)
|
||||
transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction)
|
||||
}
|
||||
|
||||
if component.progress != nil {
|
||||
var progressTransition = transition
|
||||
let progressLayer: StoryProgressLayer
|
||||
if let current = self.progressLayer {
|
||||
progressLayer = current
|
||||
} else {
|
||||
progressTransition = .immediate
|
||||
progressLayer = StoryProgressLayer()
|
||||
self.progressLayer = progressLayer
|
||||
self.indicatorMaskLayer.addSublayer(progressLayer)
|
||||
}
|
||||
let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size)
|
||||
progressTransition.setFrame(layer: progressLayer, frame: progressFrame)
|
||||
progressLayer.update(size: progressFrame.size, lineWidth: 4.0)
|
||||
|
||||
self.indicatorShapeLayer.opacity = 0.0
|
||||
} else {
|
||||
self.indicatorShapeLayer.opacity = 1.0
|
||||
|
||||
if let progressLayer = self.progressLayer {
|
||||
self.progressLayer = nil
|
||||
if transition.animation.isImmediate {
|
||||
progressLayer.removeFromSuperlayer()
|
||||
} else {
|
||||
progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in
|
||||
progressLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import LegacyComponents
|
|||
import LegacyMediaPickerUI
|
||||
import LegacyCamera
|
||||
import AvatarNode
|
||||
import LocalMediaResources
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
|
|
@ -62,7 +63,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh
|
|||
}
|
||||
}
|
||||
|
||||
public final class TelegramRootController: NavigationController {
|
||||
public final class TelegramRootController: NavigationController, TelegramRootControllerInterface {
|
||||
private let context: AccountContext
|
||||
|
||||
public var rootTabController: TabBarController?
|
||||
|
|
@ -262,9 +263,6 @@ public final class TelegramRootController: NavigationController {
|
|||
var presentImpl: ((ViewController) -> Void)?
|
||||
var returnToCameraImpl: (() -> Void)?
|
||||
var dismissCameraImpl: (() -> Void)?
|
||||
let _ = presentImpl
|
||||
let _ = returnToCameraImpl
|
||||
let _ = dismissCameraImpl
|
||||
let cameraController = CameraScreen(context: context, mode: .story, completion: { result in
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreen.Subject? in
|
||||
|
|
@ -352,15 +350,24 @@ public final class TelegramRootController: NavigationController {
|
|||
selectionController?.displayProgress = true
|
||||
|
||||
switch mediaResult {
|
||||
case let .image(image):
|
||||
case let .image(image, _):
|
||||
if let data = image.jpegData(compressionQuality: 0.8) {
|
||||
let _ = context.engine.messages.uploadStory(media: .image(dimensions: PixelDimensions(image.size), data: data), privacy: privacy).start()
|
||||
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
|
||||
storyListContext.upload(media: .image(dimensions: PixelDimensions(image.size), data: data), text: nil, entities: nil, privacy: privacy)
|
||||
}
|
||||
}
|
||||
case .video:
|
||||
break
|
||||
// let resource = VideoLibraryMediaResource(localIdentifier: asset.localIdentifier, conversion: VideoLibraryMediaResourceConversion.passthrough)
|
||||
//
|
||||
// if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
|
||||
// storyListContext.upload(media: .video(dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: Int(asset.duration), resource: resource), privacy: privacy)
|
||||
// }
|
||||
// selectionController?.dismiss()
|
||||
}
|
||||
dismissCameraImpl?()
|
||||
commit()
|
||||
selectionController?.dismiss()
|
||||
})
|
||||
})
|
||||
controller.sourceHint = .camera
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue