[WIP] Stories

This commit is contained in:
Ali 2023-06-16 22:42:48 +03:00
parent 0882817bed
commit b64aa1445c
59 changed files with 1601 additions and 365 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, openPremiumIntro: {
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
}, openStories: { _ in
}, openStories: { _, _ in
})
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 : [] },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {},

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/TelegramUniversalVideoContent",
"//submodules/AvatarNode",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/Utils/VolumeButtons",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ swift_library(
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/SemanticStatusNode",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -115,6 +115,9 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
self.videoNode.playOnceWithSound(playAndRecord: playAndRecord)
}
func continueWithOverridingAmbientMode() {
}
func pause() {
self.videoNode.pause()
}

View File

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

View File

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

View File

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

View File

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

View File

@ -430,6 +430,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
}
func continueWithOverridingAmbientMode() {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}

View File

@ -267,6 +267,9 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
}
func continueWithOverridingAmbientMode() {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}

View File

@ -164,6 +164,9 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
}
}
func continueWithOverridingAmbientMode() {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}

View 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",
],
)

View File

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