Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-05-14 15:27:54 +04:00
commit 37e7a74e4e
28 changed files with 1478 additions and 250 deletions

View file

@ -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 }

View file

@ -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 {

View file

@ -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

View file

@ -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?

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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> {

View file

@ -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))
}
}
}

View file

@ -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",

View file

@ -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()
}

View file

@ -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;
}

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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