diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2606f7bf7c..d4092a9efe 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2604,6 +2604,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) } else { + items.append(.action(ContextMenuActionItem(text: "Send Message", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + }))) + items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2671,7 +2683,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }))) - items.append(.action(ContextMenuActionItem(text: "Hide", icon: { theme in + items.append(.action(ContextMenuActionItem(text: "Move to Contacts", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToContacts"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -3727,7 +3739,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController searchContentNode.placeholderNode.frame = previousFrame } + self.chatListDisplayNode.tempAllowAvatarExpansion = true self.requestLayout(transition: .animated(duration: 0.5, curve: .spring)) + self.chatListDisplayNode.tempAllowAvatarExpansion = false //TODO:swap tabs diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 32a607fe0b..6fa08b2bc6 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1690,7 +1690,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { var didBeginSelectingChatsWhileEditing: Bool = false var isEditing: Bool = false - private var tempAllowAvatarExpansion: Bool = false + var tempAllowAvatarExpansion: Bool = false private var tempDisableStoriesAnimations: Bool = false private var tempNavigationScrollingTransition: ContainedViewLayoutTransition? diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index b486e1b250..e1d07a9672 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2807,7 +2807,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyState.hasUnseen, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, - isDarkTheme: item.presentationData.theme.overallDarkAppearance, + theme: item.presentationData.theme, activeLineWidth: 2.0, inactiveLineWidth: 1.0 + UIScreenPixel, counters: nil diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index c7d0d84875..e9e43ff651 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/ComponentFlow", "//submodules/TooltipUI", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index 138295c2c8..404daf4337 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -9,6 +9,7 @@ import AlertUI import PresentationDataUtils import OverlayStatusController import LocalizedPeerData +import UndoUI import TooltipUI func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?, isStories: Bool) -> Signal<[ContextMenuItem], NoError> { @@ -17,16 +18,76 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con return context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.AreVoiceCallsAvailable(id: peerId), - TelegramEngine.EngineData.Item.Peer.AreVideoCallsAvailable(id: peerId) + TelegramEngine.EngineData.Item.Peer.AreVideoCallsAvailable(id: peerId), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) ) - |> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable -> [ContextMenuItem] in + |> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable, notificationSettings -> [ContextMenuItem] in var items: [ContextMenuItem] = [] if isStories { //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Unhide", icon: { theme in + items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.dismiss(completion: { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (contactsController?.navigationController as? NavigationController)?.pushViewController(controller) + }) + }) + }))) + + let isMuted = notificationSettings.storiesMuted == true + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + + let _ = context.engine.peers.togglePeerStoriesMuted(peerId: peerId).start() + + if let peer { + let iconColor = UIColor.white + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if isMuted { + contactsController?.present(UndoOverlayController( + presentationData: presentationData, + content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: "You will now get a notification whenever **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } else { + contactsController?.present(UndoOverlayController( + presentationData: presentationData, + content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: "You will no longer receive a notification when **\(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** posts a story.", customUndoText: nil, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } + } + }))) + + items.append(.action(ContextMenuActionItem(text: "Move to chats", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MoveToChats"), color: theme.contextMenu.primaryColor) - }, action: { [weak contactsController] _, f in + }, action: { _, f in f(.default) context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false) @@ -45,6 +106,8 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con ) contactsController?.present(tooltipController, in: .window(.root)) }))) + + return items } items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_SendMessage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.contextMenu.primaryColor) }, action: { _, f in diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 47d3505934..3014b5126b 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -559,67 +559,6 @@ public class ContactsController: ViewController { self.push(storyContainerScreen) }) } - - /*componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in - guard let self else { - return - } - - var items: [ContextMenuItem] = [] - - //TODO:localize - if peer.id == self.context.account.peerId { - } else { - items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self else { - return - } - guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { - return - } - (self.navigationController as? NavigationController)?.pushViewController(controller) - }) - }) - }))) - /*items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - })))*/ - - if case let .user(user) = peer, let storiesHidden = user.storiesHidden, storiesHidden { - items.append(.action(ContextMenuActionItem(text: "Unarchive", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - guard let self else { - return - } - self.context.engine.peers.updatePeerStoriesHidden(id: peer.id, isHidden: false) - }))) - } - } - - if items.isEmpty { - return - } - - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) - self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) - } - }*/ } @objc private func sortPressed() { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index c27de73dfe..0b46c1cb32 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -527,10 +527,18 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { guard let contactsController = self.controller else { return } - let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) - chatController.canReadHistory.set(false) - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - contactsController.presentInGlobalOverlay(contextController) + + let items = contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) } + + if isStories, let node = node?.subnodes?.first(where: { $0 is ContextExtractedContentContainingNode }) as? ContextExtractedContentContainingNode { + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactContextExtractedContentSource(sourceNode: node, shouldBeDismissed: .single(false))), items: items, recognizer: nil, gesture: gesture) + contactsController.presentInGlobalOverlay(controller) + } else { + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: items, gesture: gesture) + contactsController.presentInGlobalOverlay(contextController) + } } func activateSearch(placeholderNode: SearchBarPlaceholderNode) { @@ -616,3 +624,26 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } } + +private final class ContactContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + + let shouldBeDismissed: Signal + + private let sourceNode: ContextExtractedContentContainingNode + + init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal? = nil) { + self.sourceNode = sourceNode + self.shouldBeDismissed = shouldBeDismissed ?? .single(false) + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index f7c799279c..81f89d6035 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1115,7 +1115,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyStats.unseen != 0, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends, - isDarkTheme: item.presentationData.theme.overallDarkAppearance, + theme: item.presentationData.theme, activeLineWidth: 1.0 + UIScreenPixel, inactiveLineWidth: 1.0 + UIScreenPixel, counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 1242462a17..7fecddfd0b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -105,6 +105,9 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt if (flags & (1 << 1)) != 0 { videoFlags.insert(.supportsStreaming) } + if (flags & (1 << 3)) != 0 { + videoFlags.insert(.isSilent) + } result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize)) case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 7dd47bf835..855e7ec65e 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -569,6 +569,9 @@ func inputDocumentAttributesFromFileAttributes(_ fileAttributes: [TelegramMediaF if preloadSize != nil { flags |= (1 << 2) } + if videoFlags.contains(.isSilent) { + flags |= (1 << 3) + } attributes.append(.documentAttributeVideo(flags: flags, duration: duration, w: Int32(size.width), h: Int32(size.height), preloadPrefixSize: preloadSize)) case let .Audio(isVoice, duration, title, performer, waveform): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 38ee46bc30..0dbfbf7aa3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -188,6 +188,7 @@ public struct TelegramMediaVideoFlags: OptionSet { public static let instantRoundVideo = TelegramMediaVideoFlags(rawValue: 1 << 0) public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1) + public static let isSilent = TelegramMediaVideoFlags(rawValue: 1 << 3) } public struct StickerMaskCoords: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 20052eb602..b49037821e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -13,6 +13,7 @@ public extension Stories { case entities case pin case privacy + case isForwardingDisabled case period case randomId } @@ -24,6 +25,7 @@ public extension Stories { public let entities: [MessageTextEntity] public let pin: Bool public let privacy: EngineStoryPrivacy + public let isForwardingDisabled: Bool public let period: Int32 public let randomId: Int64 @@ -35,6 +37,7 @@ public extension Stories { entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, + isForwardingDisabled: Bool, period: Int32, randomId: Int64 ) { @@ -45,6 +48,7 @@ public extension Stories { self.entities = entities self.pin = pin self.privacy = privacy + self.isForwardingDisabled = isForwardingDisabled self.period = period self.randomId = randomId } @@ -62,6 +66,7 @@ public extension Stories { 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.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.period = try container.decode(Int32.self, forKey: .period) self.randomId = try container.decode(Int64.self, forKey: .randomId) } @@ -80,6 +85,7 @@ public extension Stories { 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.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.period, forKey: .period) try container.encode(self.randomId, forKey: .randomId) } @@ -106,6 +112,9 @@ public extension Stories { if lhs.privacy != rhs.privacy { return false } + if lhs.isForwardingDisabled != rhs.isForwardingDisabled { + return false + } if lhs.period != rhs.period { return false } @@ -260,7 +269,7 @@ final class PendingStoryManager { 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) + 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, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId) |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index ae962bed6a..fd85989db6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -125,6 +125,7 @@ public enum Stories { case isExpired case isPublic case isCloseFriends + case isForwardingDisabled } public let id: Int32 @@ -139,6 +140,7 @@ public enum Stories { public let isExpired: Bool public let isPublic: Bool public let isCloseFriends: Bool + public let isForwardingDisabled: Bool public init( id: Int32, @@ -152,7 +154,8 @@ public enum Stories { isPinned: Bool, isExpired: Bool, isPublic: Bool, - isCloseFriends: Bool + isCloseFriends: Bool, + isForwardingDisabled: Bool ) { self.id = id self.timestamp = timestamp @@ -166,6 +169,7 @@ public enum Stories { self.isExpired = isExpired self.isPublic = isPublic self.isCloseFriends = isCloseFriends + self.isForwardingDisabled = isForwardingDisabled } public init(from decoder: Decoder) throws { @@ -189,6 +193,7 @@ public enum Stories { self.isExpired = try container.decodeIfPresent(Bool.self, forKey: .isExpired) ?? false self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false + self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false } public func encode(to encoder: Encoder) throws { @@ -213,6 +218,7 @@ public enum Stories { try container.encode(self.isExpired, forKey: .isExpired) try container.encode(self.isPublic, forKey: .isPublic) try container.encode(self.isCloseFriends, forKey: .isCloseFriends) + try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -260,6 +266,9 @@ public enum Stories { if lhs.isCloseFriends != rhs.isCloseFriends { return false } + if lhs.isForwardingDisabled != rhs.isForwardingDisabled { + return false + } return true } @@ -680,7 +689,7 @@ 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) { +func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) { let inputMedia = prepareUploadStoryContent(account: account, media: media) let _ = (account.postbox.transaction { transaction in @@ -702,6 +711,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: entities: entities, pin: pin, privacy: privacy, + isForwardingDisabled: isForwardingDisabled, period: Int32(period), randomId: randomId )) @@ -747,7 +757,7 @@ private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId: } } -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 { +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, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal { let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods) return contentSignal |> mapToSignal { result -> Signal in @@ -787,6 +797,10 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId flags |= 1 << 3 + if isForwardingDisabled { + flags |= 1 << 4 + } + return network.request(Api.functions.stories.sendStory( flags: flags, media: inputMedia, @@ -835,7 +849,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isPinned: item.isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)) @@ -983,7 +998,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isPinned: item.isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { transaction.setStory(id: storyId, value: entry) @@ -1005,7 +1021,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isPinned: item.isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) @@ -1136,7 +1153,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isPinned: isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) @@ -1157,7 +1175,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isPinned: isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) updatedItems.append(updatedItem) } @@ -1253,6 +1272,7 @@ extension Stories.StoredItem { let isExpired = (flags & (1 << 6)) != 0 let isPublic = (flags & (1 << 7)) != 0 let isCloseFriends = (flags & (1 << 8)) != 0 + let isForwardingDisabled = (flags & (1 << 10)) != 0 let item = Stories.Item( id: id, @@ -1266,7 +1286,8 @@ extension Stories.StoredItem { isPinned: isPinned, isExpired: isExpired, isPublic: isPublic, - isCloseFriends: isCloseFriends + isCloseFriends: isCloseFriends, + isForwardingDisabled: isForwardingDisabled ) self = .item(item) } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 095185c284..26c0ce6330 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -44,8 +44,9 @@ public final class EngineStoryItem: Equatable { public let isPublic: Bool public let isPending: Bool public let isCloseFriends: Bool + public let isForwardingDisabled: 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, isCloseFriends: 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, isCloseFriends: Bool, isForwardingDisabled: Bool) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -59,6 +60,7 @@ public final class EngineStoryItem: Equatable { self.isPublic = isPublic self.isPending = isPending self.isCloseFriends = isCloseFriends + self.isForwardingDisabled = isForwardingDisabled } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -101,6 +103,9 @@ public final class EngineStoryItem: Equatable { if lhs.isCloseFriends != rhs.isCloseFriends { return false } + if lhs.isForwardingDisabled != rhs.isForwardingDisabled { + return false + } return true } } @@ -129,7 +134,8 @@ extension EngineStoryItem { isPinned: self.isPinned, isExpired: self.isExpired, isPublic: self.isPublic, - isCloseFriends: self.isCloseFriends + isCloseFriends: self.isCloseFriends, + isForwardingDisabled: self.isForwardingDisabled ) } } @@ -487,7 +493,8 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) items.append(mappedItem) } @@ -593,7 +600,8 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) storyItems.append(mappedItem) } @@ -726,7 +734,8 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) finalUpdatedState = updatedState } @@ -764,7 +773,8 @@ public final class PeerStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -911,7 +921,8 @@ public final class PeerExpiringStoryListContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) items.append(.item(mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 1544e1d43d..322853aeaf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -966,7 +966,8 @@ public extension TelegramEngine { isPinned: item.isPinned, isExpired: item.isExpired, isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) @@ -980,8 +981,8 @@ public extension TelegramEngine { } } - 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 uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) { + _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId) } public func lookUpPendingStoryIdMapping(stableId: Int32) -> Int32? { diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 030abd5b99..8c4a386330 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -214,7 +214,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyData.hasUnseen, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, - isDarkTheme: theme.overallDarkAppearance, + theme: theme, activeLineWidth: 1.0, inactiveLineWidth: 1.0, counters: nil diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index cfd558d152..f38b194b2b 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -312,6 +312,7 @@ public final class ChatListHeaderComponent: Component { var contentOffsetFraction: CGFloat = 0.0 private(set) var centerContentWidth: CGFloat = 0.0 + private(set) var centerContentLeftInset: CGFloat = 0.0 private(set) var centerContentRightInset: CGFloat = 0.0 private(set) var centerContentOffsetX: CGFloat = 0.0 @@ -639,8 +640,11 @@ public final class ChatListHeaderComponent: Component { } } + var centerContentLeftInset: CGFloat = 0.0 + centerContentLeftInset = leftOffset - 4.0 + var centerContentRightInset: CGFloat = 0.0 - centerContentRightInset = size.width - rightOffset + 16.0 + centerContentRightInset = size.width - rightOffset - 8.0 var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 @@ -705,6 +709,7 @@ public final class ChatListHeaderComponent: Component { self.centerContentOffsetX = centerContentOffsetX self.centerContentOrigin = centerContentOrigin self.centerContentRightInset = centerContentRightInset + self.centerContentLeftInset = centerContentLeftInset } } @@ -815,7 +820,7 @@ public final class ChatListHeaderComponent: Component { let previousComponent = self.component self.component = component - if let primaryContent = component.primaryContent { + if var primaryContent = component.primaryContent { var primaryContentTransition = transition let primaryContentView: ContentView if let current = self.primaryContentView { @@ -848,6 +853,19 @@ public final class ChatListHeaderComponent: Component { let sideContentWidth: CGFloat = 0.0 + if component.storySubscriptions != nil { + primaryContent = Content( + title: "", + navigationBackTitle: primaryContent.navigationBackTitle, + titleComponent: nil, + chatListTitle: nil, + leftButton: primaryContent.leftButton, + rightButtons: primaryContent.rightButtons, + backTitle: primaryContent.backTitle, + backPressed: primaryContent.backPressed + ) + } + primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -868,6 +886,28 @@ public final class ChatListHeaderComponent: Component { self.storyPeerList = storyPeerList } + var primaryTitle = "" + var primaryTitleHasLock = false + var primaryTitleHasActivity = false + var primaryTitlePeerStatus: StoryPeerListComponent.PeerStatus? + if let primaryContent = component.primaryContent { + if let chatListTitle = primaryContent.chatListTitle { + primaryTitle = chatListTitle.text + primaryTitleHasLock = chatListTitle.isPasscodeSet + primaryTitleHasActivity = chatListTitle.activity + if let peerStatus = chatListTitle.peerStatus { + switch peerStatus { + case .premium: + primaryTitlePeerStatus = .premium + case let .emoji(status): + primaryTitlePeerStatus = .emoji(status) + } + } + } else { + primaryTitle = primaryContent.title + } + } + let _ = storyPeerList.update( transition: storyListTransition, component: AnyComponent(StoryPeerListComponent( @@ -876,7 +916,11 @@ public final class ChatListHeaderComponent: Component { theme: component.theme, strings: component.strings, sideInset: component.sideInset, - titleContentWidth: self.primaryContentView?.centerContentWidth ?? 0.0, + title: primaryTitle, + titleHasLock: primaryTitleHasLock, + titleHasActivity: primaryTitleHasActivity, + titlePeerStatus: primaryTitlePeerStatus, + minTitleX: self.primaryContentView?.centerContentLeftInset ?? 0.0, maxTitleX: availableSize.width - (self.primaryContentView?.centerContentRightInset ?? 0.0), useHiddenList: component.storiesIncludeHidden, storySubscriptions: storySubscriptions, @@ -895,14 +939,11 @@ public final class ChatListHeaderComponent: Component { } self.storyContextPeerAction?(sourceNode, gesture, peer) }, - updateTitleContentOffset: { [weak self] offset, transition in - guard let self, let primaryContentView = self.primaryContentView else { + openStatusSetup: { [weak self] sourceView in + guard let self else { return } - guard let chatListTitleView = primaryContentView.chatListTitleView else { - return - } - transition.setSublayerTransform(view: chatListTitleView, transform: CATransform3DMakeTranslation(offset, 0.0, 0.0)) + self.component?.openStatusSetup(sourceView) } )), environment: {}, @@ -1002,12 +1043,7 @@ public final class ChatListHeaderComponent: Component { storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + 0.0, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0))) - var storyListNormalAlpha: CGFloat = 1.0 - if let chatListTitle = component.primaryContent?.chatListTitle { - if chatListTitle.activity { - storyListNormalAlpha = component.storiesFraction - } - } + let storyListNormalAlpha: CGFloat = 1.0 let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD new file mode 100644 index 0000000000..dc8ed1d04c --- /dev/null +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmptyStateIndicatorComponent", + module_name = "EmptyStateIndicatorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/AnimatedStickerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift new file mode 100644 index 0000000000..6b4afe2853 --- /dev/null +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AnimatedStickerComponent +import ButtonComponent +import TelegramPresentationData +import AccountContext +import MultilineTextComponent + +public final class EmptyStateIndicatorComponent: Component { + public let context: AccountContext + public let theme: PresentationTheme + public let animationName: String + public let title: String + public let text: String + public let actionTitle: String + public let action: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + animationName: String, + title: String, + text: String, + actionTitle: String, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.animationName = animationName + self.title = title + self.text = text + self.actionTitle = actionTitle + self.action = action + } + + public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.animationName != rhs.animationName { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.actionTitle != rhs.actionTitle { + return false + } + return true + } + + public final class View: UIView { + private var component: EmptyStateIndicatorComponent? + private weak var componentState: EmptyComponentState? + + private let animation = ComponentView() + private let title = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + public func update(component: EmptyStateIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.componentState = state + + let animationSize = self.animation.update( + transition: transition, + component: AnyComponent(AnimatedStickerComponent( + account: component.context.account, + animation: AnimatedStickerComponent.Animation(source: .bundle(name: component.animationName), loop: true), + size: CGSize(width: 120.0, height: 120.0) + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 120.0) + ) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity(id: 0, component: AnyComponent( + Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: CGSize(width: 240.0, height: 50.0) + ) + + let animationSpacing: CGFloat = 11.0 + let titleSpacing: CGFloat = 17.0 + let buttonSpacing: CGFloat = 17.0 + + let totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height + buttonSpacing + buttonSize.height + + var contentY = floor((availableSize.height - totalHeight) * 0.5) + + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize)) + contentY += animationSize.height + animationSpacing + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentY), size: titleSize)) + contentY += titleSize.height + titleSpacing + } + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentY), size: textSize)) + contentY += textSize.height + buttonSpacing + } + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) + contentY += buttonSize.height + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index f6d37aa4e8..be12de4009 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -131,31 +131,33 @@ final class PeerInfoStoryGridScreenComponent: Component { paneNode.clearSelection() }))) - } else { - var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? - let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in - let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement - let canZoom: Bool = nextZoomLevel != nil + } else if let paneNode = self.paneNode { + if !paneNode.isEmpty { + var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? + let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in + let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement + let canZoom: Bool = nextZoomLevel != nil + + return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) + }, action: canZoom ? { action in + guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { + return + } + pane.updateZoomLevel(level: zoomLevel) + if let recurseGenerateAction = recurseGenerateAction { + action.updateAction(0, recurseGenerateAction(true)) + action.updateAction(1, recurseGenerateAction(false)) + } + } : nil) + } + recurseGenerateAction = { isZoomIn in + return generateAction(isZoomIn) + } - return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) - }, action: canZoom ? { action in - guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { - return - } - pane.updateZoomLevel(level: zoomLevel) - if let recurseGenerateAction = recurseGenerateAction { - action.updateAction(0, recurseGenerateAction(true)) - action.updateAction(1, recurseGenerateAction(false)) - } - } : nil) + items.append(.action(generateAction(true))) + items.append(.action(generateAction(false))) } - recurseGenerateAction = { isZoomIn in - return generateAction(isZoomIn) - } - - items.append(.action(generateAction(true))) - items.append(.action(generateAction(false))) if component.peerId == component.context.account.peerId, case .saved = component.scope { var ignoreNextActions = false @@ -392,6 +394,13 @@ final class PeerInfoStoryGridScreenComponent: Component { self.paneNode = paneNode self.addSubview(paneNode.view) + paneNode.emptyAction = { [weak self] in + guard let self, let component = self.component else { + return + } + self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive)) + } + self.paneStatusDisposable = (paneNode.status |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 712da58eaa..b861093abe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/InvisibleInkDustNode", "//submodules/MediaPickerUI", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", + "//submodules/TelegramUI/Components/EmptyStateIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 77f5e9b15a..73c8f528d2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -26,6 +26,7 @@ import AppBundle import InvisibleInkDustNode import MediaPickerUI import StoryContainerScreen +import EmptyStateIndicatorComponent private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -849,6 +850,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return result } + public var isEmpty: Bool { + if let items = self.items, items.items.count != 0 { + return false + } else { + return true + } + } + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() @@ -883,6 +892,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? + public var emptyAction: (() -> Void)? private weak var currentGestureItem: SparseItemGridDisplayItem? @@ -892,6 +902,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private weak var pendingOpenListContext: PeerStoryListContentContextImpl? private var preloadArchiveListContext: PeerStoryListContext? + + private var emptyStateView: ComponentView? public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context @@ -1863,6 +1875,67 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + if let items = self.items, items.items.isEmpty, items.count == 0, !self.isArchive { + let emptyStateView: ComponentView + var emptyStateTransition = Transition(transition) + if let current = self.emptyStateView { + emptyStateView = current + } else { + emptyStateTransition = .immediate + emptyStateView = ComponentView() + self.emptyStateView = emptyStateView + } + //TODO:localize + let emptyStateSize = emptyStateView.update( + transition: emptyStateTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: presentationData.theme, + animationName: "StoryListEmpty", + title: "No saved stories", + text: "Open the Archive to select stories you\nwant to be displayed in your profile.", + actionTitle: "Open Archive", + action: { [weak self] in + guard let self else { + return + } + self.emptyAction?() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: size.height - topInset - bottomInset) + ) + if let emptyStateComponentView = emptyStateView.view { + if emptyStateComponentView.superview == nil { + self.view.addSubview(emptyStateComponentView) + if self.didUpdateItemsOnce { + emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + emptyStateTransition.setFrame(view: emptyStateComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: topInset), size: emptyStateSize)) + } + if self.didUpdateItemsOnce { + Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else { + self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + } else { + if let emptyStateView = self.emptyStateView { + let subTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + self.emptyStateView = nil + + if let emptyStateComponentView = emptyStateView.view { + subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in + emptyStateComponentView?.removeFromSuperview() + }) + } + + subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else { + self.view.backgroundColor = .clear + } + } transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) if let items = self.items { diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index 08ed7b98ea..9f526295f0 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -17,7 +17,7 @@ public final class AvatarStoryIndicatorComponent: Component { public let hasUnseen: Bool public let hasUnseenCloseFriendsItems: Bool - public let isDarkTheme: Bool + public let theme: PresentationTheme public let activeLineWidth: CGFloat public let inactiveLineWidth: CGFloat public let counters: Counters? @@ -25,14 +25,14 @@ public final class AvatarStoryIndicatorComponent: Component { public init( hasUnseen: Bool, hasUnseenCloseFriendsItems: Bool, - isDarkTheme: Bool, + theme: PresentationTheme, activeLineWidth: CGFloat, inactiveLineWidth: CGFloat, counters: Counters? ) { self.hasUnseen = hasUnseen self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems - self.isDarkTheme = isDarkTheme + self.theme = theme self.activeLineWidth = activeLineWidth self.inactiveLineWidth = inactiveLineWidth self.counters = counters @@ -45,7 +45,7 @@ public final class AvatarStoryIndicatorComponent: Component { if lhs.hasUnseenCloseFriendsItems != rhs.hasUnseenCloseFriendsItems { return false } - if lhs.isDarkTheme != rhs.isDarkTheme { + if lhs.theme !== rhs.theme { return false } if lhs.activeLineWidth != rhs.activeLineWidth { @@ -112,8 +112,8 @@ public final class AvatarStoryIndicatorComponent: Component { ] } - if component.isDarkTheme { - inactiveColors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + if component.theme.overallDarkAppearance { + inactiveColors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor] } else { inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index f465ab18bc..e639f98dc7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -138,7 +138,8 @@ public final class StoryContentContextImpl: StoryContentContext { isExpired: item.isExpired, isPublic: item.isPublic, isPending: false, - isCloseFriends: item.isCloseFriends + isCloseFriends: item.isCloseFriends, + isForwardingDisabled: item.isForwardingDisabled ) } if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { @@ -156,7 +157,8 @@ public final class StoryContentContextImpl: StoryContentContext { isExpired: false, isPublic: false, isPending: true, - isCloseFriends: false + isCloseFriends: false, + isForwardingDisabled: false )) } } @@ -954,7 +956,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isExpired: itemValue.isExpired, isPublic: itemValue.isPublic, isPending: false, - isCloseFriends: itemValue.isCloseFriends + isCloseFriends: itemValue.isCloseFriends, + isForwardingDisabled: itemValue.isForwardingDisabled ) let mainItem = StoryContentItem( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index c0adfdb3bf..48550f5008 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -392,30 +392,46 @@ private final class StoryContainerScreenComponent: Component { let translation = recognizer.translation(in: self) self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true) self.state?.updated(transition: .immediate) - case .cancelled, .ended: - let translation = recognizer.translation(in: self) - let velocity = recognizer.velocity(in: self) - self.verticalPanState = nil - var updateState = true - - if translation.y > 100.0 || velocity.y > 10.0 { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - self.environment?.controller()?.dismiss() - } else if translation.y < -100.0 || velocity.y < -40.0 { + if translation.y < -40.0 { if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { - if itemSetComponentView.activateInput() { - updateState = false + if let activateInputWhileDragging = itemSetComponentView.activateInputWhileDragging() { + activateInputWhileDragging() + + self.verticalPanState = nil + recognizer.state = .cancelled + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) } } } + } + case .cancelled, .ended: + if self.verticalPanState != nil { + let translation = recognizer.translation(in: self) + let velocity = recognizer.velocity(in: self) - if updateState || "".isEmpty { + self.verticalPanState = nil + var updateState = true + + if translation.y > 100.0 || velocity.y > 10.0 { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.environment?.controller()?.dismiss() + } else if translation.y < -100.0 || velocity.y < -40.0 { + if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + if itemSetComponentView.activateInput() { + updateState = false + } + } + } + + if updateState || "".isEmpty { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + } else { self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) } - } else { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) } default: break diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 0f90d00d73..618e1f64da 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -141,7 +141,7 @@ final class StoryItemContentComponent: Component { useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, tempFilePath: nil, - captureProtected: false, + captureProtected: component.item.isForwardingDisabled, hintDimensions: file.dimensions?.cgSize, storeAfterDownload: nil, displayImage: false @@ -487,9 +487,17 @@ final class StoryItemContentComponent: Component { } if let dimensions { + var imageSize = dimensions.aspectFilled(availableSize) + if imageSize.width < availableSize.width && imageSize.width >= availableSize.width - 5.0 { + imageSize.width = availableSize.width + } + if imageSize.height < availableSize.height && imageSize.height >= availableSize.height - 5.0 { + imageSize.height = availableSize.height + } + self.imageNode.captureProtected = component.item.isForwardingDisabled let apply = self.imageNode.asyncLayout()(TransformImageArguments( corners: ImageCorners(), - imageSize: dimensions.aspectFilled(availableSize), + imageSize: imageSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets() )) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 23c645e0d2..f3729ea6f5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -30,6 +30,7 @@ import LocalMediaResources import SaveToCameraRoll import BundleIconComponent import PeerListItemComponent +import PremiumUI public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -182,16 +183,16 @@ public final class StoryItemSetContainerComponent: Component { } struct ItemLayout { - var size: CGSize + var containerSize: CGSize var contentFrame: CGRect var contentVisualScale: CGFloat init( - size: CGSize, + containerSize: CGSize, contentFrame: CGRect, contentVisualScale: CGFloat ) { - self.size = size + self.containerSize = containerSize self.contentFrame = contentFrame self.contentVisualScale = contentVisualScale } @@ -256,10 +257,6 @@ public final class StoryItemSetContainerComponent: Component { override func touchesShouldCancel(in view: UIView) -> Bool { return true } - - /*@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - }*/ } public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { @@ -567,7 +564,7 @@ public final class StoryItemSetContainerComponent: Component { let point = recognizer.location(in: self) var direction: NavigationDirection? - if point.x < itemLayout.size.width * 0.25 { + if point.x < itemLayout.containerSize.width * 0.25 { direction = .previous } else { direction = .next @@ -670,7 +667,7 @@ public final class StoryItemSetContainerComponent: Component { } self.scrollingCenterX = leftWidth - self.scroller.contentSize = CGSize(width: leftWidth + itemLayout.size.width + rightWidth, height: 1.0) + self.scroller.contentSize = CGSize(width: leftWidth + itemLayout.containerSize.width + rightWidth, height: 1.0) if !self.initializedOffset { self.initializedOffset = true @@ -862,7 +859,7 @@ public final class StoryItemSetContainerComponent: Component { environment: { itemEnvironment }, - containerSize: itemLayout.size + containerSize: itemLayout.contentFrame.size ) if let view = visibleItem.view.view { if visibleItem.contentContainerView.superview == nil { @@ -969,6 +966,22 @@ public final class StoryItemSetContainerComponent: Component { return false } + func activateInputWhileDragging() -> (() -> Void)? { + guard let component = self.component else { + return nil + } + if component.slice.peer.id == component.context.account.peerId { + } else { + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { + return { [weak inputPanelView] in + inputPanelView?.activateInput() + } + } + } + + return nil + } + func animateIn(transitionIn: StoryContainerScreen.TransitionIn) { self.closeButton.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, delay: 0.12, timingFunction: kCAMediaTimingFunctionSpring) @@ -1510,7 +1523,7 @@ public final class StoryItemSetContainerComponent: Component { self.state?.updated(transition: .immediate) }, timeoutAction: nil, - forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in + forwardAction: component.slice.item.storyItem.isPublic && !component.slice.item.storyItem.isForwardingDisabled ? { [weak self] in guard let self else { return } @@ -2048,7 +2061,7 @@ public final class StoryItemSetContainerComponent: Component { let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize) let itemLayout = ItemLayout( - size: contentFrame.size, + containerSize: availableSize, contentFrame: contentFrame, contentVisualScale: contentVisualScale ) @@ -2539,6 +2552,32 @@ public final class StoryItemSetContainerComponent: Component { }) }) } + + reactionContextNode.premiumReactionsSelected = { [weak self] file in + guard let self, let file, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: nil, text: presentationData.strings.Chat_PremiumReactionToastTitle, undoText: presentationData.strings.Chat_PremiumReactionToastAction, customAction: { [weak self] in + guard let self, let component = self.component else { + return + } + + let context = component.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { + let controller = PremiumIntroScreen(context: context, source: .reactions) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + component.controller()?.push(controller) + }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in true }) + //strongSelf.currentUndoController = undoController + component.controller()?.present(undoController, in: .current) + } } var animateReactionsIn = false diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD index d4096a81ff..9fa6297ec4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/ContextUI", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/Components/MultilineTextComponent", + "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 3cd83c0c8f..e43b5f739c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -10,6 +10,7 @@ import Postbox import SwiftSignalKit import TelegramPresentationData import StoryContainerScreen +import EmojiStatusComponent public func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions) -> Bool { if !storySubscriptions.items.isEmpty { @@ -48,6 +49,11 @@ private let modelSpringAnimation: CABasicAnimation = { }() public final class StoryPeerListComponent: Component { + public enum PeerStatus: Equatable { + case premium + case emoji(PeerEmojiStatus) + } + public final class ExternalState { public fileprivate(set) var collapsedWidth: CGFloat = 0.0 @@ -74,7 +80,11 @@ public final class StoryPeerListComponent: Component { public let theme: PresentationTheme public let strings: PresentationStrings public let sideInset: CGFloat - public let titleContentWidth: CGFloat + public let title: String + public let titleHasLock: Bool + public let titleHasActivity: Bool + public let titlePeerStatus: PeerStatus? + public let minTitleX: CGFloat public let maxTitleX: CGFloat public let useHiddenList: Bool public let storySubscriptions: EngineStorySubscriptions? @@ -83,7 +93,7 @@ public final class StoryPeerListComponent: Component { public let uploadProgress: Float? public let peerAction: (EnginePeer?) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void - public let updateTitleContentOffset: (CGFloat, Transition) -> Void + public let openStatusSetup: (UIView) -> Void public init( externalState: ExternalState, @@ -91,7 +101,11 @@ public final class StoryPeerListComponent: Component { theme: PresentationTheme, strings: PresentationStrings, sideInset: CGFloat, - titleContentWidth: CGFloat, + title: String, + titleHasLock: Bool, + titleHasActivity: Bool, + titlePeerStatus: PeerStatus?, + minTitleX: CGFloat, maxTitleX: CGFloat, useHiddenList: Bool, storySubscriptions: EngineStorySubscriptions?, @@ -100,14 +114,18 @@ public final class StoryPeerListComponent: Component { uploadProgress: Float?, peerAction: @escaping (EnginePeer?) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void, - updateTitleContentOffset: @escaping (CGFloat, Transition) -> Void + openStatusSetup: @escaping (UIView) -> Void ) { self.externalState = externalState self.context = context self.theme = theme self.strings = strings self.sideInset = sideInset - self.titleContentWidth = titleContentWidth + self.title = title + self.titleHasLock = titleHasLock + self.titleHasActivity = titleHasActivity + self.titlePeerStatus = titlePeerStatus + self.minTitleX = minTitleX self.maxTitleX = maxTitleX self.useHiddenList = useHiddenList self.storySubscriptions = storySubscriptions @@ -116,7 +134,7 @@ public final class StoryPeerListComponent: Component { self.uploadProgress = uploadProgress self.peerAction = peerAction self.contextPeerAction = contextPeerAction - self.updateTitleContentOffset = updateTitleContentOffset + self.openStatusSetup = openStatusSetup } public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { @@ -132,7 +150,19 @@ public final class StoryPeerListComponent: Component { if lhs.sideInset != rhs.sideInset { return false } - if lhs.titleContentWidth != rhs.titleContentWidth { + if lhs.title != rhs.title { + return false + } + if lhs.titleHasLock != rhs.titleHasLock { + return false + } + if lhs.titleHasActivity != rhs.titleHasActivity { + return false + } + if lhs.titlePeerStatus != rhs.titlePeerStatus { + return false + } + if lhs.minTitleX != rhs.minTitleX { return false } if lhs.maxTitleX != rhs.maxTitleX { @@ -195,7 +225,18 @@ public final class StoryPeerListComponent: Component { } func frame(at index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) + if self.itemCount <= 1 { + return CGRect(origin: CGPoint(x: floor((self.containerSize.width - self.itemSize.width) * 0.5), y: self.containerInsets.top), size: self.itemSize) + } else if self.contentSize.width < self.containerSize.width { + let usableWidth = self.containerSize.width - self.containerInsets.left - self.containerInsets.right + let usableSpacingWidth = usableWidth - self.itemSize.width * CGFloat(self.itemCount) + + var spacing = floor(usableSpacingWidth / CGFloat(self.itemCount + 1)) + spacing = min(120.0, spacing) + return CGRect(origin: CGPoint(x: self.containerInsets.left + spacing + (self.itemSize.width + spacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) + } else { + return CGRect(origin: CGPoint(x: self.containerInsets.left + (self.itemSize.width + self.itemSpacing) * CGFloat(index), y: self.containerInsets.top), size: self.itemSize) + } } } @@ -240,12 +281,19 @@ public final class StoryPeerListComponent: Component { public final class View: UIView, UIScrollViewDelegate { private let collapsedButton: HighlightableButton private let scrollView: ScrollView + private let scrollContainerView: UIView private var ignoreScrolling: Bool = false private var itemLayout: ItemLayout? private var sortedItems: [EngineStorySubscriptions.Item] = [] + private var visibleItems: [EnginePeer.Id: VisibleItem] = [:] + private var visibleCollapsableItems: [EnginePeer.Id: VisibleItem] = [:] + + private var titleIndicatorView: ComponentView? + private let titleView = ComponentView() + private var titleIconView: ComponentView? private var component: StoryPeerListComponent? private weak var state: EmptyComponentState? @@ -275,10 +323,15 @@ public final class StoryPeerListComponent: Component { self.scrollView.alwaysBounceHorizontal = true self.scrollView.clipsToBounds = false + self.scrollContainerView = UIView() + super.init(frame: frame) self.scrollView.delegate = self + self.scrollView.alpha = 0.0 + self.scrollContainerView.addGestureRecognizer(self.scrollView.panGestureRecognizer) self.addSubview(self.scrollView) + self.addSubview(self.scrollContainerView) self.addSubview(self.collapsedButton) self.collapsedButton.highligthedChanged = { [weak self] highlighted in @@ -373,20 +426,116 @@ public final class StoryPeerListComponent: Component { public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate, keepVisibleUntilCompletion: false) + self.updateScrolling(transition: .immediate) } } - private func updateScrolling(transition: Transition, keepVisibleUntilCompletion: Bool) { + private func updateScrolling(transition: Transition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } - var hasStories: Bool = false - if let storySubscriptions = component.storySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions) { - hasStories = true + let titleIconSpacing: CGFloat = 4.0 + let titleIndicatorSpacing: CGFloat = 8.0 + + var titleContentWidth: CGFloat = 0.0 + + var titleIndicatorSize: CGSize? + if component.titleHasActivity { + let titleIndicatorView: ComponentView + if let current = self.titleIndicatorView { + titleIndicatorView = current + } else { + titleIndicatorView = ComponentView() + self.titleIndicatorView = titleIndicatorView + } + let titleIndicatorSizeValue = titleIndicatorView.update( + transition: .immediate, + component: AnyComponent(TitleActivityIndicatorComponent( + color: component.theme.rootController.navigationBar.accentTextColor + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleIndicatorSize = titleIndicatorSizeValue + titleContentWidth += titleIndicatorSizeValue.width + titleIndicatorSpacing + } else { + if let titleIndicatorView = self.titleIndicatorView { + self.titleIndicatorView = nil + titleIndicatorView.view?.removeFromSuperview() + } + } + + let titleSize = self.titleView.update( + transition: .immediate, + component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + titleContentWidth += titleSize.width + + var titleIconSize: CGSize? + if let peerStatus = component.titlePeerStatus { + let statusContent: EmojiStatusComponent.Content + switch peerStatus { + case .premium: + statusContent = .premium(color: component.theme.list.itemAccentColor) + case let .emoji(emoji): + statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) + } + + var animateStatusTransition = false + + let titleIconView: ComponentView + if let current = self.titleIconView { + animateStatusTransition = true + titleIconView = current + } else { + titleIconView = ComponentView() + self.titleIconView = titleIconView + } + + var titleIconTransition: Transition + if animateStatusTransition { + titleIconTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } else { + titleIconTransition = .immediate + } + + let titleIconSizeValue = titleIconView.update( + transition: titleIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: statusContent, + isVisibleForAnimations: true, + action: { [weak self] in + guard let self, let component = self.component, let titleIconView = self.titleIconView?.view else { + return + } + component.openStatusSetup(titleIconView) + } + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + + titleIconSize = titleIconSizeValue + + if let titleIconComponentView = titleIconView.view { + titleIconComponentView.isHidden = component.titleHasActivity + } + + if !component.titleHasActivity { + titleContentWidth += titleIconSpacing + titleIconSizeValue.width + } + } else { + if let titleIconView = self.titleIconView { + self.titleIconView = nil + titleIconView.view?.removeFromSuperview() + } } - let _ = hasStories let collapseStartIndex: Int if component.useHiddenList { @@ -415,13 +564,28 @@ public final class StoryPeerListComponent: Component { let collapsedItemOffsetY: CGFloat let titleContentSpacing: CGFloat = 8.0 - var combinedTitleContentWidth = component.titleContentWidth + var combinedTitleContentWidth = titleContentWidth if !combinedTitleContentWidth.isZero { combinedTitleContentWidth += titleContentSpacing } - let centralContentWidth: CGFloat = collapsedContentWidth + combinedTitleContentWidth + + let centralContentWidth: CGFloat + centralContentWidth = collapsedContentWidth + combinedTitleContentWidth + collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5) + + if component.titleHasActivity { + collapsedContentOrigin -= (collapsedContentWidth + titleContentSpacing) * 0.5 + } + collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0) + + var collapsedContentOriginOffset: CGFloat = 0.0 + + if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 { + collapsedContentOriginOffset += 4.0 + } + collapsedContentOrigin -= collapsedContentOriginOffset collapsedItemOffsetY = -59.0 struct CollapseState { @@ -432,11 +596,6 @@ public final class StoryPeerListComponent: Component { var sideAlphaFraction: CGFloat } - /*let calculateCollapedFraction: (CGFloat) -> CGFloat = { t in - let offset = scrollingRubberBandingOffset(offset: (1.0 - t) * 94.0, bandingStart: 0.0, range: 400.0, coefficient: 0.4) - return 1.0 - max(0.0, min(1.0, offset / 94.0)) - }*/ - let targetExpandedFraction = component.collapseFraction let targetFraction: CGFloat = component.collapseFraction @@ -504,7 +663,7 @@ public final class StoryPeerListComponent: Component { var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) rawProgress = max(0.0, min(1.0, rawProgress)) - if !animationState.fromIsUnlocked && animationState.bounce { + if !animationState.fromIsUnlocked && animationState.bounce && itemLayout.itemCount > 3 { expandBoundsFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: 1.0, toFraction: 0.0) } else { expandBoundsFraction = 0.0 @@ -520,15 +679,6 @@ public final class StoryPeerListComponent: Component { expandBoundsFraction = 0.0 } - let defaultCollapsedTitleOffset = floor((itemLayout.containerSize.width - component.titleContentWidth) * 0.5) - let targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentWidth + titleContentSpacing - let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset - - let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction) - let titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: 0.0 as CGFloat, amount: collapsedState.maxFraction) - - component.updateTitleContentOffset(titleContentOffset, transition) - self.currentFraction = collapsedState.globalFraction component.externalState.collapsedWidth = collapsedContentWidth @@ -536,32 +686,47 @@ public final class StoryPeerListComponent: Component { let effectiveVisibleBounds = self.scrollView.bounds let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0) + var effectiveFirstVisibleIndex = 0 + for i in 0 ..< self.sortedItems.count { + let regularItemFrame = itemLayout.frame(at: i) + let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame) + if isReallyVisible { + effectiveFirstVisibleIndex = i + break + } + } + struct MeasuredItem { var itemFrame: CGRect var itemScale: CGFloat } - let calculateItem: (Int) -> MeasuredItem = { i in - let regularItemFrame = itemLayout.frame(at: i) + let calculateItem: (Int) -> MeasuredItem = { index in + let frameIndex = index + let regularItemFrame = itemLayout.frame(at: frameIndex) let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame) + let collapseIndex = index - effectiveFirstVisibleIndex + let collapsedItemX: CGFloat - if i < collapseStartIndex { + if collapseIndex < collapseStartIndex { collapsedItemX = collapsedContentOrigin - } else if i > collapseEndIndex { + } else if collapseIndex > collapseEndIndex { collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5 } else { - collapsedItemX = collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance + collapsedItemX = collapsedContentOrigin + CGFloat(collapseIndex - collapseStartIndex) * collapsedItemDistance } let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height)) var collapsedMaxItemFrame = collapsedItemFrame - var collapseDistance: CGFloat = CGFloat(i - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex) - collapseDistance = max(0.0, min(1.0, collapseDistance)) - collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0 - collapsedMaxItemFrame.origin.x += collapseDistance * 20.0 - collapsedMaxItemFrame.origin.y += collapseDistance * 20.0 - collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0 + if itemLayout.itemCount > 1 { + var collapseDistance: CGFloat = CGFloat(collapseIndex - collapseStartIndex) / CGFloat(collapseEndIndex - collapseStartIndex) + collapseDistance = max(0.0, min(1.0, collapseDistance)) + collapsedMaxItemFrame.origin.x -= collapsedState.minFraction * 4.0 + collapsedMaxItemFrame.origin.x += collapseDistance * 20.0 + collapsedMaxItemFrame.origin.y += collapseDistance * 20.0 + collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0 + } let minimizedItemScale: CGFloat = 24.0 / 52.0 let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0 @@ -574,11 +739,12 @@ public final class StoryPeerListComponent: Component { let itemFrame: CGRect if isReallyVisible { var adjustedRegularFrame = regularItemFrame - if i < collapseStartIndex { - adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseStartIndex), amount: 0.0) - } else if i > collapseEndIndex { - adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: collapseEndIndex), amount: 0.0) + if index < collapseStartIndex { + adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: effectiveFirstVisibleIndex + collapseStartIndex), amount: 0.0) + } else if index > collapseEndIndex { + adjustedRegularFrame = adjustedRegularFrame.interpolate(to: itemLayout.frame(at: effectiveFirstVisibleIndex + collapseEndIndex), amount: 0.0) } + adjustedRegularFrame.origin.x -= effectiveVisibleBounds.minX let collapsedItemPosition: CGPoint = collapsedItemFrame.center.interpolate(to: collapsedMaxItemFrame.center, amount: collapsedState.minFraction) @@ -588,13 +754,11 @@ public final class StoryPeerListComponent: Component { bounceOffsetFraction = max(-1.0, min(1.0, bounceOffsetFraction)) itemPosition.x += min(10.0, expandBoundsFraction * collapsedState.maxFraction * 1200.0) * bounceOffsetFraction - //let itemPosition = solveParabolicMotion(from: collapsedItemPosition, to: adjustedRegularFrame.center, progress: collapsedState.maxFraction) - let itemSize = CGSize(width: adjustedRegularFrame.width * itemScale, height: adjustedRegularFrame.height) itemFrame = itemSize.centered(around: itemPosition) } else { - itemFrame = regularItemFrame + itemFrame = regularItemFrame.offsetBy(dx: -effectiveVisibleBounds.minX, dy: 0.0) } return MeasuredItem( @@ -604,12 +768,20 @@ public final class StoryPeerListComponent: Component { } var validIds: [EnginePeer.Id] = [] + var validCollapsableIds: [EnginePeer.Id] = [] + for i in 0 ..< self.sortedItems.count { let itemSet = self.sortedItems[i] let peer = itemSet.peer let regularItemFrame = itemLayout.frame(at: i) + + var isItemVisible = true if !visibleBounds.intersects(regularItemFrame) { + isItemVisible = false + } + + if !isItemVisible { continue } @@ -654,18 +826,34 @@ public final class StoryPeerListComponent: Component { var itemAlpha: CGFloat = 1.0 var isCollapsable: Bool = false + var itemScale = measuredItem.itemScale + if itemLayout.itemCount == 1 { + let singleScaleFactor = min(1.0, collapsedState.minFraction + collapsedState.maxFraction) + itemScale = 0.001 * (1.0 - singleScaleFactor) + itemScale * singleScaleFactor + } - if i >= collapseStartIndex && i <= collapseEndIndex { + let collapseIndex = i - effectiveFirstVisibleIndex + if collapseIndex >= collapseStartIndex && collapseIndex <= collapseEndIndex { isCollapsable = true - if i != collapseStartIndex { + if collapseIndex != collapseStartIndex { leftItemFrame = calculateItem(i - 1).itemFrame } - if i != collapseEndIndex { + if collapseIndex != collapseEndIndex { rightItemFrame = calculateItem(i + 1).itemFrame } + + if effectiveFirstVisibleIndex == 0 && !component.titleHasActivity { + itemAlpha = 1.0 + } else { + itemAlpha = collapsedState.sideAlphaFraction + } } else { - itemAlpha = collapsedState.sideAlphaFraction + if itemLayout.itemCount == 1 { + itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0) + } else { + itemAlpha = collapsedState.sideAlphaFraction + } } var leftNeighborDistance: CGPoint? @@ -690,7 +878,7 @@ public final class StoryPeerListComponent: Component { hasItems: hasItems, ringAnimation: itemRingAnimation, collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0, - scale: measuredItem.itemScale, + scale: itemScale, collapsedWidth: collapsedItemWidth, expandedAlphaFraction: collapsedState.sideAlphaFraction, leftNeighborDistance: leftNeighborDistance, @@ -704,8 +892,151 @@ public final class StoryPeerListComponent: Component { if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { if itemView.superview == nil { - self.scrollView.addSubview(itemView) - self.scrollView.addSubview(itemView.backgroundContainer) + self.scrollContainerView.addSubview(itemView) + self.scrollContainerView.addSubview(itemView.backgroundContainer) + } + + if isCollapsable { + itemView.layer.zPosition = 1000.0 - CGFloat(i) * 0.01 + itemView.backgroundContainer.layer.zPosition = 1.0 + } else { + itemView.layer.zPosition = 0.5 + itemView.backgroundContainer.layer.zPosition = 0.0 + } + + itemTransition.setFrame(view: itemView, frame: measuredItem.itemFrame) + itemTransition.setAlpha(view: itemView, alpha: itemAlpha) + itemTransition.setScale(view: itemView, scale: 1.0) + + itemTransition.setFrame(view: itemView.backgroundContainer, frame: measuredItem.itemFrame) + itemTransition.setAlpha(view: itemView.backgroundContainer, alpha: itemAlpha) + itemTransition.setScale(view: itemView.backgroundContainer, scale: 1.0) + + itemView.updateIsPreviewing(isPreviewing: self.previewedItemId == itemSet.peer.id) + } + } + + for i in 0 ..< self.sortedItems.count { + let itemSet = self.sortedItems[i] + let peer = itemSet.peer + + if i >= collapseStartIndex && i <= collapseEndIndex { + } else { + continue + } + + validCollapsableIds.append(itemSet.peer.id) + + let visibleItem: VisibleItem + var itemTransition = transition + if let current = self.visibleCollapsableItems[itemSet.peer.id] { + visibleItem = current + } else { + itemTransition = .immediate + visibleItem = VisibleItem() + self.visibleCollapsableItems[itemSet.peer.id] = visibleItem + } + + var hasUnseen = false + hasUnseen = itemSet.hasUnseen + + var hasUnseenCloseFriendsItems = itemSet.hasUnseenCloseFriends + + var hasItems = true + var itemRingAnimation: StoryPeerListItemComponent.RingAnimation? + if peer.id == component.context.account.peerId { + if let storySubscriptions = component.storySubscriptions, let accountItem = storySubscriptions.accountItem { + hasItems = accountItem.storyCount != 0 + } else { + hasItems = false + } + if let uploadProgress = component.uploadProgress { + itemRingAnimation = .progress(uploadProgress) + } + + hasUnseenCloseFriendsItems = false + } + + let collapseIndex = i + effectiveFirstVisibleIndex + let measuredItem = calculateItem(collapseIndex) + + var leftItemFrame: CGRect? + var rightItemFrame: CGRect? + + var itemAlpha: CGFloat = 1.0 + var isCollapsable: Bool = false + var itemScale = measuredItem.itemScale + if itemLayout.itemCount == 1 { + let singleScaleFactor = min(1.0, collapsedState.minFraction + collapsedState.maxFraction) + itemScale = 0.001 * (1.0 - singleScaleFactor) + itemScale * singleScaleFactor + } + + if i >= collapseStartIndex && i <= collapseEndIndex { + isCollapsable = true + + if i != collapseStartIndex { + leftItemFrame = calculateItem(collapseIndex - 1).itemFrame + } + if i != collapseEndIndex { + rightItemFrame = calculateItem(collapseIndex + 1).itemFrame + } + + if effectiveFirstVisibleIndex == 0 { + itemAlpha = 0.0 + } else { + itemAlpha = 1.0 - collapsedState.sideAlphaFraction + } + } else { + if itemLayout.itemCount == 1 { + itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0) + } else { + itemAlpha = collapsedState.sideAlphaFraction + } + } + + if component.titleHasActivity { + itemAlpha = 0.0 + } + + var leftNeighborDistance: CGPoint? + var rightNeighborDistance: CGPoint? + + if let leftItemFrame { + leftNeighborDistance = CGPoint(x: abs(leftItemFrame.midX - measuredItem.itemFrame.midX), y: leftItemFrame.minY - measuredItem.itemFrame.minY) + } + if let rightItemFrame { + rightNeighborDistance = CGPoint(x: abs(rightItemFrame.midX - measuredItem.itemFrame.midX), y: rightItemFrame.minY - measuredItem.itemFrame.minY) + } + + let _ = visibleItem.view.update( + transition: itemTransition, + component: AnyComponent(StoryPeerListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: peer, + hasUnseen: hasUnseen, + hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems, + hasItems: hasItems, + ringAnimation: itemRingAnimation, + collapseFraction: 1.0 - collapsedState.maxFraction, + scale: itemScale, + collapsedWidth: collapsedItemWidth, + expandedAlphaFraction: collapsedState.sideAlphaFraction, + leftNeighborDistance: leftNeighborDistance, + rightNeighborDistance: rightNeighborDistance, + action: component.peerAction, + contextGesture: component.contextPeerAction + )), + environment: {}, + containerSize: itemLayout.itemSize + ) + + if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { + if itemView.superview == nil { + itemView.isUserInteractionEnabled = false + self.scrollContainerView.addSubview(itemView) + self.scrollContainerView.addSubview(itemView.backgroundContainer) } if isCollapsable { @@ -733,16 +1064,8 @@ public final class StoryPeerListComponent: Component { if !validIds.contains(id) { removedIds.append(id) if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { - if keepVisibleUntilCompletion && !transition.animation.isImmediate { - let backgroundContainer = itemView.backgroundContainer - transition.attachAnimation(view: itemView, id: "keep", completion: { [weak itemView, weak backgroundContainer] _ in - itemView?.removeFromSuperview() - backgroundContainer?.removeFromSuperview() - }) - } else { - itemView.backgroundContainer.removeFromSuperview() - itemView.removeFromSuperview() - } + itemView.backgroundContainer.removeFromSuperview() + itemView.removeFromSuperview() } } } @@ -750,13 +1073,77 @@ public final class StoryPeerListComponent: Component { self.visibleItems.removeValue(forKey: id) } - transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: collapsedContentOrigin - 4.0, y: 6.0 - 59.0), size: CGSize(width: collapsedContentWidth + 4.0, height: 44.0))) + var removedCollapsableIds: [EnginePeer.Id] = [] + for (id, visibleItem) in self.visibleCollapsableItems { + if !validCollapsableIds.contains(id) { + removedCollapsableIds.append(id) + if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { + itemView.backgroundContainer.removeFromSuperview() + itemView.removeFromSuperview() + } + } + } + for id in removedCollapsableIds { + self.visibleCollapsableItems.removeValue(forKey: id) + } + + transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: component.minTitleX, y: 6.0 - 59.0), size: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX), height: 44.0))) + + let defaultCollapsedTitleOffset: CGFloat = 0.0 + + var targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentOriginOffset + collapsedContentWidth + titleContentSpacing + if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 { + let singleScaleFactor = min(1.0, collapsedState.minFraction) + targetCollapsedTitleOffset += singleScaleFactor * 4.0 + } + + let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset + + let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction) + var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - titleContentWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction) + + if let titleIndicatorSize, let titleIndicatorView = self.titleIndicatorView?.view { + let titleIndicatorFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIndicatorSize.height) * 0.5)), size: titleIndicatorSize) + if titleIndicatorView.superview == nil { + self.addSubview(titleIndicatorView) + } + titleIndicatorView.frame = titleIndicatorFrame + titleContentOffset += titleIndicatorSize.width + titleIndicatorSpacing + } + + let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize) + if let titleComponentView = self.titleView.view { + if titleComponentView.superview == nil { + titleComponentView.isUserInteractionEnabled = false + self.addSubview(titleComponentView) + } + titleComponentView.frame = titleFrame + } + titleContentOffset += titleSize.width + + if let titleIconSize, let titleIconView = self.titleIconView?.view { + titleContentOffset += titleIconSpacing + + let titleIconFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIconSize.height) * 0.5)), size: titleIconSize) + + if titleIconView.superview == nil { + self.addSubview(titleIconView) + } + titleIconView.frame = titleIconFrame + } } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero { return nil } + + if let titleIconView = self.titleIconView?.view { + if let result = titleIconView.hitTest(self.convert(point, to: titleIconView), with: event) { + return result + } + } + var result: UIView? for view in self.subviews.reversed() { if let resultValue = view.hitTest(self.convert(point, to: view), with: event), resultValue.isUserInteractionEnabled { @@ -773,7 +1160,7 @@ public final class StoryPeerListComponent: Component { return nil } } else { - if !result.isDescendant(of: self.scrollView) { + if !result.isDescendant(of: self.scrollContainerView) { return nil } } @@ -784,20 +1171,6 @@ public final class StoryPeerListComponent: Component { var transition = transition transition.animation = .none - if self.component != nil { - if !component.unlocked && self.scrollView.bounds.minX != 0.0 { - self.ignoreScrolling = true - - let scrollingDistance = self.scrollView.bounds.minX - self.scrollView.bounds = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size) - let tempTransition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - self.updateScrolling(transition: tempTransition, keepVisibleUntilCompletion: true) - tempTransition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: scrollingDistance, y: 0.0), to: CGPoint(), additive: true) - - self.ignoreScrolling = false - } - } - let animationHint = transition.userData(AnimationHint.self) var useAnimation = false if let previousComponent = self.component, component.unlocked != previousComponent.unlocked { @@ -892,12 +1265,13 @@ public final class StoryPeerListComponent: Component { self.ignoreScrolling = true transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: CGSize(width: availableSize.width, height: availableSize.height + 4.0))) + transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: CGSize(width: availableSize.width, height: availableSize.height + 4.0))) if self.scrollView.contentSize != itemLayout.contentSize { self.scrollView.contentSize = itemLayout.contentSize } self.ignoreScrolling = false - self.updateScrolling(transition: transition, keepVisibleUntilCompletion: false) + self.updateScrolling(transition: transition) return availableSize } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 8ae8e466c0..65bcfe1bbc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -665,7 +665,7 @@ public final class StoryPeerListItemComponent: Component { } } else { if component.theme.overallDarkAppearance { - colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor] + colors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor] } else { colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift new file mode 100644 index 0000000000..88c4d0c8c1 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/TitleActivityIndicatorComponent.swift @@ -0,0 +1,61 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ActivityIndicator + +public final class TitleActivityIndicatorComponent: Component { + let color: UIColor + + public init( + color: UIColor + ) { + self.color = color + } + + public static func ==(lhs: TitleActivityIndicatorComponent, rhs: TitleActivityIndicatorComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var activityIndicator: ActivityIndicator? + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + deinit { + } + + func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(component.color, availableSize.width, 2.0, true)) + self.activityIndicator = activityIndicator + self.addSubview(activityIndicator.view) + } + + activityIndicator.frame = CGRect(origin: CGPoint(), size: availableSize) + activityIndicator.isHidden = false + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/Contents.json new file mode 100644 index 0000000000..05f42bc4d7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "messagebubble.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/messagebubble.svg b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/messagebubble.svg new file mode 100644 index 0000000000..9638416545 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/MessageBubble.imageset/messagebubble.svg @@ -0,0 +1,3 @@ + + + diff --git a/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs b/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs new file mode 100644 index 0000000000..0fb36135ee Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs differ diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 4fdbf59faf..a4dba03b70 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -469,7 +469,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { component: AnyComponent(AvatarStoryIndicatorComponent( hasUnseen: storyData.hasUnseen, hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends, - isDarkTheme: theme.overallDarkAppearance, + theme: theme, activeLineWidth: 3.0, inactiveLineWidth: 2.0, counters: nil diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index fa48398a12..d694126b59 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -369,7 +369,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .image(image, dimensions): if let imageData = compressImageToJPEG(image, quality: 0.7) { let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) } @@ -392,7 +392,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } let entities = generateChatInputTextEntities(caption) - self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, period: privacy.timeout, randomId: randomId) + self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), text: caption.string, entities: entities, pin: privacy.archive, privacy: privacy.privacy, isForwardingDisabled: false, period: privacy.timeout, randomId: randomId) Queue.mainQueue().justDispatch { commit({}) }