mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 06:10:03 +00:00
[WIP] Stories
This commit is contained in:
parent
0882817bed
commit
b64aa1445c
@ -466,8 +466,9 @@ public final class NavigateToChatControllerParams {
|
||||
public let changeColors: Bool
|
||||
public let setupController: (ChatController) -> Void
|
||||
public let completion: (ChatController) -> Void
|
||||
public let pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)?
|
||||
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
|
||||
self.navigationController = navigationController
|
||||
self.chatController = chatController
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
@ -495,6 +496,7 @@ public final class NavigateToChatControllerParams {
|
||||
self.chatNavigationStack = chatNavigationStack
|
||||
self.changeColors = changeColors
|
||||
self.setupController = setupController
|
||||
self.pushController = pushController
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject {
|
||||
func setSoundEnabled(_ value: Bool)
|
||||
func seek(_ timestamp: Double)
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||
func continueWithOverridingAmbientMode()
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
|
||||
@ -283,6 +284,14 @@ public final class UniversalVideoNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
public func continueWithOverridingAmbientMode() {
|
||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||
if let contentNode = contentNode {
|
||||
contentNode.continueWithOverridingAmbientMode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
|
||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||
if let contentNode = contentNode {
|
||||
|
||||
@ -97,6 +97,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -177,6 +177,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
private(set) var storySubscriptions: EngineStorySubscriptions?
|
||||
|
||||
private var storyProgressDisposable: Disposable?
|
||||
private var storySubscriptionsDisposable: Disposable?
|
||||
private var preloadStorySubscriptionsDisposable: Disposable?
|
||||
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||
@ -717,6 +718,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.powerSavingMonitoringDisposable?.dispose()
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
self.preloadStorySubscriptionsDisposable?.dispose()
|
||||
self.storyProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateNavigationMetadata() {
|
||||
@ -1240,7 +1242,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.openStories = { [weak self] peerId in
|
||||
self.chatListDisplayNode.mainContainerNode.openStories = { [weak self] peerId, itemNode in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -1249,16 +1251,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let _ = (storyContent.state
|
||||
|> filter { $0.slice != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var transitionIn: StoryContainerScreen.TransitionIn?
|
||||
if let itemNode = itemNode as? ChatListItemNode {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: itemNode.avatarNode.view,
|
||||
sourceRect: itemNode.avatarNode.view.bounds,
|
||||
sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
|
||||
sourceIsAvatar: true
|
||||
)
|
||||
itemNode.avatarNode.isHidden = true
|
||||
}
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: self.context,
|
||||
content: storyContent,
|
||||
transitionIn: nil,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { _, _ in
|
||||
if let itemNode = itemNode as? ChatListItemNode {
|
||||
let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view)
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: itemNode.view,
|
||||
transitionView: nil,
|
||||
destinationRect: rect,
|
||||
destinationCornerRadius: rect.height * 0.5,
|
||||
destinationIsAvatar: true,
|
||||
completed: { [weak itemNode] in
|
||||
guard let itemNode else {
|
||||
return
|
||||
}
|
||||
itemNode.avatarNode.isHidden = false
|
||||
}
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
@ -1760,22 +1789,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
|
||||
var chatListState = chatListState
|
||||
|
||||
var peersWithNewStories = Set<EnginePeer.Id>()
|
||||
var peerStoryMapping: [EnginePeer.Id: Bool] = [:]
|
||||
for item in storySubscriptions.items {
|
||||
if item.peer.id == self.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if item.hasUnseen {
|
||||
peersWithNewStories.insert(item.peer.id)
|
||||
}
|
||||
peerStoryMapping[item.peer.id] = item.hasUnseen
|
||||
}
|
||||
chatListState.peersWithNewStories = peersWithNewStories
|
||||
chatListState.peerStoryMapping = peerStoryMapping
|
||||
|
||||
return chatListState
|
||||
}
|
||||
|
||||
self.storiesReady.set(.single(true))
|
||||
})
|
||||
self.storyProgressDisposable = (self.context.engine.messages.allStoriesUploadProgress()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateStoryUploadProgress(progress)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2345,7 +2379,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5,
|
||||
sourceIsAvatar: true
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2506,9 +2541,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
private(set) var storyUploadProgress: Float?
|
||||
public func updateStoryUploadProgress(_ progress: Float?) {
|
||||
private func updateStoryUploadProgress(_ progress: Float?) {
|
||||
self.storyUploadProgress = progress.flatMap { max(0.027, min(0.99, $0)) }
|
||||
self.chatListDisplayNode.requestNavigationBarLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
|
||||
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
navigationBarView.updateStoryUploadProgress(storyUploadProgress: self.storyUploadProgress)
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollToStories() {
|
||||
|
||||
@ -191,7 +191,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _ in })
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
|
||||
interaction.isInlineMode = isInlineMode
|
||||
|
||||
let items = (0 ..< 2).map { _ -> ChatListItem in
|
||||
@ -240,7 +240,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
|
||||
@ -979,8 +979,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
itemNode.listNode.activateChatPreview = { [weak self] item, threadId, sourceNode, gesture, location in
|
||||
self?.activateChatPreview?(item, threadId, sourceNode, gesture, location)
|
||||
}
|
||||
itemNode.listNode.openStories = { [weak self] peerId in
|
||||
self?.openStories?(peerId)
|
||||
itemNode.listNode.openStories = { [weak self] peerId, itemNode in
|
||||
self?.openStories?(peerId, itemNode)
|
||||
}
|
||||
itemNode.listNode.addedVisibleChatsWithPeerIds = { [weak self] ids in
|
||||
self?.addedVisibleChatsWithPeerIds?(ids)
|
||||
@ -1052,7 +1052,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
|
||||
public var contentScrollingEnded: ((ListView) -> Bool)?
|
||||
var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
var openStories: ((EnginePeer.Id) -> Void)?
|
||||
var openStories: ((EnginePeer.Id, ASDisplayNode?) -> Void)?
|
||||
var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
|
||||
var didBeginSelectingChats: (() -> Void)?
|
||||
var canExpandHiddenItems: (() -> Bool)?
|
||||
|
||||
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
case let .addContact(phoneNumber, theme, strings):
|
||||
@ -2167,7 +2167,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}, openPremiumIntro: {
|
||||
}, openChatFolderUpdates: {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
chatListInteraction.isSearchMode = true
|
||||
|
||||
@ -3402,7 +3402,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
var isInlineMode = false
|
||||
if case .topics = key {
|
||||
@ -3458,7 +3458,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
case .media:
|
||||
return nil
|
||||
|
||||
@ -26,6 +26,7 @@ import TextNodeWithEntities
|
||||
import ComponentFlow
|
||||
import EmojiStatusComponent
|
||||
import AvatarVideoNode
|
||||
import AvatarStoryIndicatorComponent
|
||||
|
||||
public enum ChatListItemContent {
|
||||
public struct ThreadInfo: Equatable {
|
||||
@ -82,7 +83,7 @@ public enum ChatListItemContent {
|
||||
public var forumTopicData: EngineChatList.ForumTopicData?
|
||||
public var topForumTopicItems: [EngineChatList.ForumTopicData]
|
||||
public var autoremoveTimeout: Int32?
|
||||
public var hasNewStories: Bool
|
||||
public var storyState: Bool?
|
||||
|
||||
public init(
|
||||
messages: [EngineMessage],
|
||||
@ -102,7 +103,7 @@ public enum ChatListItemContent {
|
||||
forumTopicData: EngineChatList.ForumTopicData?,
|
||||
topForumTopicItems: [EngineChatList.ForumTopicData],
|
||||
autoremoveTimeout: Int32?,
|
||||
hasNewStories: Bool
|
||||
storyState: Bool?
|
||||
) {
|
||||
self.messages = messages
|
||||
self.peer = peer
|
||||
@ -121,7 +122,7 @@ public enum ChatListItemContent {
|
||||
self.forumTopicData = forumTopicData
|
||||
self.topForumTopicItems = topForumTopicItems
|
||||
self.autoremoveTimeout = autoremoveTimeout
|
||||
self.hasNewStories = hasNewStories
|
||||
self.storyState = storyState
|
||||
}
|
||||
}
|
||||
|
||||
@ -902,7 +903,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var avatarIconView: ComponentHostView<Empty>?
|
||||
var avatarIconComponent: EmojiStatusComponent?
|
||||
var avatarVideoNode: AvatarVideoNode?
|
||||
var avatarStoryIndicatorNode: ASImageNode?
|
||||
var avatarStoryIndicator: ComponentView<Empty>?
|
||||
|
||||
private var inlineNavigationMarkLayer: SimpleLayer?
|
||||
|
||||
@ -2746,9 +2747,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
|
||||
|
||||
var displayStoryIndicator = false
|
||||
if case let .peer(peerData) = item.content, peerData.hasNewStories {
|
||||
displayStoryIndicator = true
|
||||
var displayStoryIndicator: Bool?
|
||||
if case let .peer(peerData) = item.content {
|
||||
displayStoryIndicator = peerData.storyState
|
||||
}
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
|
||||
@ -2763,7 +2764,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
let storyIndicatorScale = avatarScale
|
||||
if displayStoryIndicator {
|
||||
if displayStoryIndicator != nil {
|
||||
avatarScale *= (avatarFrame.width - 4.0 * 2.0) / avatarFrame.width
|
||||
}
|
||||
|
||||
@ -2774,50 +2775,47 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.avatarNode.updateSize(size: avatarFrame.size)
|
||||
strongSelf.updateVideoVisibility()
|
||||
|
||||
if displayStoryIndicator {
|
||||
let avatarStoryIndicatorNode: ASImageNode
|
||||
if let current = strongSelf.avatarStoryIndicatorNode {
|
||||
avatarStoryIndicatorNode = current
|
||||
if let displayStoryIndicator {
|
||||
var indicatorTransition = Transition(transition)
|
||||
let avatarStoryIndicator: ComponentView<Empty>
|
||||
if let current = strongSelf.avatarStoryIndicator {
|
||||
avatarStoryIndicator = current
|
||||
} else {
|
||||
avatarStoryIndicatorNode = ASImageNode()
|
||||
strongSelf.avatarStoryIndicatorNode = avatarStoryIndicatorNode
|
||||
strongSelf.contextContainer.insertSubnode(avatarStoryIndicatorNode, belowSubnode: strongSelf.avatarContainerNode)
|
||||
|
||||
avatarStoryIndicatorNode.isUserInteractionEnabled = true
|
||||
avatarStoryIndicatorNode.view.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:))))
|
||||
indicatorTransition = .immediate
|
||||
avatarStoryIndicator = ComponentView()
|
||||
strongSelf.avatarStoryIndicator = avatarStoryIndicator
|
||||
}
|
||||
var updateImage = false
|
||||
if let image = avatarStoryIndicatorNode.image {
|
||||
if image.size != avatarFrame.size {
|
||||
updateImage = true
|
||||
|
||||
var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 4.0, y: avatarFrame.minY + 4.0), size: CGSize(width: avatarFrame.width - 4.0 - 4.0, height: avatarFrame.height - 4.0 - 4.0))
|
||||
indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5
|
||||
|
||||
let _ = avatarStoryIndicator.update(
|
||||
transition: indicatorTransition,
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: displayStoryIndicator,
|
||||
isDarkTheme: item.presentationData.theme.overallDarkAppearance,
|
||||
activeLineWidth: 2.0,
|
||||
inactiveLineWidth: 1.0 + UIScreenPixel
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: indicatorFrame.size
|
||||
)
|
||||
if let avatarStoryIndicatorView = avatarStoryIndicator.view {
|
||||
if avatarStoryIndicatorView.superview == nil {
|
||||
avatarStoryIndicatorView.isUserInteractionEnabled = true
|
||||
avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:))))
|
||||
|
||||
strongSelf.contextContainer.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarContainerNode.view)
|
||||
}
|
||||
} else {
|
||||
updateImage = true
|
||||
|
||||
indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center)
|
||||
indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
|
||||
indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale)
|
||||
}
|
||||
if updateImage {
|
||||
avatarStoryIndicatorNode.image = generateImage(avatarFrame.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] = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).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())
|
||||
})
|
||||
}
|
||||
transition.updateFrame(node: avatarStoryIndicatorNode, frame: CGRect(origin: CGPoint(x: avatarFrame.minX, y: avatarFrame.minY + (avatarFrame.height - avatarFrame.height * storyIndicatorScale) * 0.5), size: CGSize(width: avatarFrame.width * storyIndicatorScale, height: avatarFrame.height * storyIndicatorScale)))
|
||||
} else {
|
||||
if let avatarStoryIndicatorNode = strongSelf.avatarStoryIndicatorNode {
|
||||
strongSelf.avatarStoryIndicatorNode = nil
|
||||
avatarStoryIndicatorNode.removeFromSupernode()
|
||||
if let avatarStoryIndicator = strongSelf.avatarStoryIndicator {
|
||||
strongSelf.avatarStoryIndicator = nil
|
||||
avatarStoryIndicator.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@ -3844,7 +3842,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let avatarStoryIndicatorNode = self.avatarStoryIndicatorNode, let result = avatarStoryIndicatorNode.view.hitTest(self.view.convert(point, to: avatarStoryIndicatorNode.view), with: event) {
|
||||
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
@ -3856,7 +3854,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
guard let item = self.item, case let .peer(peerData) = item.content else {
|
||||
return
|
||||
}
|
||||
item.interaction.openStories(peerData.peer.peerId)
|
||||
item.interaction.openStories(peerData.peer.peerId, self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ public final class ChatListNodeInteraction {
|
||||
let openPremiumIntro: () -> Void
|
||||
let openChatFolderUpdates: () -> Void
|
||||
let hideChatFolderUpdates: () -> Void
|
||||
let openStories: (EnginePeer.Id) -> Void
|
||||
let openStories: (EnginePeer.Id, ASDisplayNode?) -> Void
|
||||
|
||||
public var searchTextHighightState: String?
|
||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||
@ -148,7 +148,7 @@ public final class ChatListNodeInteraction {
|
||||
openPremiumIntro: @escaping () -> Void,
|
||||
openChatFolderUpdates: @escaping () -> Void,
|
||||
hideChatFolderUpdates: @escaping () -> Void,
|
||||
openStories: @escaping (EnginePeer.Id) -> Void
|
||||
openStories: @escaping (EnginePeer.Id, ASDisplayNode?) -> Void
|
||||
) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
@ -241,7 +241,7 @@ public struct ChatListNodeState: Equatable {
|
||||
public var foundPeers: [(EnginePeer, EnginePeer?)]
|
||||
public var selectedPeerMap: [EnginePeer.Id: EnginePeer]
|
||||
public var selectedThreadIds: Set<Int64>
|
||||
public var peersWithNewStories: Set<EnginePeer.Id>
|
||||
public var peerStoryMapping: [EnginePeer.Id: Bool]
|
||||
|
||||
public init(
|
||||
presentationData: ChatListPresentationData,
|
||||
@ -257,7 +257,7 @@ public struct ChatListNodeState: Equatable {
|
||||
hiddenItemShouldBeTemporaryRevealed: Bool,
|
||||
hiddenPsaPeerId: EnginePeer.Id?,
|
||||
selectedThreadIds: Set<Int64>,
|
||||
peersWithNewStories: Set<EnginePeer.Id>
|
||||
peerStoryMapping: [EnginePeer.Id: Bool]
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.editing = editing
|
||||
@ -272,7 +272,7 @@ public struct ChatListNodeState: Equatable {
|
||||
self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed
|
||||
self.hiddenPsaPeerId = hiddenPsaPeerId
|
||||
self.selectedThreadIds = selectedThreadIds
|
||||
self.peersWithNewStories = peersWithNewStories
|
||||
self.peerStoryMapping = peerStoryMapping
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool {
|
||||
@ -315,7 +315,7 @@ public struct ChatListNodeState: Equatable {
|
||||
if lhs.selectedThreadIds != rhs.selectedThreadIds {
|
||||
return false
|
||||
}
|
||||
if lhs.peersWithNewStories != rhs.peersWithNewStories {
|
||||
if lhs.peerStoryMapping != rhs.peerStoryMapping {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -396,7 +396,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
forumTopicData: forumTopicData,
|
||||
topForumTopicItems: topForumTopicItems,
|
||||
autoremoveTimeout: peerEntry.autoremoveTimeout,
|
||||
hasNewStories: peerEntry.hasNewStories
|
||||
storyState: peerEntry.storyState
|
||||
)),
|
||||
editing: editing,
|
||||
hasActiveRevealControls: hasActiveRevealControls,
|
||||
@ -742,7 +742,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
forumTopicData: forumTopicData,
|
||||
topForumTopicItems: topForumTopicItems,
|
||||
autoremoveTimeout: peerEntry.autoremoveTimeout,
|
||||
hasNewStories: peerEntry.hasNewStories
|
||||
storyState: peerEntry.storyState
|
||||
)),
|
||||
editing: editing,
|
||||
hasActiveRevealControls: hasActiveRevealControls,
|
||||
@ -1086,7 +1086,7 @@ public final class ChatListNode: ListView {
|
||||
public var toggleArchivedFolderHiddenByDefault: (() -> Void)?
|
||||
public var hidePsa: ((EnginePeer.Id) -> Void)?
|
||||
public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
public var openStories: ((EnginePeer.Id) -> Void)?
|
||||
public var openStories: ((EnginePeer.Id, ASDisplayNode?) -> Void)?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
|
||||
@ -1210,7 +1210,7 @@ public final class ChatListNode: ListView {
|
||||
isSelecting = true
|
||||
}
|
||||
|
||||
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), peersWithNewStories: Set())
|
||||
self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), peerStoryMapping: [:])
|
||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||
|
||||
self.theme = theme
|
||||
@ -1549,11 +1549,11 @@ public final class ChatListNode: ListView {
|
||||
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start()
|
||||
}
|
||||
})
|
||||
}, openStories: { [weak self] peerId in
|
||||
}, openStories: { [weak self] peerId, itemNode in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openStories?(peerId)
|
||||
self.openStories?(peerId, itemNode)
|
||||
})
|
||||
nodeInteraction.isInlineMode = isInlineMode
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
var forumTopicData: EngineChatList.ForumTopicData?
|
||||
var topForumTopicItems: [EngineChatList.ForumTopicData]
|
||||
var revealed: Bool
|
||||
var hasNewStories: Bool
|
||||
var storyState: Bool?
|
||||
|
||||
init(
|
||||
index: EngineChatList.Item.Index,
|
||||
@ -136,7 +136,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
forumTopicData: EngineChatList.ForumTopicData?,
|
||||
topForumTopicItems: [EngineChatList.ForumTopicData],
|
||||
revealed: Bool,
|
||||
hasNewStories: Bool
|
||||
storyState: Bool?
|
||||
) {
|
||||
self.index = index
|
||||
self.presentationData = presentationData
|
||||
@ -160,7 +160,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
self.forumTopicData = forumTopicData
|
||||
self.topForumTopicItems = topForumTopicItems
|
||||
self.revealed = revealed
|
||||
self.hasNewStories = hasNewStories
|
||||
self.storyState = storyState
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
|
||||
@ -270,7 +270,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
if lhs.revealed != rhs.revealed {
|
||||
return false
|
||||
}
|
||||
if lhs.hasNewStories != rhs.hasNewStories {
|
||||
if lhs.storyState != rhs.storyState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -639,7 +639,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
forumTopicData: entry.forumTopicData,
|
||||
topForumTopicItems: entry.topForumTopicItems,
|
||||
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing),
|
||||
hasNewStories: state.peersWithNewStories.contains(entry.renderedPeer.peerId)
|
||||
storyState: state.peerStoryMapping[entry.renderedPeer.peerId]
|
||||
))
|
||||
|
||||
if let threadInfo, threadInfo.isHidden {
|
||||
@ -689,7 +689,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
revealed: false,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)))
|
||||
if foundPinningIndex != 0 {
|
||||
foundPinningIndex -= 1
|
||||
@ -720,7 +720,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
revealed: false,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)))
|
||||
} else {
|
||||
if !filteredAdditionalItemEntries.isEmpty {
|
||||
@ -771,7 +771,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
forumTopicData: item.item.forumTopicData,
|
||||
topForumTopicItems: item.item.topForumTopicItems,
|
||||
revealed: state.hiddenItemShouldBeTemporaryRevealed || state.editing,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)))
|
||||
if pinningIndex != 0 {
|
||||
pinningIndex -= 1
|
||||
|
||||
@ -533,7 +533,8 @@ public class ContactsController: ViewController {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5,
|
||||
sourceIsAvatar: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,23 +254,6 @@ final class ContactsControllerNode: ASDisplayNode {
|
||||
self.controller?.requestLayout(transition: transition)
|
||||
//self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
|
||||
|
||||
/*self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in
|
||||
var chatListState = chatListState
|
||||
|
||||
var peersWithNewStories = Set<EnginePeer.Id>()
|
||||
for item in storySubscriptions.items {
|
||||
if item.peer.id == self.context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if item.hasUnseen {
|
||||
peersWithNewStories.insert(item.peer.id)
|
||||
}
|
||||
}
|
||||
chatListState.peersWithNewStories = peersWithNewStories
|
||||
|
||||
return chatListState
|
||||
}*/
|
||||
|
||||
self.storiesReady.set(.single(true))
|
||||
})
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, openPremiumIntro: {
|
||||
}, openChatFolderUpdates: {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
|
||||
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
|
||||
|
||||
@ -599,6 +599,29 @@ private final class MediaPlayerContext {
|
||||
self.stoppedAtEnd = false
|
||||
}
|
||||
|
||||
fileprivate func continueWithOverridingAmbientMode() {
|
||||
if self.ambient {
|
||||
self.ambient = false
|
||||
|
||||
var loadedState: MediaPlayerLoadedState?
|
||||
switch self.state {
|
||||
case .empty:
|
||||
break
|
||||
case let .playing(currentLoadedState):
|
||||
loadedState = currentLoadedState
|
||||
case let .paused(currentLoadedState):
|
||||
loadedState = currentLoadedState
|
||||
case .seeking:
|
||||
break
|
||||
}
|
||||
|
||||
if let loadedState = loadedState {
|
||||
let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase))
|
||||
self.seek(timestamp: timestamp, action: .play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func continuePlayingWithoutSound() {
|
||||
if self.enableSound {
|
||||
self.lastStatusUpdateTimestamp = nil
|
||||
@ -1134,6 +1157,14 @@ public final class MediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
public func continueWithOverridingAmbientMode() {
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.continueWithOverridingAmbientMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func continuePlayingWithoutSound() {
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
|
||||
@ -341,6 +341,17 @@ public final class MediaBox {
|
||||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)"
|
||||
}
|
||||
|
||||
public func cachedRepresentationCompletePath(_ id: MediaResourceId, keepDuration: CachedMediaRepresentationKeepDuration, representationId: String) -> String {
|
||||
let cacheString: String
|
||||
switch keepDuration {
|
||||
case .general:
|
||||
cacheString = "cache"
|
||||
case .shortLived:
|
||||
cacheString = "short-cache"
|
||||
}
|
||||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representationId)"
|
||||
}
|
||||
|
||||
public func shortLivedResourceCachePathPrefix(_ id: MediaResourceId) -> String {
|
||||
let cacheString = "short-cache"
|
||||
return "\(self.basePath)/\(cacheString)/\(fileNameForId(id))"
|
||||
|
||||
@ -223,7 +223,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
@ -290,7 +290,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
|
||||
@ -857,7 +857,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
@ -923,7 +923,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
|
||||
@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
|
||||
func makeChatListItem(
|
||||
@ -436,7 +436,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
|
||||
@ -25,10 +25,10 @@ public enum ManagedAudioSessionType: Equatable {
|
||||
|
||||
var isPlay: Bool {
|
||||
switch self {
|
||||
case .play, .playWithPossiblePortOverride:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case .play, .ambient, .playWithPossiblePortOverride:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,7 +186,7 @@ public class ManagedAudioSessionControl {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ManagedAudioSession {
|
||||
public final class ManagedAudioSession: NSObject {
|
||||
public private(set) static var shared: ManagedAudioSession?
|
||||
|
||||
private var nextId: Int32 = 0
|
||||
@ -211,6 +211,11 @@ public final class ManagedAudioSession {
|
||||
|
||||
private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>()
|
||||
|
||||
private let volumeUpDetectedPromise = Promise<Void>()
|
||||
public var volumeUpDetected: Signal<Void, NoError> {
|
||||
return self.volumeUpDetectedPromise.get()
|
||||
}
|
||||
|
||||
private var availableOutputsValue: [AudioSessionOutput] = []
|
||||
private var currentOutputValue: AudioSessionOutput?
|
||||
|
||||
@ -220,11 +225,13 @@ public final class ManagedAudioSession {
|
||||
private var isActiveValue: Bool = false
|
||||
private var callKitAudioSessionIsActive: Bool = false
|
||||
|
||||
public init() {
|
||||
override public init() {
|
||||
self.queue = Queue()
|
||||
|
||||
self.hasLoudspeaker = UIDevice.current.model == "iPhone"
|
||||
|
||||
super.init()
|
||||
|
||||
let queue = self.queue
|
||||
NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in
|
||||
queue.async {
|
||||
@ -263,6 +270,8 @@ public final class ManagedAudioSession {
|
||||
})
|
||||
})
|
||||
|
||||
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: [.new, .old], context: nil)
|
||||
|
||||
queue.async {
|
||||
self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn()
|
||||
self.updateCurrentAudioRouteInfo()
|
||||
@ -273,6 +282,17 @@ public final class ManagedAudioSession {
|
||||
|
||||
deinit {
|
||||
self.deactivateTimer?.invalidate()
|
||||
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "outputVolume", let change {
|
||||
if let oldValue = (change[.oldKey] as? NSNumber)?.doubleValue, let newValue = (change[.newKey] as? NSNumber)?.doubleValue {
|
||||
if oldValue < newValue || newValue == 1.0 {
|
||||
self.volumeUpDetectedPromise.set(.single(Void()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCurrentAudioRouteInfo() {
|
||||
|
||||
@ -912,6 +912,7 @@ public class Account {
|
||||
private var resetPeerHoleManagement: ((PeerId) -> Void)?
|
||||
|
||||
public private(set) var pendingMessageManager: PendingMessageManager!
|
||||
private(set) var pendingStoryManager: PendingStoryManager?
|
||||
public private(set) var pendingUpdateMessageManager: PendingUpdateMessageManager!
|
||||
private(set) var messageMediaPreuploadManager: MessageMediaPreuploadManager!
|
||||
private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext!
|
||||
@ -1041,6 +1042,11 @@ public class Account {
|
||||
|
||||
self.messageMediaPreuploadManager = MessageMediaPreuploadManager()
|
||||
self.pendingMessageManager = PendingMessageManager(network: network, postbox: postbox, accountPeerId: peerId, auxiliaryMethods: auxiliaryMethods, stateManager: self.stateManager, localInputActivityManager: self.localInputActivityManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext)
|
||||
if !supplementary {
|
||||
self.pendingStoryManager = PendingStoryManager(postbox: postbox, network: network, accountPeerId: peerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext, auxiliaryMethods: self.auxiliaryMethods)
|
||||
} else {
|
||||
self.pendingStoryManager = nil
|
||||
}
|
||||
self.pendingUpdateMessageManager = PendingUpdateMessageManager(postbox: postbox, network: network, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext)
|
||||
self.pendingPeerMediaUploadManager = PendingPeerMediaUploadManager(postbox: postbox, network: network, stateManager: self.stateManager, accountPeerId: self.peerId)
|
||||
|
||||
@ -1155,6 +1161,7 @@ public class Account {
|
||||
let extractedExpr: [Signal<AccountRunningImportantTasks, NoError>] = [
|
||||
managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] },
|
||||
self.pendingMessageManager.hasPendingMessages |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
(self.pendingStoryManager?.hasPending ?? .single(false)) |> map { hasPending in hasPending ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.pendingUpdateMessageManager.updatingMessageMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.pendingPeerMediaUploadManager.uploadingPeerMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] },
|
||||
self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] },
|
||||
|
||||
@ -38,6 +38,12 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force:
|
||||
if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id {
|
||||
copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox)
|
||||
}
|
||||
let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame")
|
||||
let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame")
|
||||
if FileManager.default.fileExists(atPath: videoFirstFrameFromPath) {
|
||||
let _ = try? FileManager.default.copyItem(atPath: videoFirstFrameFromPath, toPath: videoFirstFrameToPath)
|
||||
}
|
||||
|
||||
if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType {
|
||||
copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox)
|
||||
}
|
||||
|
||||
@ -0,0 +1,324 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
public extension Stories {
|
||||
final class PendingItem: Equatable, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case stableId
|
||||
case timestamp
|
||||
case media
|
||||
case text
|
||||
case entities
|
||||
case pin
|
||||
case privacy
|
||||
case period
|
||||
case randomId
|
||||
}
|
||||
|
||||
public let stableId: Int32
|
||||
public let timestamp: Int32
|
||||
public let media: Media
|
||||
public let text: String
|
||||
public let entities: [MessageTextEntity]
|
||||
public let pin: Bool
|
||||
public let privacy: EngineStoryPrivacy
|
||||
public let period: Int32
|
||||
public let randomId: Int64
|
||||
|
||||
public init(
|
||||
stableId: Int32,
|
||||
timestamp: Int32,
|
||||
media: Media,
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
pin: Bool,
|
||||
privacy: EngineStoryPrivacy,
|
||||
period: Int32,
|
||||
randomId: Int64
|
||||
) {
|
||||
self.stableId = stableId
|
||||
self.timestamp = timestamp
|
||||
self.media = media
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.pin = pin
|
||||
self.privacy = privacy
|
||||
self.period = period
|
||||
self.randomId = randomId
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.stableId = try container.decode(Int32.self, forKey: .stableId)
|
||||
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
|
||||
|
||||
let mediaData = try container.decode(Data.self, forKey: .media)
|
||||
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as! Media
|
||||
|
||||
self.text = try container.decode(String.self, forKey: .text)
|
||||
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
|
||||
self.pin = try container.decode(Bool.self, forKey: .pin)
|
||||
self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy)
|
||||
self.period = try container.decode(Int32.self, forKey: .period)
|
||||
self.randomId = try container.decode(Int64.self, forKey: .randomId)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.stableId, forKey: .stableId)
|
||||
try container.encode(self.timestamp, forKey: .timestamp)
|
||||
|
||||
let mediaEncoder = PostboxEncoder()
|
||||
mediaEncoder.encodeRootObject(self.media)
|
||||
try container.encode(mediaEncoder.makeData(), forKey: .media)
|
||||
|
||||
try container.encode(self.text, forKey: .text)
|
||||
try container.encode(self.entities, forKey: .entities)
|
||||
try container.encode(self.pin, forKey: .pin)
|
||||
try container.encode(self.privacy, forKey: .privacy)
|
||||
try container.encode(self.period, forKey: .period)
|
||||
try container.encode(self.randomId, forKey: .randomId)
|
||||
}
|
||||
|
||||
public static func ==(lhs: PendingItem, rhs: PendingItem) -> Bool {
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.stableId != rhs.stableId {
|
||||
return false
|
||||
}
|
||||
if !lhs.media.isEqual(to: rhs.media) {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.entities != rhs.entities {
|
||||
return false
|
||||
}
|
||||
if lhs.pin != rhs.pin {
|
||||
return false
|
||||
}
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.period != rhs.period {
|
||||
return false
|
||||
}
|
||||
if lhs.randomId != rhs.randomId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalState: Equatable, Codable {
|
||||
public var items: [PendingItem]
|
||||
|
||||
public init(
|
||||
items: [PendingItem]
|
||||
) {
|
||||
self.items = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class PendingStoryManager {
|
||||
private final class PendingItemContext {
|
||||
let queue: Queue
|
||||
let item: Stories.PendingItem
|
||||
let updated: () -> Void
|
||||
|
||||
var progress: Float = 0.0
|
||||
var disposable: Disposable?
|
||||
|
||||
init(queue: Queue, item: Stories.PendingItem, updated: @escaping () -> Void) {
|
||||
self.queue = queue
|
||||
self.item = item
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
let queue: Queue
|
||||
let postbox: Postbox
|
||||
let network: Network
|
||||
let accountPeerId: PeerId
|
||||
let stateManager: AccountStateManager
|
||||
let messageMediaPreuploadManager: MessageMediaPreuploadManager
|
||||
let revalidationContext: MediaReferenceRevalidationContext
|
||||
let auxiliaryMethods: AccountAuxiliaryMethods
|
||||
|
||||
var itemsDisposable: Disposable?
|
||||
var currentPendingItemContext: PendingItemContext?
|
||||
|
||||
var storyObserverContexts: [Int32: Bag<(Float) -> Void>] = [:]
|
||||
|
||||
private let allStoriesUploadProgressPromise = ValuePromise<Float?>(nil, ignoreRepeated: true)
|
||||
var allStoriesUploadProgress: Signal<Float?, NoError> {
|
||||
return self.allStoriesUploadProgressPromise.get()
|
||||
}
|
||||
|
||||
private let hasPendingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
var hasPending: Signal<Bool, NoError> {
|
||||
return self.hasPendingPromise.get()
|
||||
}
|
||||
|
||||
func storyUploadProgress(stableId: Int32, next: @escaping (Float) -> Void) -> Disposable {
|
||||
let bag: Bag<(Float) -> Void>
|
||||
if let current = self.storyObserverContexts[stableId] {
|
||||
bag = current
|
||||
} else {
|
||||
bag = Bag()
|
||||
self.storyObserverContexts[stableId] = bag
|
||||
}
|
||||
|
||||
let index = bag.add(next)
|
||||
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
|
||||
next(currentPendingItemContext.progress)
|
||||
} else {
|
||||
next(1.0)
|
||||
}
|
||||
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self, weak bag] in
|
||||
queue.async {
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
if let bag, let listBag = self.storyObserverContexts[stableId], listBag === bag {
|
||||
bag.remove(index)
|
||||
if bag.isEmpty {
|
||||
self.storyObserverContexts.removeValue(forKey: stableId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
|
||||
self.queue = queue
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
self.accountPeerId = accountPeerId
|
||||
self.stateManager = stateManager
|
||||
self.messageMediaPreuploadManager = messageMediaPreuploadManager
|
||||
self.revalidationContext = revalidationContext
|
||||
self.auxiliaryMethods = auxiliaryMethods
|
||||
|
||||
self.itemsDisposable = (postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .local)])
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] views in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
guard let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
let localState: Stories.LocalState
|
||||
if let value = view.value?.get(Stories.LocalState.self) {
|
||||
localState = value
|
||||
} else {
|
||||
localState = Stories.LocalState(items: [])
|
||||
}
|
||||
self.update(localState: localState)
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func update(localState: Stories.LocalState) {
|
||||
if let currentPendingItemContext = self.currentPendingItemContext, !localState.items.contains(where: { $0.randomId == currentPendingItemContext.item.randomId }) {
|
||||
self.currentPendingItemContext = nil
|
||||
}
|
||||
|
||||
if self.currentPendingItemContext == nil, let firstItem = localState.items.first {
|
||||
let queue = self.queue
|
||||
let itemStableId = firstItem.stableId
|
||||
let pendingItemContext = PendingItemContext(queue: queue, item: firstItem, updated: { [weak self] in
|
||||
queue.async {
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
self.processContextsUpdated()
|
||||
if let pendingItemContext = self.currentPendingItemContext, pendingItemContext.item.stableId == itemStableId, let bag = self.storyObserverContexts[itemStableId] {
|
||||
for f in bag.copyItems() {
|
||||
f(pendingItemContext.progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
self.currentPendingItemContext = pendingItemContext
|
||||
|
||||
let stableId = firstItem.stableId
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, pin: firstItem.pin, privacy: firstItem.privacy, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] event in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
}
|
||||
switch event {
|
||||
case let .progress(progress):
|
||||
if let currentPendingItemContext = self.currentPendingItemContext, currentPendingItemContext.item.stableId == stableId {
|
||||
currentPendingItemContext.progress = progress
|
||||
currentPendingItemContext.updated()
|
||||
}
|
||||
case .completed:
|
||||
// wait for the local state to change via Postbox
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.processContextsUpdated()
|
||||
}
|
||||
|
||||
private func processContextsUpdated() {
|
||||
self.allStoriesUploadProgressPromise.set(self.currentPendingItemContext?.progress)
|
||||
self.hasPendingPromise.set(self.currentPendingItemContext != nil)
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
private let accountPeerId: PeerId
|
||||
|
||||
public var allStoriesUploadProgress: Signal<Float?, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.allStoriesUploadProgress.start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
|
||||
public var hasPending: Signal<Bool, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.hasPending.start(next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
|
||||
public func storyUploadProgress(stableId: Int32) -> Signal<Float, NoError> {
|
||||
return self.impl.signalWith { impl, subscriber in
|
||||
return impl.storyUploadProgress(stableId: stableId, next: subscriber.putNext)
|
||||
}
|
||||
}
|
||||
|
||||
init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) {
|
||||
let queue = Queue.mainQueue()
|
||||
self.queue = queue
|
||||
self.accountPeerId = accountPeerId
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
|
||||
})
|
||||
}
|
||||
|
||||
func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? {
|
||||
return _internal_lookUpPendingStoryIdMapping(accountPeerId: self.accountPeerId, stableId: stableId)
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,10 @@ import TelegramApi
|
||||
|
||||
public enum EngineStoryInputMedia {
|
||||
case image(dimensions: PixelDimensions, data: Data)
|
||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource)
|
||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameImageData: Data?)
|
||||
}
|
||||
|
||||
public struct EngineStoryPrivacy: Equatable {
|
||||
public struct EngineStoryPrivacy: Codable, Equatable {
|
||||
public typealias Base = Stories.Item.Privacy.Base
|
||||
|
||||
public var base: Base
|
||||
@ -66,11 +66,27 @@ public enum Stories {
|
||||
case additionallyIncludePeers = "addPeers"
|
||||
}
|
||||
|
||||
public enum Base: Int32 {
|
||||
public enum Base: Int32, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case value
|
||||
}
|
||||
|
||||
case everyone = 0
|
||||
case contacts = 1
|
||||
case closeFriends = 2
|
||||
case nobody = 3
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.init(rawValue: try container.decode(Int32.self, forKey: .value))!
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.rawValue, forKey: .value)
|
||||
}
|
||||
}
|
||||
|
||||
public var base: Base
|
||||
@ -501,10 +517,7 @@ public enum StoryUploadResult {
|
||||
case completed(Int32?)
|
||||
}
|
||||
|
||||
private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||
let originalMedia: Media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
private func prepareUploadStoryContent(account: Account, media: EngineStoryInputMedia) -> Media {
|
||||
switch media {
|
||||
case let .image(dimensions, data):
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
@ -518,31 +531,21 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia
|
||||
partialReference: nil,
|
||||
flags: []
|
||||
)
|
||||
originalMedia = imageMedia
|
||||
return imageMedia
|
||||
case let .video(dimensions, duration, resource, firstFrameImageData):
|
||||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||
if let firstFrameImageData = firstFrameImageData {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
account.postbox.mediaBox.storeResourceData(resource.id, data: firstFrameImageData)
|
||||
|
||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
}
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: account.peerId,
|
||||
network: account.network,
|
||||
postbox: account.postbox,
|
||||
auxiliaryMethods: account.auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: false,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [imageMedia]
|
||||
)
|
||||
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: [],
|
||||
previewRepresentations: previewRepresentations,
|
||||
videoThumbnails: [],
|
||||
immediateThumbnailData: nil,
|
||||
mimeType: "video/mp4",
|
||||
@ -551,26 +554,32 @@ private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia
|
||||
TelegramMediaFileAttribute.Video(duration: duration, size: dimensions, flags: .supportsStreaming, preloadSize: nil)
|
||||
]
|
||||
)
|
||||
originalMedia = fileMedia
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: account.peerId,
|
||||
network: account.network,
|
||||
postbox: account.postbox,
|
||||
auxiliaryMethods: account.auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: account.messageMediaPreuploadManager,
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: true,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [fileMedia]
|
||||
)
|
||||
return fileMedia
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadedStoryContent(postbox: Postbox, network: Network, media: Media, accountPeerId: PeerId, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||
let originalMedia: Media = media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
contentToUpload = messageContentToUpload(
|
||||
accountPeerId: accountPeerId,
|
||||
network: network,
|
||||
postbox: postbox,
|
||||
auxiliaryMethods: auxiliaryMethods,
|
||||
transformOutgoingMessageMedia: nil,
|
||||
messageMediaPreuploadManager: messageMediaPreuploadManager,
|
||||
revalidationContext: revalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: false,
|
||||
peerId: accountPeerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
text: "",
|
||||
media: [media]
|
||||
)
|
||||
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>
|
||||
switch contentToUpload {
|
||||
@ -624,15 +633,82 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
|
||||
return privacyRules
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
let inputMedia = prepareUploadStoryContent(account: account, media: media)
|
||||
|
||||
let _ = (account.postbox.transaction { transaction in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
var stableId: Int32 = Int32.random(in: 2000000 ..< Int32.max)
|
||||
while currentState.items.contains(where: { $0.stableId == stableId }) {
|
||||
stableId = Int32.random(in: 2000000 ..< Int32.max)
|
||||
}
|
||||
currentState.items.append(Stories.PendingItem(
|
||||
stableId: stableId,
|
||||
timestamp: Int32(Date().timeIntervalSince1970),
|
||||
media: inputMedia,
|
||||
text: text,
|
||||
entities: entities,
|
||||
pin: pin,
|
||||
privacy: privacy,
|
||||
period: Int32(period),
|
||||
randomId: randomId
|
||||
))
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}).start()
|
||||
}
|
||||
|
||||
func _internal_cancelStoryUpload(account: Account, stableId: Int32) {
|
||||
let _ = (account.postbox.transaction { transaction in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private struct PendingStoryIdMappingKey: Hashable {
|
||||
var accountPeerId: PeerId
|
||||
var stableId: Int32
|
||||
}
|
||||
|
||||
private let pendingStoryIdMapping = Atomic<[PendingStoryIdMappingKey: Int32]>(value: [:])
|
||||
|
||||
func _internal_lookUpPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32) -> Int32? {
|
||||
return pendingStoryIdMapping.with { dict in
|
||||
return dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)]
|
||||
}
|
||||
}
|
||||
|
||||
private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId: Int32, id: Int32) {
|
||||
let _ = pendingStoryIdMapping.modify { dict in
|
||||
var dict = dict
|
||||
|
||||
dict[PendingStoryIdMappingKey(accountPeerId: accountPeerId, stableId: stableId)] = id
|
||||
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods)
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .content(content):
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
return postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
@ -664,7 +740,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
|
||||
flags |= 1 << 3
|
||||
|
||||
return account.network.request(Api.functions.stories.sendStory(
|
||||
return network.request(Api.functions.stories.sendStory(
|
||||
flags: flags,
|
||||
media: inputMedia,
|
||||
caption: apiCaption,
|
||||
@ -678,27 +754,69 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
|
||||
var id: Int32?
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
|
||||
id = idValue
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return postbox.transaction { transaction -> StoryUploadResult in
|
||||
var currentState: Stories.LocalState
|
||||
if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) {
|
||||
currentState = value
|
||||
} else {
|
||||
currentState = Stories.LocalState(items: [])
|
||||
}
|
||||
if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) {
|
||||
currentState.items.remove(at: index)
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}
|
||||
|
||||
account.stateManager.addUpdates(updates)
|
||||
var id: Int32?
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
|
||||
if let parsedStory = Stories.StoredItem(apiStoryItem: story, peerId: accountPeerId, transaction: transaction) {
|
||||
var items = transaction.getStoryItems(peerId: accountPeerId)
|
||||
var updatedItems: [Stories.Item] = []
|
||||
if items.firstIndex(where: { $0.id == id }) == nil, case let .item(item) = parsedStory {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items.append(StoryItemsTableEntry(value: entry, id: item.id))
|
||||
}
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
transaction.setStoryItems(peerId: accountPeerId, items: items)
|
||||
}
|
||||
|
||||
id = idValue
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, accountPeerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let id = id {
|
||||
_internal_putPendingStoryIdMapping(accountPeerId: accountPeerId, stableId: stableId, id: id)
|
||||
}
|
||||
|
||||
stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .completed(id)
|
||||
}
|
||||
|
||||
return .single(.completed(id))
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
@ -715,7 +833,7 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||
let originalMedia: Media?
|
||||
if let media = media {
|
||||
(contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
(contentSignal, originalMedia) = uploadedStoryContent(postbox: account.postbox, network: account.network, media: prepareUploadStoryContent(account: account, media: media), accountPeerId: account.peerId, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, auxiliaryMethods: account.auxiliaryMethods)
|
||||
} else {
|
||||
contentSignal = .single(nil)
|
||||
originalMedia = nil
|
||||
@ -803,6 +921,73 @@ func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: In
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> [Api.InputPrivacyRule] in
|
||||
let storyId = StoryId(peerId: account.peerId, id: id)
|
||||
if let storyItem = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self), case let .item(item) = storyItem {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
transaction.setStory(id: storyId, value: entry)
|
||||
}
|
||||
}
|
||||
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
var updatedItems: [Stories.Item] = []
|
||||
if let index = items.firstIndex(where: { $0.id == id }), case let .item(item) = items[index].value.get(Stories.StoredItem.self) {
|
||||
let updatedItem = Stories.Item(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: item.media,
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views,
|
||||
privacy: Stories.Item.Privacy(base: privacy.base, additionallyIncludePeers: privacy.additionallyIncludePeers),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id)
|
||||
}
|
||||
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
transaction.setStoryItems(peerId: account.peerId, items: items)
|
||||
|
||||
return apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
}
|
||||
|> mapToSignal { inputRules -> Signal<Never, NoError> in
|
||||
var flags: Int32 = 0
|
||||
flags |= 1 << 2
|
||||
|
||||
return account.network.request(Api.functions.stories.editStory(flags: flags, id: id, media: nil, caption: nil, entities: nil, privacyRules: inputRules))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
if let updates = updates {
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_deleteStories(account: Account, ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
var items = transaction.getStoryItems(peerId: account.peerId)
|
||||
|
||||
@ -42,8 +42,9 @@ public final class EngineStoryItem: Equatable {
|
||||
public let isPinned: Bool
|
||||
public let isExpired: Bool
|
||||
public let isPublic: Bool
|
||||
public let isPending: Bool
|
||||
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool) {
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.expirationTimestamp = expirationTimestamp
|
||||
@ -55,6 +56,7 @@ public final class EngineStoryItem: Equatable {
|
||||
self.isPinned = isPinned
|
||||
self.isExpired = isExpired
|
||||
self.isPublic = isPublic
|
||||
self.isPending = isPending
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
@ -91,6 +93,9 @@ public final class EngineStoryItem: Equatable {
|
||||
if lhs.isPublic != rhs.isPublic {
|
||||
return false
|
||||
}
|
||||
if lhs.isPending != rhs.isPending {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -474,7 +479,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
items.append(mappedItem)
|
||||
}
|
||||
@ -578,7 +584,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
@ -709,7 +716,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
@ -745,7 +753,8 @@ public final class PeerStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
@ -890,7 +899,8 @@ public final class PeerExpiringStoryListContext {
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
items.append(.item(mappedItem))
|
||||
}
|
||||
|
||||
@ -902,14 +902,40 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) {
|
||||
_internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
}
|
||||
|
||||
public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? {
|
||||
return self.account.pendingStoryManager?.lookUpPendingStoryIdMapping(stableId: stableId)
|
||||
}
|
||||
|
||||
public func allStoriesUploadProgress() -> Signal<Float?, NoError> {
|
||||
guard let pendingStoryManager = self.account.pendingStoryManager else {
|
||||
return .single(nil)
|
||||
}
|
||||
return pendingStoryManager.allStoriesUploadProgress
|
||||
}
|
||||
|
||||
public func storyUploadProgress(stableId: Int32) -> Signal<Float, NoError> {
|
||||
guard let pendingStoryManager = self.account.pendingStoryManager else {
|
||||
return .single(0.0)
|
||||
}
|
||||
return pendingStoryManager.storyUploadProgress(stableId: stableId)
|
||||
}
|
||||
|
||||
public func cancelStoryUpload(stableId: Int32) {
|
||||
_internal_cancelStoryUpload(account: self.account, stableId: stableId)
|
||||
}
|
||||
|
||||
public func editStory(media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
|
||||
public func editStoryPrivacy(id: Int32, privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return _internal_editStoryPrivacy(account: self.account, id: id, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStories(ids: [Int32]) -> Signal<Never, NoError> {
|
||||
return _internal_deleteStories(account: self.account, ids: ids)
|
||||
}
|
||||
|
||||
@ -375,6 +375,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
||||
@ -828,6 +828,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sideInset: component.sideInset,
|
||||
includesHidden: component.storiesIncludeHidden,
|
||||
storySubscriptions: storySubscriptions,
|
||||
collapseFraction: 1.0 - component.storiesFraction,
|
||||
|
||||
@ -163,6 +163,8 @@ public final class ChatListNavigationBar: Component {
|
||||
private weak var disappearingTabsView: UIView?
|
||||
private var disappearingTabsViewSearch: Bool = false
|
||||
|
||||
private var currentHeaderComponent: ChatListHeaderComponent?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
@ -332,35 +334,37 @@ public final class ChatListNavigationBar: Component {
|
||||
self.storiesOffsetFraction = storiesOffsetFraction
|
||||
self.storiesUnlockedFraction = storiesUnlockedOffsetFraction
|
||||
|
||||
let headerComponent = ChatListHeaderComponent(
|
||||
sideInset: component.sideInset + 16.0,
|
||||
primaryContent: component.primaryContent,
|
||||
secondaryContent: component.secondaryContent,
|
||||
secondaryTransition: component.secondaryTransition,
|
||||
networkStatus: nil,
|
||||
storySubscriptions: component.storySubscriptions,
|
||||
storiesIncludeHidden: component.storiesIncludeHidden,
|
||||
storiesFraction: 1.0 - storiesOffsetFraction,
|
||||
storiesUnlockedFraction: 1.0 - storiesUnlockedOffsetFraction,
|
||||
uploadProgress: component.uploadProgress,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
openStatusSetup: { [weak self] sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openStatusSetup(sourceView)
|
||||
},
|
||||
toggleIsLocked: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.context.sharedContext.appLockContext.lock()
|
||||
}
|
||||
)
|
||||
self.currentHeaderComponent = headerComponent
|
||||
let headerContentSize = self.headerContent.update(
|
||||
transition: headerTransition,
|
||||
component: AnyComponent(ChatListHeaderComponent(
|
||||
sideInset: component.sideInset + 16.0,
|
||||
primaryContent: component.primaryContent,
|
||||
secondaryContent: component.secondaryContent,
|
||||
secondaryTransition: component.secondaryTransition,
|
||||
networkStatus: nil,
|
||||
storySubscriptions: component.storySubscriptions,
|
||||
storiesIncludeHidden: component.storiesIncludeHidden,
|
||||
storiesFraction: 1.0 - storiesOffsetFraction,
|
||||
storiesUnlockedFraction: 1.0 - storiesUnlockedOffsetFraction,
|
||||
uploadProgress: component.uploadProgress,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
openStatusSetup: { [weak self] sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openStatusSetup(sourceView)
|
||||
},
|
||||
toggleIsLocked: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.context.sharedContext.appLockContext.lock()
|
||||
}
|
||||
)),
|
||||
component: AnyComponent(headerComponent),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: currentLayout.size.width, height: 44.0)
|
||||
)
|
||||
@ -439,6 +443,60 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStoryUploadProgress(storyUploadProgress: Float?) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if component.uploadProgress != storyUploadProgress {
|
||||
self.component = ChatListNavigationBar(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
statusBarHeight: component.statusBarHeight,
|
||||
sideInset: component.sideInset,
|
||||
isSearchActive: component.isSearchActive,
|
||||
storiesUnlocked: component.storiesUnlocked,
|
||||
primaryContent: component.primaryContent,
|
||||
secondaryContent: component.secondaryContent,
|
||||
secondaryTransition: component.secondaryTransition,
|
||||
storySubscriptions: component.storySubscriptions,
|
||||
storiesIncludeHidden: component.storiesIncludeHidden,
|
||||
uploadProgress: storyUploadProgress,
|
||||
tabsNode: component.tabsNode,
|
||||
tabsNodeIsSearch: component.tabsNodeIsSearch,
|
||||
activateSearch: component.activateSearch,
|
||||
openStatusSetup: component.openStatusSetup
|
||||
)
|
||||
if let currentLayout = self.currentLayout, let headerComponent = self.currentHeaderComponent {
|
||||
let headerComponent = ChatListHeaderComponent(
|
||||
sideInset: headerComponent.sideInset,
|
||||
primaryContent: headerComponent.primaryContent,
|
||||
secondaryContent: headerComponent.secondaryContent,
|
||||
secondaryTransition: headerComponent.secondaryTransition,
|
||||
networkStatus: headerComponent.networkStatus,
|
||||
storySubscriptions: headerComponent.storySubscriptions,
|
||||
storiesIncludeHidden: headerComponent.storiesIncludeHidden,
|
||||
storiesFraction: headerComponent.storiesFraction,
|
||||
storiesUnlockedFraction: headerComponent.storiesUnlockedFraction,
|
||||
uploadProgress: storyUploadProgress,
|
||||
context: headerComponent.context,
|
||||
theme: headerComponent.theme,
|
||||
strings: headerComponent.strings,
|
||||
openStatusSetup: headerComponent.openStatusSetup,
|
||||
toggleIsLocked: headerComponent.toggleIsLocked
|
||||
)
|
||||
self.currentHeaderComponent = headerComponent
|
||||
|
||||
let _ = self.headerContent.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(headerComponent),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: currentLayout.size.width, height: 44.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
|
||||
@ -983,7 +983,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: self.view,
|
||||
sourceRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
||||
sourceCornerRadius: 0.0
|
||||
sourceCornerRadius: 0.0,
|
||||
sourceIsAvatar: false
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ public final class PlainButtonComponent: Component {
|
||||
public enum EffectAlignment {
|
||||
case left
|
||||
case right
|
||||
case center
|
||||
}
|
||||
|
||||
public let content: AnyComponent<Empty>
|
||||
@ -136,9 +137,18 @@ public final class PlainButtonComponent: Component {
|
||||
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
|
||||
}
|
||||
|
||||
self.contentContainer.layer.anchorPoint = CGPoint(x: component.effectAlignment == .left ? 0.0 : 1.0, y: 0.5)
|
||||
let anchorX: CGFloat
|
||||
switch component.effectAlignment {
|
||||
case .left:
|
||||
anchorX = 0.0
|
||||
case .center:
|
||||
anchorX = 0.5
|
||||
case .right:
|
||||
anchorX = 1.0
|
||||
}
|
||||
self.contentContainer.layer.anchorPoint = CGPoint(x: anchorX, y: 0.5)
|
||||
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setPosition(view: self.contentContainer, position: CGPoint(x: component.effectAlignment == .left ? 0.0 : size.width, y: size.height * 0.5))
|
||||
transition.setPosition(view: self.contentContainer, position: CGPoint(x: size.width * anchorX, y: size.height * 0.5))
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@ -7,13 +7,19 @@ import TelegramPresentationData
|
||||
public final class AvatarStoryIndicatorComponent: Component {
|
||||
public let hasUnseen: Bool
|
||||
public let isDarkTheme: Bool
|
||||
public let activeLineWidth: CGFloat
|
||||
public let inactiveLineWidth: CGFloat
|
||||
|
||||
public init(
|
||||
hasUnseen: Bool,
|
||||
isDarkTheme: Bool
|
||||
isDarkTheme: Bool,
|
||||
activeLineWidth: CGFloat,
|
||||
inactiveLineWidth: CGFloat
|
||||
) {
|
||||
self.hasUnseen = hasUnseen
|
||||
self.isDarkTheme = isDarkTheme
|
||||
self.activeLineWidth = activeLineWidth
|
||||
self.inactiveLineWidth = inactiveLineWidth
|
||||
}
|
||||
|
||||
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
|
||||
@ -23,6 +29,12 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
if lhs.isDarkTheme != rhs.isDarkTheme {
|
||||
return false
|
||||
}
|
||||
if lhs.activeLineWidth != rhs.activeLineWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveLineWidth != rhs.inactiveLineWidth {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -50,23 +62,21 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
|
||||
let lineWidth: CGFloat
|
||||
let diameter: CGFloat
|
||||
let outerInset: CGFloat
|
||||
|
||||
if component.hasUnseen {
|
||||
lineWidth = 3.0
|
||||
outerInset = 3.0 + lineWidth
|
||||
diameter = availableSize.width + outerInset * 2.0
|
||||
lineWidth = component.activeLineWidth
|
||||
} else {
|
||||
lineWidth = 2.0
|
||||
outerInset = 3.0 + lineWidth
|
||||
diameter = availableSize.width + outerInset * 2.0
|
||||
lineWidth = component.inactiveLineWidth
|
||||
}
|
||||
let maxOuterInset = component.activeLineWidth + component.activeLineWidth
|
||||
diameter = availableSize.width + maxOuterInset * 2.0
|
||||
let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0
|
||||
|
||||
self.indicatorView.image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
||||
self.indicatorView.image = generateImage(CGSize(width: imageDiameter, height: imageDiameter), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(lineWidth)
|
||||
context.addEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
|
||||
context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
|
||||
context.replacePathWithStrokedPath()
|
||||
context.clip()
|
||||
|
||||
@ -87,7 +97,7 @@ public final class AvatarStoryIndicatorComponent: Component {
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(), size: availableSize).insetBy(dx: -outerInset, dy: -outerInset))
|
||||
transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter)))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/ContextUI",
|
||||
|
||||
@ -162,6 +162,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
private let focusedItem = ValuePromise<StoryId?>(nil, ignoreRepeated: true)
|
||||
private var contentUpdatedDisposable: Disposable?
|
||||
|
||||
private let storyItemSharedState = StoryContentItem.SharedState()
|
||||
private var visibleItemSetViews: [EnginePeer.Id: ItemSetView] = [:]
|
||||
|
||||
private var itemSetPanState: ItemSetPanState?
|
||||
@ -171,6 +172,8 @@ private final class StoryContainerScreenComponent: Component {
|
||||
private var isAnimatingOut: Bool = false
|
||||
private var didAnimateOut: Bool = false
|
||||
|
||||
var dismissWithoutTransitionOut: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
|
||||
@ -467,7 +470,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) {
|
||||
if !self.dismissWithoutTransitionOut, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) {
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
@ -482,12 +485,18 @@ private final class StoryContainerScreenComponent: Component {
|
||||
focusedItemPromise.set(.single(nil))
|
||||
})
|
||||
} else {
|
||||
let transition: Transition
|
||||
if self.dismissWithoutTransitionOut {
|
||||
transition = Transition(animation: .curve(duration: 0.5, curve: .spring))
|
||||
} else {
|
||||
transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
self.verticalPanState = ItemSetPanState(fraction: 1.0, didBegin: true)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||
self.state?.updated(transition: transition)
|
||||
|
||||
let focusedItemPromise = self.component?.focusedItemPromise
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
focusedItemPromise?.set(.single(nil))
|
||||
@ -690,6 +699,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
component: AnyComponent(StoryItemSetContainerComponent(
|
||||
context: component.context,
|
||||
externalState: itemSetView.externalState,
|
||||
storyItemSharedState: self.storyItemSharedState,
|
||||
slice: slice,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
@ -989,15 +999,18 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceCornerRadius: CGFloat
|
||||
public let sourceIsAvatar: Bool
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceCornerRadius: CGFloat
|
||||
sourceCornerRadius: CGFloat,
|
||||
sourceIsAvatar: Bool
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceCornerRadius = sourceCornerRadius
|
||||
self.sourceIsAvatar = sourceIsAvatar
|
||||
}
|
||||
}
|
||||
|
||||
@ -1081,6 +1094,15 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
func dismissWithoutTransitionOut() {
|
||||
self.focusedItemPromise.set(.single(nil))
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View {
|
||||
componentView.dismissWithoutTransitionOut = true
|
||||
}
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
|
||||
@ -12,6 +12,13 @@ public final class StoryContentItem {
|
||||
}
|
||||
}
|
||||
|
||||
public final class SharedState {
|
||||
public var useAmbientMode: Bool = true
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
open class View: UIView {
|
||||
open func setIsProgressPaused(_ isProgressPaused: Bool) {
|
||||
}
|
||||
@ -22,15 +29,18 @@ public final class StoryContentItem {
|
||||
|
||||
public final class Environment: Equatable {
|
||||
public let externalState: ExternalState
|
||||
public let sharedState: SharedState
|
||||
public let presentationProgressUpdated: (Double, Bool) -> Void
|
||||
public let markAsSeen: (StoryId) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
sharedState: SharedState,
|
||||
presentationProgressUpdated: @escaping (Double, Bool) -> Void,
|
||||
markAsSeen: @escaping (StoryId) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.sharedState = sharedState
|
||||
self.presentationProgressUpdated = presentationProgressUpdated
|
||||
self.markAsSeen = markAsSeen
|
||||
}
|
||||
@ -39,6 +49,9 @@ public final class StoryContentItem {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.sharedState !== rhs.sharedState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,22 @@ final class StoryContentCaptionComponent: Component {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout {
|
||||
var containerSize: CGSize
|
||||
var visibleTextHeight: CGFloat
|
||||
var verticalInset: CGFloat
|
||||
|
||||
init(
|
||||
containerSize: CGSize,
|
||||
visibleTextHeight: CGFloat,
|
||||
verticalInset: CGFloat
|
||||
) {
|
||||
self.containerSize = containerSize
|
||||
self.visibleTextHeight = visibleTextHeight
|
||||
self.verticalInset = verticalInset
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollViewContainer: UIView
|
||||
@ -51,15 +67,23 @@ final class StoryContentCaptionComponent: Component {
|
||||
private let scrollCenterMaskView: UIView
|
||||
private let scrollBottomMaskView: UIImageView
|
||||
|
||||
private let shadowGradientLayer: SimpleGradientLayer
|
||||
private let shadowPlainLayer: SimpleLayer
|
||||
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryContentCaptionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var ignoreExternalState: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.shadowGradientLayer = SimpleGradientLayer()
|
||||
self.shadowPlainLayer = SimpleLayer()
|
||||
|
||||
self.scrollViewContainer = UIView()
|
||||
|
||||
self.scrollView = UIScrollView()
|
||||
@ -87,6 +111,9 @@ final class StoryContentCaptionComponent: Component {
|
||||
self.scrollMaskContainer.addSubview(self.scrollBottomMaskView)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.shadowGradientLayer)
|
||||
self.layer.addSublayer(self.shadowPlainLayer)
|
||||
|
||||
self.scrollViewContainer.addSubview(self.scrollView)
|
||||
self.scrollView.delegate = self
|
||||
@ -145,7 +172,7 @@ final class StoryContentCaptionComponent: Component {
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component else {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -155,6 +182,11 @@ final class StoryContentCaptionComponent: Component {
|
||||
let edgeDistanceFraction = edgeDistance / 7.0
|
||||
transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction)
|
||||
|
||||
let shadowOverflow: CGFloat = 26.0
|
||||
let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow))
|
||||
transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame)
|
||||
transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0)))
|
||||
|
||||
let expandDistance: CGFloat = 50.0
|
||||
var expandFraction: CGFloat = self.scrollView.contentOffset.y / expandDistance
|
||||
expandFraction = max(0.0, min(1.0, expandFraction))
|
||||
@ -205,6 +237,12 @@ final class StoryContentCaptionComponent: Component {
|
||||
textView.frame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textSize)
|
||||
}
|
||||
|
||||
self.itemLayout = ItemLayout(
|
||||
containerSize: availableSize,
|
||||
visibleTextHeight: visibleTextHeight,
|
||||
verticalInset: verticalInset
|
||||
)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
if self.scrollView.contentSize != scrollContentSize {
|
||||
@ -213,6 +251,28 @@ final class StoryContentCaptionComponent: Component {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
|
||||
if self.shadowGradientLayer.colors == nil {
|
||||
var locations: [NSNumber] = []
|
||||
var colors: [CGColor] = []
|
||||
let numStops = 10
|
||||
let baseAlpha: CGFloat = 0.3
|
||||
for i in 0 ..< numStops {
|
||||
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
|
||||
locations.append((1.0 - step) as NSNumber)
|
||||
let alphaStep: CGFloat = pow(step, 1.2)
|
||||
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
|
||||
}
|
||||
|
||||
self.shadowGradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
self.shadowGradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
|
||||
|
||||
self.shadowGradientLayer.locations = locations
|
||||
self.shadowGradientLayer.colors = colors
|
||||
self.shadowGradientLayer.type = .axial
|
||||
|
||||
self.shadowPlainLayer.backgroundColor = UIColor(white: 0.0, alpha: baseAlpha).cgColor
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import AvatarNode
|
||||
import MediaEditorScreen
|
||||
import ImageCompression
|
||||
import ShareWithPeersScreen
|
||||
import PlainButtonComponent
|
||||
|
||||
public final class StoryItemSetContainerComponent: Component {
|
||||
public final class ExternalState {
|
||||
@ -37,6 +38,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
public let context: AccountContext
|
||||
public let externalState: ExternalState
|
||||
public let storyItemSharedState: StoryContentItem.SharedState
|
||||
public let slice: StoryContentContextState.FocusedSlice
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
@ -59,6 +61,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
externalState: ExternalState,
|
||||
storyItemSharedState: StoryContentItem.SharedState,
|
||||
slice: StoryContentContextState.FocusedSlice,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
@ -80,6 +83,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
) {
|
||||
self.context = context
|
||||
self.externalState = externalState
|
||||
self.storyItemSharedState = storyItemSharedState
|
||||
self.slice = slice
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -531,6 +535,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let itemEnvironment = StoryContentItem.Environment(
|
||||
externalState: visibleItem.externalState,
|
||||
sharedState: component.storyItemSharedState,
|
||||
presentationProgressUpdated: { [weak self, weak visibleItem] progress, canSwitch in
|
||||
guard let self = self, let component = self.component else {
|
||||
return
|
||||
@ -667,11 +672,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
|
||||
if let rightInfoView = self.rightInfoItem?.view.view {
|
||||
if transitionIn.sourceCornerRadius != 0.0 {
|
||||
if transitionIn.sourceIsAvatar {
|
||||
let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: CGPoint(x: innerSourceLocalFrame.center.x - rightInfoView.layer.position.x, y: innerSourceLocalFrame.center.y - rightInfoView.layer.position.y), to: CGPoint(), elevation: 0.0, duration: 0.3, curve: .spring, reverse: false)
|
||||
rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", additive: true)
|
||||
|
||||
rightInfoView.layer.animateScale(from: innerSourceLocalFrame.width / rightInfoView.bounds.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
rightInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1553,6 +1560,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.contextController = contextController
|
||||
self.updateIsProgressPaused()
|
||||
controller.present(contextController, in: .window(.root))
|
||||
},
|
||||
openPeer: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.navigateToPeer(peer: peer)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -1658,7 +1671,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let rightInfoItemSize = currentRightInfoItem.view.update(
|
||||
transition: .immediate,
|
||||
component: currentRightInfoItem.component,
|
||||
component: AnyComponent(PlainButtonComponent(content: currentRightInfoItem.component, effectAlignment: .center, action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.navigateToPeer(peer: component.slice.peer)
|
||||
})),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 36.0, height: 36.0)
|
||||
)
|
||||
@ -1762,13 +1780,15 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width, height: contentFrame.height)
|
||||
)
|
||||
captionItem.view.parentState = state
|
||||
let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.maxY - captionSize.height), size: captionSize)
|
||||
let captionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.height - captionSize.height), size: captionSize)
|
||||
if let captionItemView = captionItem.view.view {
|
||||
if captionItemView.superview == nil {
|
||||
self.addSubview(captionItemView)
|
||||
if self.contentContainerView.subviews.count >= 1 {
|
||||
self.contentContainerView.insertSubview(captionItemView, at: 1)
|
||||
}
|
||||
}
|
||||
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame)
|
||||
captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList || self.inputPanelExternalState.isEditing) ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2108,10 +2128,18 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
initialPrivacy: privacy,
|
||||
stateContext: stateContext,
|
||||
completion: { [weak self] privacy in
|
||||
self?.updateIsProgressPaused()
|
||||
},
|
||||
editCategory: { privacy in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
|
||||
|
||||
self.updateIsProgressPaused()
|
||||
},
|
||||
editCategory: { [weak self] privacy in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
|
||||
}
|
||||
)
|
||||
self.component?.controller()?.push(controller)
|
||||
@ -2121,6 +2149,33 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
private func navigateToPeer(peer: EnginePeer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let controller = component.controller() as? StoryContainerScreen else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in
|
||||
guard let controller, let navigationController else {
|
||||
return
|
||||
}
|
||||
var viewControllers = navigationController.viewControllers
|
||||
if let index = viewControllers.firstIndex(where: { $0 === controller }) {
|
||||
viewControllers.insert(chatController, at: index)
|
||||
} else {
|
||||
viewControllers.append(chatController)
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: animated)
|
||||
}))
|
||||
|
||||
controller.dismissWithoutTransitionOut()
|
||||
}
|
||||
|
||||
private func openStoryEditing() {
|
||||
guard let context = self.component?.context, let id = self.component?.slice.item.storyItem.id else {
|
||||
return
|
||||
|
||||
@ -32,6 +32,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
let expandViewStats: () -> Void
|
||||
let deleteAction: () -> Void
|
||||
let moreAction: (UIView, ContextGesture?) -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
@ -44,7 +45,8 @@ final class StoryItemSetViewListComponent: Component {
|
||||
close: @escaping () -> Void,
|
||||
expandViewStats: @escaping () -> Void,
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -57,6 +59,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.expandViewStats = expandViewStats
|
||||
self.deleteAction = deleteAction
|
||||
self.moreAction = moreAction
|
||||
self.openPeer = openPeer
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
|
||||
@ -432,8 +435,11 @@ final class StoryItemSetViewListComponent: Component {
|
||||
subtitle: dateText,
|
||||
selectionState: .none,
|
||||
hasNext: index != viewListState.totalCount - 1,
|
||||
action: { _ in
|
||||
|
||||
action: { [weak self] peer in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.openPeer(peer)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -58,8 +58,16 @@ final class StoryAuthorInfoComponent: Component {
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
let title = component.peer?.debugDisplayTitle ?? ""
|
||||
let subtitle = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: component.timestamp).string
|
||||
let title: String
|
||||
if component.peer?.id == component.context.account.peerId {
|
||||
//TODO:localize
|
||||
title = "Your story"
|
||||
} else {
|
||||
title = component.peer?.debugDisplayTitle ?? ""
|
||||
}
|
||||
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
|
||||
@ -28,13 +28,14 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
private var disposable: Disposable?
|
||||
private var loadDisposable: Disposable?
|
||||
|
||||
private let currentFocusedIdPromise = Promise<Int32?>()
|
||||
private let currentFocusedIdUpdatedPromise = Promise<Void>()
|
||||
private var storedFocusedId: Int32?
|
||||
private var currentMappedItems: [EngineStoryItem]?
|
||||
var currentFocusedId: Int32? {
|
||||
didSet {
|
||||
if self.currentFocusedId != self.storedFocusedId {
|
||||
self.storedFocusedId = self.currentFocusedId
|
||||
self.currentFocusedIdPromise.set(.single(self.currentFocusedId))
|
||||
self.currentFocusedIdUpdatedPromise.set(.single(Void()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,20 +44,25 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
|
||||
self.currentFocusedIdPromise.set(.single(initialFocusedId))
|
||||
self.currentFocusedId = initialFocusedId
|
||||
self.currentFocusedIdUpdatedPromise.set(.single(Void()))
|
||||
|
||||
var inputKeys: [PostboxViewKey] = [
|
||||
PostboxViewKey.basicPeer(peerId),
|
||||
PostboxViewKey.storiesState(key: .peer(peerId)),
|
||||
PostboxViewKey.storyItems(peerId: peerId)
|
||||
]
|
||||
if peerId == context.account.peerId {
|
||||
inputKeys.append(PostboxViewKey.storiesState(key: .local))
|
||||
}
|
||||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
self.currentFocusedIdPromise.get(),
|
||||
self.currentFocusedIdUpdatedPromise.get(),
|
||||
context.account.postbox.combinedView(
|
||||
keys: [
|
||||
PostboxViewKey.basicPeer(peerId),
|
||||
PostboxViewKey.storiesState(key: .peer(peerId)),
|
||||
PostboxViewKey.storyItems(peerId: peerId)
|
||||
]
|
||||
keys: inputKeys
|
||||
)
|
||||
)
|
||||
|> mapToSignal { currentFocusedId, views -> Signal<(Int32?, CombinedView, [PeerId: Peer]), NoError> in
|
||||
return context.account.postbox.transaction { transaction -> (Int32?, CombinedView, [PeerId: Peer]) in
|
||||
|> mapToSignal { _, views -> Signal<(CombinedView, [PeerId: Peer]), NoError> in
|
||||
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer]) in
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView {
|
||||
for item in itemsView.items {
|
||||
@ -71,10 +77,10 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
return (currentFocusedId, views, peers)
|
||||
return (views, peers)
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] currentFocusedId, views, peers in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] views, peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -84,7 +90,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
guard let stateView = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
||||
return
|
||||
}
|
||||
guard let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
guard let peerStoryItemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView else {
|
||||
return
|
||||
}
|
||||
guard let peer = peerView.peer.flatMap(EnginePeer.init) else {
|
||||
@ -92,86 +98,134 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
}
|
||||
let state = stateView.value?.get(Stories.PeerState.self)
|
||||
|
||||
var mappedItems: [EngineStoryItem] = peerStoryItemsView.items.compactMap { item -> EngineStoryItem? in
|
||||
guard case let .item(item) = item.value.get(Stories.StoredItem.self) else {
|
||||
return nil
|
||||
}
|
||||
guard let media = item.media else {
|
||||
return nil
|
||||
}
|
||||
return EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
}
|
||||
if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
|
||||
for item in localState.items {
|
||||
mappedItems.append(EngineStoryItem(
|
||||
id: item.stableId,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: nil,
|
||||
privacy: item.privacy,
|
||||
isPinned: item.pin,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: true
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let currentFocusedId = self.storedFocusedId
|
||||
|
||||
var focusedIndex: Int?
|
||||
if let currentFocusedId {
|
||||
focusedIndex = itemsView.items.firstIndex(where: { $0.id == currentFocusedId })
|
||||
focusedIndex = mappedItems.firstIndex(where: { $0.id == currentFocusedId })
|
||||
if focusedIndex == nil {
|
||||
if let currentMappedItems = self.currentMappedItems {
|
||||
if let previousIndex = currentMappedItems.firstIndex(where: { $0.id == currentFocusedId }) {
|
||||
if currentMappedItems[previousIndex].isPending {
|
||||
if let updatedId = context.engine.messages.lookUpPendingStoryIdMapping(stableId: currentFocusedId) {
|
||||
if let index = mappedItems.firstIndex(where: { $0.id == updatedId }) {
|
||||
focusedIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if focusedIndex == nil && previousIndex != 0 {
|
||||
for index in (0 ..< previousIndex).reversed() {
|
||||
if let value = mappedItems.firstIndex(where: { $0.id == currentMappedItems[index].id }) {
|
||||
focusedIndex = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if focusedIndex == nil, let state {
|
||||
if let storedFocusedId = self.storedFocusedId {
|
||||
focusedIndex = itemsView.items.firstIndex(where: { $0.id >= storedFocusedId })
|
||||
focusedIndex = mappedItems.firstIndex(where: { $0.id >= storedFocusedId })
|
||||
} else if let index = mappedItems.firstIndex(where: { $0.isPending }) {
|
||||
focusedIndex = index
|
||||
} else {
|
||||
focusedIndex = itemsView.items.firstIndex(where: { $0.id > state.maxReadId })
|
||||
focusedIndex = mappedItems.firstIndex(where: { $0.id > state.maxReadId })
|
||||
}
|
||||
}
|
||||
if focusedIndex == nil {
|
||||
if !itemsView.items.isEmpty {
|
||||
if !mappedItems.isEmpty {
|
||||
focusedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
self.currentMappedItems = mappedItems
|
||||
|
||||
if let focusedIndex {
|
||||
self.storedFocusedId = itemsView.items[focusedIndex].id
|
||||
self.storedFocusedId = mappedItems[focusedIndex].id
|
||||
|
||||
var previousItemId: Int32?
|
||||
var nextItemId: Int32?
|
||||
|
||||
if focusedIndex != 0 {
|
||||
previousItemId = itemsView.items[focusedIndex - 1].id
|
||||
previousItemId = mappedItems[focusedIndex - 1].id
|
||||
}
|
||||
if focusedIndex != itemsView.items.count - 1 {
|
||||
nextItemId = itemsView.items[focusedIndex + 1].id
|
||||
if focusedIndex != mappedItems.count - 1 {
|
||||
nextItemId = mappedItems[focusedIndex + 1].id
|
||||
}
|
||||
|
||||
var loadKeys: [StoryKey] = []
|
||||
for index in (focusedIndex - 2) ... (focusedIndex + 2) {
|
||||
if index >= 0 && index < itemsView.items.count {
|
||||
if let item = itemsView.items[index].value.get(Stories.StoredItem.self), case .placeholder = item {
|
||||
loadKeys.append(StoryKey(peerId: peerId, id: item.id))
|
||||
if let mappedFocusedIndex = peerStoryItemsView.items.firstIndex(where: { $0.id == mappedItems[focusedIndex].id }) {
|
||||
for index in (mappedFocusedIndex - 2) ... (mappedFocusedIndex + 2) {
|
||||
if index >= 0 && index < peerStoryItemsView.items.count {
|
||||
if let item = peerStoryItemsView.items[index].value.get(Stories.StoredItem.self), case .placeholder = item {
|
||||
loadKeys.append(StoryKey(peerId: peerId, id: item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !loadKeys.isEmpty {
|
||||
loadIds(loadKeys)
|
||||
if !loadKeys.isEmpty {
|
||||
loadIds(loadKeys)
|
||||
}
|
||||
}
|
||||
|
||||
if let item = itemsView.items[focusedIndex].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||
let mappedItem = EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: item.views.flatMap { views in
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
do {
|
||||
let mappedItem = mappedItems[focusedIndex]
|
||||
|
||||
var nextItems: [EngineStoryItem] = []
|
||||
for i in (focusedIndex + 1) ..< min(focusedIndex + 4, itemsView.items.count) {
|
||||
if let item = itemsView.items[i].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||
nextItems.append(EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
expirationTimestamp: item.expirationTimestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: nil,
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
))
|
||||
for i in (focusedIndex + 1) ..< min(focusedIndex + 4, mappedItems.count) {
|
||||
do {
|
||||
let item = mappedItems[i]
|
||||
nextItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +233,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
self.sliceValue = StoryContentContextState.FocusedSlice(
|
||||
peer: peer,
|
||||
item: StoryContentItem(
|
||||
id: AnyHashable(item.id),
|
||||
id: AnyHashable(mappedItem.id),
|
||||
position: focusedIndex,
|
||||
component: AnyComponent(StoryItemContentComponent(
|
||||
context: context,
|
||||
@ -189,7 +243,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
centerInfoComponent: AnyComponent(StoryAuthorInfoComponent(
|
||||
context: context,
|
||||
peer: peer,
|
||||
timestamp: item.timestamp
|
||||
timestamp: mappedItem.timestamp
|
||||
)),
|
||||
rightInfoComponent: AnyComponent(StoryAvatarInfoComponent(
|
||||
context: context,
|
||||
@ -199,7 +253,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
storyItem: mappedItem,
|
||||
isMy: peerId == context.account.peerId
|
||||
),
|
||||
totalCount: itemsView.items.count,
|
||||
totalCount: mappedItems.count,
|
||||
previousItemId: previousItemId,
|
||||
nextItemId: nextItemId
|
||||
)
|
||||
@ -840,15 +894,30 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
|
||||
self.storyDisposable = (combineLatest(queue: .mainQueue(),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId)),
|
||||
context.account.postbox.transaction { transaction -> Stories.StoredItem? in
|
||||
return transaction.getStory(id: storyId)?.get(Stories.StoredItem.self)
|
||||
context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer]) in
|
||||
guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else {
|
||||
return (nil, [:])
|
||||
}
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
if case let .item(item) = item {
|
||||
if let views = item.views {
|
||||
for id in views.seenPeerIds {
|
||||
if let peer = transaction.getPeer(id) {
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (item, peers)
|
||||
}
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer, item in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer, itemAndPeers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let (item, peers) = itemAndPeers
|
||||
|
||||
if item == nil {
|
||||
let storyKey = StoryKey(peerId: storyId.peerId, id: storyId.id)
|
||||
if !self.requestedStoryKeys.contains(storyKey) {
|
||||
@ -870,14 +939,15 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
return EngineStoryItem.Views(
|
||||
seenCount: views.seenCount,
|
||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||
return nil
|
||||
return peers[id].flatMap(EnginePeer.init)
|
||||
}
|
||||
)
|
||||
},
|
||||
privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: itemValue.isPinned,
|
||||
isExpired: itemValue.isExpired,
|
||||
isPublic: itemValue.isPublic
|
||||
isPublic: itemValue.isPublic,
|
||||
isPending: false
|
||||
)
|
||||
|
||||
let stateValue = StoryContentContextState(
|
||||
|
||||
@ -12,6 +12,7 @@ import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
import StoryContainerScreen
|
||||
import HierarchyTrackingLayer
|
||||
import VolumeButtons
|
||||
|
||||
final class StoryItemContentComponent: Component {
|
||||
typealias EnvironmentType = StoryContentItem.Environment
|
||||
@ -93,6 +94,8 @@ final class StoryItemContentComponent: Component {
|
||||
private let imageNode: TransformImageNode
|
||||
private var videoNode: UniversalVideoNode?
|
||||
|
||||
private var volumeButtonsListener: VolumeButtonsListener?
|
||||
|
||||
private var currentMessageMedia: EngineMedia?
|
||||
private var fetchDisposable: Disposable?
|
||||
|
||||
@ -141,7 +144,7 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
|
||||
private func performActionAfterImageContentLoaded(update: Bool) {
|
||||
guard let component = self.component, let currentMessageMedia = self.currentMessageMedia else {
|
||||
guard let component = self.component, let environment = self.environment, let currentMessageMedia = self.currentMessageMedia else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -160,6 +163,7 @@ final class StoryItemContentComponent: Component {
|
||||
streamVideo: .story,
|
||||
loopVideo: true,
|
||||
enableSound: true,
|
||||
beginWithAmbientSound: environment.sharedState.useAmbientMode,
|
||||
tempFilePath: nil,
|
||||
captureProtected: false,
|
||||
storeAfterDownload: nil
|
||||
@ -189,6 +193,19 @@ final class StoryItemContentComponent: Component {
|
||||
if update {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
|
||||
if self.volumeButtonsListener == nil, let sharedState = self.environment?.sharedState, sharedState.useAmbientMode {
|
||||
self.volumeButtonsListener = VolumeButtonsListener(shouldBeActive: .single(true), valueChanged: { [weak self] in
|
||||
guard let self, let sharedState = self.environment?.sharedState, sharedState.useAmbientMode else {
|
||||
return
|
||||
}
|
||||
sharedState.useAmbientMode = false
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.continueWithOverridingAmbientMode()
|
||||
}
|
||||
self.volumeButtonsListener = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,7 +228,14 @@ final class StoryItemContentComponent: Component {
|
||||
|
||||
private func updateIsProgressPaused() {
|
||||
if let videoNode = self.videoNode {
|
||||
if !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy {
|
||||
var canPlay = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||
if let component = self.component {
|
||||
if component.item.isPending {
|
||||
canPlay = false
|
||||
}
|
||||
}
|
||||
|
||||
if canPlay {
|
||||
videoNode.play()
|
||||
} else {
|
||||
videoNode.pause()
|
||||
@ -223,7 +247,12 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
|
||||
private func updateProgressTimer() {
|
||||
let needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||
var needsTimer = !self.isProgressPaused && self.contentLoaded && self.hierarchyTrackingLayer.isInHierarchy
|
||||
if let component = self.component {
|
||||
if component.item.isPending {
|
||||
needsTimer = false
|
||||
}
|
||||
}
|
||||
|
||||
if needsTimer {
|
||||
if self.currentProgressTimer == nil {
|
||||
@ -245,8 +274,8 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG && false
|
||||
let currentProgressTimerLimit: Double = 1 * 60.0
|
||||
#if DEBUG && true
|
||||
let currentProgressTimerLimit: Double = 10.0
|
||||
#else
|
||||
let currentProgressTimerLimit: Double = 5.0
|
||||
#endif
|
||||
|
||||
@ -18,6 +18,7 @@ swift_library(
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||
"//submodules/SemanticStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -8,6 +8,8 @@ import AnimatedAvatarSetNode
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MoreHeaderButton
|
||||
import SemanticStatusNode
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class StoryFooterPanelComponent: Component {
|
||||
public let context: AccountContext
|
||||
@ -53,12 +55,19 @@ public final class StoryFooterPanelComponent: Component {
|
||||
private let deleteButton = ComponentView<Empty>()
|
||||
private var moreButton: MoreHeaderButton?
|
||||
|
||||
private var statusButton: HighlightableButton?
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private var uploadingText: ComponentView<Empty>?
|
||||
|
||||
private let avatarsContext: AnimatedAvatarSetContext
|
||||
private let avatarsNode: AnimatedAvatarSetNode
|
||||
|
||||
private var component: StoryFooterPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var uploadProgress: Float = 0.0
|
||||
private var uploadProgressDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.viewStatsButton = HighlightableButton()
|
||||
|
||||
@ -78,6 +87,10 @@ public final class StoryFooterPanelComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.uploadProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
@objc private func viewStatsPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -85,7 +98,37 @@ public final class StoryFooterPanelComponent: Component {
|
||||
component.expandViewStats()
|
||||
}
|
||||
|
||||
@objc private func statusPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let storyItem = component.storyItem else {
|
||||
return
|
||||
}
|
||||
component.context.engine.messages.cancelStoryUpload(stableId: storyItem.id)
|
||||
}
|
||||
|
||||
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if self.component?.storyItem?.id != component.storyItem?.id || self.component?.storyItem?.isPending != component.storyItem?.isPending {
|
||||
self.uploadProgressDisposable?.dispose()
|
||||
self.uploadProgress = 0.0
|
||||
|
||||
if let storyItem = component.storyItem, storyItem.isPending {
|
||||
var applyState = false
|
||||
self.uploadProgressDisposable = (component.context.engine.messages.storyUploadProgress(stableId: storyItem.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.uploadProgress = progress
|
||||
if applyState {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@ -96,6 +139,84 @@ public final class StoryFooterPanelComponent: Component {
|
||||
|
||||
let avatarSpacing: CGFloat = 18.0
|
||||
|
||||
let avatarsAlpha: CGFloat
|
||||
let baseViewCountAlpha: CGFloat
|
||||
if let storyItem = component.storyItem, storyItem.isPending {
|
||||
baseViewCountAlpha = 0.0
|
||||
|
||||
let statusButton: HighlightableButton
|
||||
if let current = self.statusButton {
|
||||
statusButton = current
|
||||
} else {
|
||||
statusButton = HighlightableButton()
|
||||
statusButton.addTarget(self, action: #selector(self.statusPressed), for: .touchUpInside)
|
||||
self.statusButton = statusButton
|
||||
self.addSubview(statusButton)
|
||||
}
|
||||
|
||||
let statusNode: SemanticStatusNode
|
||||
if let current = self.statusNode {
|
||||
statusNode = current
|
||||
} else {
|
||||
statusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white, image: nil, overlayForegroundNodeColor: nil, cutout: nil)
|
||||
self.statusNode = statusNode
|
||||
statusButton.addSubview(statusNode.view)
|
||||
}
|
||||
|
||||
let uploadingText: ComponentView<Empty>
|
||||
if let current = self.uploadingText {
|
||||
uploadingText = current
|
||||
} else {
|
||||
uploadingText = ComponentView()
|
||||
self.uploadingText = uploadingText
|
||||
}
|
||||
|
||||
var innerLeftOffset: CGFloat = 0.0
|
||||
|
||||
let statusSize = CGSize(width: 36.0, height: 36.0)
|
||||
statusNode.view.frame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - statusSize.height) * 0.5)), size: statusSize)
|
||||
innerLeftOffset += statusSize.width + 10.0
|
||||
|
||||
statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0)))
|
||||
|
||||
//TODO:localize
|
||||
let uploadingTextSize = uploadingText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "Uploading...", font: Font.regular(15.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||
)
|
||||
let uploadingTextFrame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - uploadingTextSize.height) * 0.5)), size: uploadingTextSize)
|
||||
if let uploadingTextView = uploadingText.view {
|
||||
if uploadingTextView.superview == nil {
|
||||
statusButton.addSubview(uploadingTextView)
|
||||
}
|
||||
uploadingTextView.frame = uploadingTextFrame
|
||||
}
|
||||
innerLeftOffset += uploadingTextSize.width + 8.0
|
||||
|
||||
transition.setFrame(view: statusButton, frame: CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: CGSize(width: innerLeftOffset, height: size.height)))
|
||||
leftOffset += innerLeftOffset
|
||||
|
||||
avatarsAlpha = 0.0
|
||||
} else {
|
||||
if let statusNode = self.statusNode {
|
||||
self.statusNode = nil
|
||||
statusNode.view.removeFromSuperview()
|
||||
}
|
||||
if let uploadingText = self.uploadingText {
|
||||
self.uploadingText = nil
|
||||
uploadingText.view?.removeFromSuperview()
|
||||
}
|
||||
if let statusButton = self.statusButton {
|
||||
self.statusButton = nil
|
||||
statusButton.removeFromSuperview()
|
||||
}
|
||||
|
||||
avatarsAlpha = pow(1.0 - component.expandFraction, 1.0)
|
||||
baseViewCountAlpha = 1.0
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
if let seenPeers = component.storyItem?.views?.seenPeers {
|
||||
peers = Array(seenPeers.prefix(3))
|
||||
@ -105,8 +226,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
|
||||
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
|
||||
self.avatarsNode.frame = avatarsNodeFrame
|
||||
//transition.setScale(view: self.avatarsNode.view, scale: CGFloat(1.0).interpolate(to: 0.001, amount: component.expandFraction))
|
||||
transition.setAlpha(view: self.avatarsNode.view, alpha: pow(1.0 - component.expandFraction, 1.0))
|
||||
transition.setAlpha(view: self.avatarsNode.view, alpha: avatarsAlpha)
|
||||
if !avatarsSize.width.isZero {
|
||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||
}
|
||||
@ -154,7 +274,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.center)
|
||||
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
|
||||
transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2))
|
||||
transition.setAlpha(view: viewStatsTextView, alpha: pow(1.0 - component.expandFraction, 1.2) * baseViewCountAlpha)
|
||||
transition.setScale(view: viewStatsTextView, scale: viewStatsCurrentFrame.width / viewStatsTextFrame.width)
|
||||
}
|
||||
|
||||
@ -166,7 +286,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: viewStatsExpandedTextView, position: viewStatsExpandedTextFrame.center)
|
||||
transition.setBounds(view: viewStatsExpandedTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsExpandedTextFrame.size))
|
||||
transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2))
|
||||
transition.setAlpha(view: viewStatsExpandedTextView, alpha: pow(component.expandFraction, 1.2) * baseViewCountAlpha)
|
||||
transition.setScale(view: viewStatsExpandedTextView, scale: viewStatsCurrentFrame.width / viewStatsExpandedTextFrame.width)
|
||||
}
|
||||
|
||||
@ -199,7 +319,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
transition.setFrame(view: deleteButtonView, frame: CGRect(origin: CGPoint(x: rightContentOffset - deleteButtonSize.width, y: floor((size.height - deleteButtonSize.height) * 0.5)), size: deleteButtonSize))
|
||||
rightContentOffset -= deleteButtonSize.width + 8.0
|
||||
|
||||
transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0))
|
||||
transition.setAlpha(view: deleteButtonView, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha)
|
||||
}
|
||||
|
||||
let moreButton: MoreHeaderButton
|
||||
@ -235,7 +355,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
let buttonSize = CGSize(width: 32.0, height: 44.0)
|
||||
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white)))
|
||||
transition.setFrame(view: moreButton.view, frame: CGRect(origin: CGPoint(x: rightContentOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize))
|
||||
transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0))
|
||||
transition.setAlpha(view: moreButton.view, alpha: pow(1.0 - component.expandFraction, 1.0) * baseViewCountAlpha)
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let sideInset: CGFloat
|
||||
public let includesHidden: Bool
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let collapseFraction: CGFloat
|
||||
@ -36,6 +37,7 @@ public final class StoryPeerListComponent: Component {
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
includesHidden: Bool,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
collapseFraction: CGFloat,
|
||||
@ -48,6 +50,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.includesHidden = includesHidden
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.collapseFraction = collapseFraction
|
||||
@ -67,6 +70,9 @@ public final class StoryPeerListComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.includesHidden != rhs.includesHidden {
|
||||
return false
|
||||
}
|
||||
@ -529,7 +535,7 @@ public final class StoryPeerListComponent: Component {
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: availableSize,
|
||||
containerInsets: UIEdgeInsets(top: 4.0, left: 10.0, bottom: 0.0, right: 10.0),
|
||||
containerInsets: UIEdgeInsets(top: 4.0, left: component.sideInset - 4.0, bottom: 0.0, right: component.sideInset - 4.0),
|
||||
itemSize: CGSize(width: 60.0, height: 77.0),
|
||||
itemSpacing: 24.0,
|
||||
itemCount: self.sortedItems.count
|
||||
|
||||
@ -708,7 +708,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: titleTransition,
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleString, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
maximumNumberOfLines: 1
|
||||
|
||||
@ -97,6 +97,7 @@ import LegacyInstantVideoController
|
||||
import StoryContainerScreen
|
||||
import StoryContentComponent
|
||||
import MoreHeaderButton
|
||||
import VolumeButtons
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
@ -4532,7 +4533,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: result,
|
||||
sourceRect: result.bounds,
|
||||
sourceCornerRadius: 2.0
|
||||
sourceCornerRadius: 2.0,
|
||||
sourceIsAvatar: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1180,6 +1180,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in
|
||||
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
})
|
||||
} else if let contentImageNode = self.contentImageNode, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) {
|
||||
return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in
|
||||
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ private enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
hasNewStories: false
|
||||
storyState: nil
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
@ -268,7 +268,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, openPremiumIntro: {
|
||||
}, openChatFolderUpdates: {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _ in
|
||||
}, openStories: { _, _ in
|
||||
})
|
||||
interaction.searchTextHighightState = searchQuery
|
||||
self.interaction = interaction
|
||||
|
||||
@ -157,9 +157,15 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
||||
resolvedKeepStack = false
|
||||
}
|
||||
if resolvedKeepStack {
|
||||
params.navigationController.pushViewController(controller, animated: params.animated, completion: {
|
||||
params.completion(controller)
|
||||
})
|
||||
if let pushController = params.pushController {
|
||||
pushController(controller, params.animated, {
|
||||
params.completion(controller)
|
||||
})
|
||||
} else {
|
||||
params.navigationController.pushViewController(controller, animated: params.animated, completion: {
|
||||
params.completion(controller)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let viewControllers = params.navigationController.viewControllers.filter({ controller in
|
||||
if controller is ForumCreateTopicScreen {
|
||||
|
||||
@ -42,14 +42,67 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
let _ = (storyContent.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak navigationController] _ in
|
||||
let transitionIn: StoryContainerScreen.TransitionIn? = nil
|
||||
var transitionIn: StoryContainerScreen.TransitionIn? = nil
|
||||
|
||||
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
selectedTransitionNode = params.transitionNode(params.message.id, story)
|
||||
|
||||
if let selectedTransitionNode {
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: selectedTransitionNode.0.view,
|
||||
sourceRect: selectedTransitionNode.1,
|
||||
sourceCornerRadius: 0.0,
|
||||
sourceIsAvatar: false
|
||||
)
|
||||
}
|
||||
|
||||
let hiddenMediaSource = params.context.sharedContext.mediaManager.galleryHiddenMediaManager.addSource(.single(GalleryHiddenMediaId.chat(params.context.account.id, params.message.id, story)))
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: context,
|
||||
content: storyContent,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { _, _ in
|
||||
let transitionOut: StoryContainerScreen.TransitionOut? = nil
|
||||
var transitionOut: StoryContainerScreen.TransitionOut? = nil
|
||||
|
||||
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
selectedTransitionNode = params.transitionNode(params.message.id, story)
|
||||
if let selectedTransitionNode {
|
||||
transitionOut = StoryContainerScreen.TransitionOut(
|
||||
destinationView: selectedTransitionNode.0.view,
|
||||
transitionView: StoryContainerScreen.TransitionView(
|
||||
makeView: {
|
||||
let view = UIView()
|
||||
if let transitionView = selectedTransitionNode.2().0 {
|
||||
transitionView.layer.anchorPoint = CGPoint()
|
||||
view.addSubview(transitionView)
|
||||
}
|
||||
return view
|
||||
},
|
||||
updateView: { view, state, transition in
|
||||
guard let view = view.subviews.first else {
|
||||
return
|
||||
}
|
||||
if state.progress == 0.0 {
|
||||
view.frame = CGRect(origin: CGPoint(), size: state.sourceSize)
|
||||
}
|
||||
|
||||
let toScale = state.sourceSize.width / state.destinationSize.width
|
||||
let fromScale: CGFloat = 1.0
|
||||
let scale = toScale.interpolate(to: fromScale, amount: state.progress)
|
||||
transition.setTransform(view: view, transform: CATransform3DMakeScale(scale, scale, 1.0))
|
||||
}
|
||||
),
|
||||
destinationRect: selectedTransitionNode.1,
|
||||
destinationCornerRadius: 0.0,
|
||||
destinationIsAvatar: false,
|
||||
completed: {
|
||||
params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource)
|
||||
}
|
||||
|
||||
return transitionOut
|
||||
}
|
||||
|
||||
@ -115,6 +115,9 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
|
||||
self.videoNode.playOnceWithSound(playAndRecord: playAndRecord)
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode() {
|
||||
}
|
||||
|
||||
func pause() {
|
||||
self.videoNode.pause()
|
||||
}
|
||||
|
||||
@ -467,7 +467,9 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
|
||||
transition: Transition(transition),
|
||||
component: AnyComponent(AvatarStoryIndicatorComponent(
|
||||
hasUnseen: hasUnseenStories,
|
||||
isDarkTheme: theme.overallDarkAppearance
|
||||
isDarkTheme: theme.overallDarkAppearance,
|
||||
activeLineWidth: 3.0,
|
||||
inactiveLineWidth: 2.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: self.avatarNode.bounds.size
|
||||
|
||||
@ -4108,7 +4108,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5,
|
||||
sourceIsAvatar: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -348,8 +348,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, period, pin):
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|
||||
/*let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
@ -364,7 +365,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})*/
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({})
|
||||
}
|
||||
@ -420,7 +421,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
commit({})
|
||||
}
|
||||
}
|
||||
case let .video(content, _, values, duration, dimensions, caption):
|
||||
case let .video(content, firstFrameImage, values, duration, dimensions, caption):
|
||||
let adjustments: VideoMediaResourceAdjustments
|
||||
if let valuesData = try? JSONEncoder().encode(values) {
|
||||
let data = MemoryBuffer(data: valuesData)
|
||||
@ -436,9 +437,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(localIdentifier):
|
||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||
}
|
||||
|
||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) }
|
||||
|
||||
if case let .story(storyPrivacy, period, pin) = privacy {
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
/*let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
@ -453,7 +457,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})*/
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({})
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
public let streamVideo: MediaPlayerStreaming
|
||||
public let loopVideo: Bool
|
||||
public let enableSound: Bool
|
||||
public let beginWithAmbientSound: Bool
|
||||
public let baseRate: Double
|
||||
let fetchAutomatically: Bool
|
||||
let onlyFullSizeThumbnail: Bool
|
||||
@ -51,7 +52,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
let hintDimensions: CGSize?
|
||||
let storeAfterDownload: (() -> Void)?
|
||||
|
||||
public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?) {
|
||||
public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, beginWithAmbientSound: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?) {
|
||||
self.id = id
|
||||
self.nativeId = id
|
||||
self.userLocation = userLocation
|
||||
@ -74,6 +75,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
self.streamVideo = streamVideo
|
||||
self.loopVideo = loopVideo
|
||||
self.enableSound = enableSound
|
||||
self.beginWithAmbientSound = beginWithAmbientSound
|
||||
self.baseRate = baseRate
|
||||
self.fetchAutomatically = fetchAutomatically
|
||||
self.onlyFullSizeThumbnail = onlyFullSizeThumbnail
|
||||
@ -91,7 +93,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload)
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, beginWithAmbientSound: self.beginWithAmbientSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload)
|
||||
}
|
||||
|
||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||
@ -113,6 +115,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
private let userLocation: MediaResourceUserLocation
|
||||
private let fileReference: FileMediaReference
|
||||
private let enableSound: Bool
|
||||
private let beginWithAmbientSound: Bool
|
||||
private let loopVideo: Bool
|
||||
private let baseRate: Double
|
||||
private let audioSessionManager: ManagedAudioSession
|
||||
@ -167,12 +170,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
|
||||
private var shouldPlay: Bool = false
|
||||
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil) {
|
||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, beginWithAmbientSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil) {
|
||||
self.postbox = postbox
|
||||
self.userLocation = userLocation
|
||||
self.fileReference = fileReference
|
||||
self.placeholderColor = placeholderColor
|
||||
self.enableSound = enableSound
|
||||
self.beginWithAmbientSound = beginWithAmbientSound
|
||||
self.loopVideo = loopVideo
|
||||
self.baseRate = baseRate
|
||||
self.audioSessionManager = audioSessionManager
|
||||
@ -181,7 +185,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage)
|
||||
self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, ambient: beginWithAmbientSound, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage)
|
||||
|
||||
var actionAtEndImpl: (() -> Void)?
|
||||
if enableSound && !loopVideo {
|
||||
@ -439,6 +443,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
self.player.setForceAudioToSpeaker(forceAudioToSpeaker)
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode() {
|
||||
self.player.continueWithOverridingAmbientMode()
|
||||
}
|
||||
|
||||
func setBaseRate(_ baseRate: Double) {
|
||||
self.player.setBaseRate(baseRate)
|
||||
}
|
||||
|
||||
@ -430,6 +430,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode() {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
}
|
||||
|
||||
|
||||
@ -267,6 +267,9 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode() {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
}
|
||||
|
||||
|
||||
@ -164,6 +164,9 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
func continueWithOverridingAmbientMode() {
|
||||
}
|
||||
|
||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
|
||||
}
|
||||
|
||||
|
||||
19
submodules/Utils/VolumeButtons/BUILD
Normal file
19
submodules/Utils/VolumeButtons/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "VolumeButtons",
|
||||
module_name = "VolumeButtons",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/LegacyComponents",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -5,12 +5,12 @@ import MediaPlayer
|
||||
|
||||
import LegacyComponents
|
||||
|
||||
class VolumeButtonsListener: NSObject {
|
||||
public class VolumeButtonsListener: NSObject {
|
||||
private let handler: PGCameraVolumeButtonHandler
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
init(shouldBeActive: Signal<Bool, NoError>, valueChanged: @escaping () -> Void) {
|
||||
public init(shouldBeActive: Signal<Bool, NoError>, valueChanged: @escaping () -> Void) {
|
||||
var impl: (() -> Void)?
|
||||
|
||||
self.handler = PGCameraVolumeButtonHandler(upButtonPressedBlock: {
|
||||
Loading…
x
Reference in New Issue
Block a user