diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 12deee4cfc..e6bb311d7b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9366,6 +9366,7 @@ Sorry for the inconvenience."; "Paint.Flip" = "Flip"; "Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@"; +"Message.ForwardedExpiredStoryShort" = "Expired Story\nFrom: %@"; "Conversation.StoryForwardTooltip.Chat.One" = "Story forwarded to **%@**"; "Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**"; diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index fc65a4582d..3f8ba39fa5 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -746,7 +746,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: let canRemove = false - let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in + let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in let _ = (updatePeerSound(peerId, sound) |> deliverOnMainQueue).start(next: { _ in }) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9fab152207..a7ca599838 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -178,7 +178,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var powerSavingMonitoringDisposable: Disposable? - private(set) var storySubscriptions: EngineStorySubscriptions? + private var rawStorySubscriptions: EngineStorySubscriptions? + private var shouldFixStorySubscriptionOrder: Bool = false + private var fixedStorySubscriptionOrder: [EnginePeer.Id] = [] + var orderedStorySubscriptions: EngineStorySubscriptions? private var storyProgressDisposable: Disposable? private var storySubscriptionsDisposable: Disposable? @@ -838,6 +841,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.present(controller, in: .window(.root)) } + func allowAutomaticOrder() { + if !self.shouldFixStorySubscriptionOrder { + return + } + + self.shouldFixStorySubscriptionOrder = false + self.fixedStorySubscriptionOrder = self.rawStorySubscriptions?.items.map(\.peer.id) ?? [] + if self.orderedStorySubscriptions != self.rawStorySubscriptions { + self.orderedStorySubscriptions = self.rawStorySubscriptions + + // important not to cause a loop + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + + self.chatListDisplayNode.requestNavigationBarLayout(transition: Transition.immediate.withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: false, + crossfadeStoryPeers: true + ))) + } + } + } + private func updateThemeAndStrings() { if case .chatList(.root) = self.location { self.tabBarItem.title = self.presentationData.strings.DialogList_Title @@ -1283,7 +1310,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: false) + let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peerId, singlePeer: true) let _ = (storyContent.state |> filter { $0.slice != nil } |> take(1) @@ -1784,7 +1811,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var autodownloadEnabled = true - if !shouldDownloadMediaAutomatically(settings: automaticMediaDownloadSettings, peerType: .contact, networkType: automaticDownloadNetworkType, authorPeerId: nil, contactsPeerIds: [], media: nil) { + if !shouldDownloadMediaAutomatically(settings: automaticMediaDownloadSettings, peerType: .contact, networkType: automaticDownloadNetworkType, authorPeerId: nil, contactsPeerIds: [], media: nil, isStory: true) { autodownloadEnabled = false } @@ -1821,17 +1848,38 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: false) - |> deliverOnMainQueue).start(next: { [weak self] storySubscriptions in + |> deliverOnMainQueue).start(next: { [weak self] rawStorySubscriptions in guard let self else { return } var wasEmpty = true - if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty { + if let rawStorySubscriptions = self.rawStorySubscriptions, !rawStorySubscriptions.items.isEmpty { wasEmpty = false } - self.storySubscriptions = storySubscriptions - let isEmpty = storySubscriptions.items.isEmpty + + self.rawStorySubscriptions = rawStorySubscriptions + var items: [EngineStorySubscriptions.Item] = [] + if self.shouldFixStorySubscriptionOrder { + for peerId in self.fixedStorySubscriptionOrder { + if let item = rawStorySubscriptions.items.first(where: { $0.peer.id == peerId }) { + items.append(item) + } + } + } + for item in rawStorySubscriptions.items { + if !items.contains(where: { $0.peer.id == item.peer.id }) { + items.append(item) + } + } + self.orderedStorySubscriptions = EngineStorySubscriptions( + accountItem: rawStorySubscriptions.accountItem, + items: items, + hasMoreToken: rawStorySubscriptions.hasMoreToken + ) + self.fixedStorySubscriptionOrder = items.map(\.peer.id) + + let isEmpty = rawStorySubscriptions.items.isEmpty let transition: ContainedViewLayoutTransition if self.didAppear { @@ -1851,7 +1899,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var chatListState = chatListState var peerStoryMapping: [EnginePeer.Id: Bool] = [:] - for item in storySubscriptions.items { + for item in rawStorySubscriptions.items { if item.peer.id == self.context.account.peerId { continue } @@ -2303,7 +2351,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - func updateHeaderContent(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { + func updateHeaderContent(layout: ContainerViewLayout) -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { var backTitle: String? @@ -2447,7 +2495,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false) + if peer.id == self.context.account.peerId { + if let rawStorySubscriptions = self.rawStorySubscriptions { + var openCamera = false + if let accountItem = rawStorySubscriptions.accountItem { + openCamera = accountItem.storyCount == 0 + } else { + openCamera = true + } + + if openCamera { + self.openStoryCamera() + return + } + } + } + + self.shouldFixStorySubscriptionOrder = true + let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false, fixedOrder: self.fixedStorySubscriptionOrder) let _ = (storyContent.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] storyContentState in diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 816becd3e4..0b3bcb72e0 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -927,7 +927,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele if itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 { if case let .known(value) = offset { if value < -1.0 { - if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { self.currentItemNode.startedScrollingAtUpperBound = true self.tempTopInset = ChatListNavigationBar.storiesScrollHeight } @@ -958,7 +958,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } let tempTopInset: CGFloat if self.currentItemNode.startedScrollingAtUpperBound { - if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { tempTopInset = ChatListNavigationBar.storiesScrollHeight } else { tempTopInset = 0.0 @@ -1796,7 +1796,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return false } - if let storySubscriptions = controller.storySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = controller.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { if navigationBarComponentView.storiesUnlocked { return true @@ -1914,8 +1914,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - private func updateNavigationBar(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { - let headerContent = self.controller?.updateHeaderContent(layout: layout, transition: transition) + private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: Transition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { + let headerContent = self.controller?.updateHeaderContent(layout: layout) var tabsNode: ASDisplayNode? var tabsNodeIsSearch = false @@ -1928,7 +1928,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } let navigationBarSize = self.navigationBarView.update( - transition: Transition(transition), + transition: transition, component: AnyComponent(ChatListNavigationBar( context: self.context, theme: self.presentationData.theme, @@ -1939,7 +1939,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, - storySubscriptions: self.controller?.storySubscriptions, + storySubscriptions: self.controller?.orderedStorySubscriptions, storiesIncludeHidden: false, uploadProgress: self.controller?.storyUploadProgress, tabsNode: tabsNode, @@ -1970,18 +1970,26 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } controller.openStatusSetup(sourceView: sourceView) + }, + allowAutomaticOrder: { [weak self] in + guard let self, let controller = self.controller else { + return + } + controller.allowAutomaticOrder() } )), environment: {}, containerSize: layout.size ) if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.deferScrollApplication = true + if deferScrollApplication { + navigationBarComponentView.deferScrollApplication = true + } if navigationBarComponentView.superview == nil { self.view.addSubview(navigationBarComponentView) } - transition.updateFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) + transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) return (navigationBarSize.height, 0.0) } else { @@ -2030,15 +2038,18 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { - navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: Transition(transition).withUserData(ChatListNavigationBar.AnimationHint(disableStoriesAnimations: self.tempDisableStoriesAnimations))) + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: Transition(transition).withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: self.tempDisableStoriesAnimations, + crossfadeStoryPeers: false + ))) } } - func requestNavigationBarLayout(transition: ContainedViewLayoutTransition) { + func requestNavigationBarLayout(transition: Transition) { guard let (layout, _, _, _, _) = self.containerLayout else { return } - let _ = self.updateNavigationBar(layout: layout, transition: transition) + let _ = self.updateNavigationBar(layout: layout, deferScrollApplication: false, transition: transition) } func scrollToStories(animated: Bool) { @@ -2046,7 +2057,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } - if let storySubscriptions = self.controller?.storySubscriptions, !storySubscriptions.items.isEmpty { + if let storySubscriptions = self.controller?.orderedStorySubscriptions, !storySubscriptions.items.isEmpty { self.tempAllowAvatarExpansion = true self.tempDisableStoriesAnimations = !animated self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate @@ -2055,12 +2066,6 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.tempDisableStoriesAnimations = false tempNavigationScrollingTransition = nil } - - /*self.mainContainerNode.scrollToTop(animated: false) - self.mainContainerNode.ignoreStoryUnlockedScrolling = true - self.controller?.requestLayout(transition: .immediate) - self.mainContainerNode.scrollToTop(animated: false) - self.mainContainerNode.ignoreStoryUnlockedScrolling = false*/ } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -2069,7 +2074,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { var cleanNavigationBarHeight = cleanNavigationBarHeight var storiesInset = storiesInset - let navigationBarLayout = self.updateNavigationBar(layout: layout, transition: transition) + let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: Transition(transition)) self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset navigationBarHeight = navigationBarLayout.navigationHeight @@ -2350,6 +2355,10 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool { + if abs(velocity) > 10.0 { + return false + } + if !isPrimary || self.inlineStackContainerNode == nil { } else { return false diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index 0a01ee8426..5eb4121f16 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -10,7 +10,7 @@ import PresentationDataUtils import OverlayStatusController import LocalizedPeerData -func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?) -> Signal<[ContextMenuItem], NoError> { +func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?, isStories: Bool) -> Signal<[ContextMenuItem], NoError> { let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings return context.engine.data.get( @@ -21,6 +21,17 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con |> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable -> [ContextMenuItem] in var items: [ContextMenuItem] = [] + if isStories { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Unhide", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + + context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false) + }))) + } + 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 let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index d131e351cf..2e36d8e53f 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -38,61 +38,18 @@ private let dropDownIcon = { () -> UIImage in return image }() +private enum ContactListNodeEntrySection: Int { + case stories = 0 + case contacts = 1 +} + private enum ContactListNodeEntryId: Hashable { case search case sort case permission(action: Bool) case option(index: Int) - case peerId(Int64) + case peerId(peerId: Int64, section: ContactListNodeEntrySection) case deviceContact(DeviceContactStableId) - - static func <(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue - } - - static func ==(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool { - switch lhs { - case .search: - switch rhs { - case .search: - return true - default: - return false - } - case .sort: - switch rhs { - case .sort: - return true - default: - return false - } - case let .permission(action): - if case .permission(action) = rhs { - return true - } else { - return false - } - case let .option(index): - if case .option(index) = rhs { - return true - } else { - return false - } - case let .peerId(lhsId): - switch rhs { - case let .peerId(rhsId): - return lhsId == rhsId - default: - return false - } - case let .deviceContact(id): - if case .deviceContact(id) = rhs { - return true - } else { - return false - } - } - } } private final class ContactListNodeInteraction { @@ -100,16 +57,18 @@ private final class ContactListNodeInteraction { fileprivate let authorize: () -> Void fileprivate let suppressWarning: () -> Void fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void - fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? + fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? + fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void let itemHighlighting = ContactItemHighlighting() - init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?) { + init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void) { self.activateSearch = activateSearch self.authorize = authorize self.suppressWarning = suppressWarning self.openPeer = openPeer self.contextAction = contextAction + self.openStories = openStories } } @@ -120,12 +79,17 @@ enum ContactListAnimation { } private enum ContactListNodeEntry: Comparable, Identifiable { + struct StoryData: Equatable { + var count: Int + var unseenCount: Int + } + case search(PresentationTheme, PresentationStrings) case sort(PresentationTheme, PresentationStrings, ContactsSortOrder) case permissionInfo(PresentationTheme, String, String, Bool) case permissionEnable(PresentationTheme, String) case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) - case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool) + case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, StoryData?) var stableId: ContactListNodeEntryId { switch self { @@ -139,10 +103,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return .permission(action: true) case let .option(index, _, _, _, _): return .option(index: index) - case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _): + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, storyData): switch peer { case let .peer(peer, _, _): - return .peerId(peer.id.toInt64()) + return .peerId(peerId: peer.id.toInt64(), section: storyData != nil ? .stories : .contacts) case let .deviceContact(id, _): return .deviceContact(id) } @@ -172,7 +136,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { }) case let .option(_, option, header, _, _): return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action) - case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled): + case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled, storyData): var status: ContactsPeerItemStatus let itemPeer: ContactsPeerItemPeer var isContextActionEnabled = false @@ -218,7 +182,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { switch itemPeer { case let .peer(peer, _): if let peer = peer { - contextAction(peer, node, gesture, location) + contextAction(peer, node, gesture, location, storyData != nil) } case .deviceContact: break @@ -237,9 +201,34 @@ private enum ContactListNodeEntry: Comparable, Identifiable { })] } + var hasUnseenStories: Bool? + if let storyData = storyData { + let text: String + //TODO:localize + if storyData.unseenCount != 0 { + if storyData.unseenCount == 1 { + text = "1 unseen story" + } else { + text = "\(storyData.unseenCount) unseen stories" + } + } else { + if storyData.count == 1 { + text = "1 story" + } else { + text = "\(storyData.count) stories" + } + } + status = .custom(string: text, multiline: false, isActive: false, icon: nil) + hasUnseenStories = storyData.unseenCount != 0 + } + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in interaction.openPeer(peer, .generic) - }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction) + }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, hasUnseenStories: hasUnseenStories, openStories: { peer, sourceNode in + if case let .peer(peerValue, _) = peer, let peerValue { + interaction.openStories(peerValue, sourceNode) + } + }) } } @@ -275,9 +264,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled): + case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled, lhsStoryData): switch rhs { - case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled): + case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled, rhsStoryData): if lhsIndex != rhsIndex { return false } @@ -318,6 +307,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsEnabled != rhsEnabled { return false } + if lhsStoryData != rhsStoryData { + return false + } return true default: return false @@ -359,18 +351,25 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case .peer: return true } - case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData): switch rhs { case .search, .sort, .permissionInfo, .permissionEnable, .option: return false - case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _): + case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData): + if (lhsStoryData == nil) != (rhsStoryData == nil) { + if lhsStoryData != nil { + return true + } else { + return false + } + } return lhsIndex < rhsIndex } } } } -private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var commonHeader: ListViewItemHeader? @@ -398,8 +397,13 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } } + if let storySubscriptions, !storySubscriptions.items.isEmpty { + addHeader = true + } + if addHeader { - commonHeader = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + //TODO:localize + commonHeader = ChatListSearchItemHeader(type: .text("SORTED BY LAST SEEN TIME", AnyHashable(1)), theme: theme, strings: strings, actionTitle: nil, action: nil) } switch presentation { @@ -522,6 +526,18 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } + if let storySubscriptions { + var index: Int = 0 + + //TODO:localize + let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text("HIDDEN STORIES", AnyHashable(0)), theme: theme, strings: strings) + + for item in storySubscriptions.items { + entries.append(.peer(index, .peer(peer: item.peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount))) + index += 1 + } + } + var index: Int = 0 var existingPeerIds = Set() @@ -546,7 +562,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis enabled = true } - entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled)) + entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil)) index += 1 } } @@ -582,7 +598,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis default: enabled = true } - entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled)) + entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil)) index += 1 } return entries @@ -606,7 +622,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta case .search: //indexSections.apend(CollectionIndexNode.searchIndex) break - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if !existingSections.contains(header.letter) { existingSections.insert(header.letter) @@ -727,6 +743,7 @@ public final class ContactListNode: ASDisplayNode { private var didSetReady = false private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?)>() + let storySubscriptions = Promise(nil) private let selectionStatePromise = Promise(nil) private var selectionStateValue: ContactListNodeGroupSelectionState? { @@ -803,7 +820,8 @@ public final class ContactListNode: ASDisplayNode { public var openPeer: ((ContactListPeer, ContactListAction) -> Void)? public var openPrivacyPolicy: (() -> Void)? public var suppressPermissionWarning: (() -> Void)? - private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? + private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? + public var openStories: ((EnginePeer, ASDisplayNode) -> Void)? private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) private let disposable = MetaDisposable() @@ -819,7 +837,7 @@ public final class ContactListNode: ASDisplayNode { private let isPeerEnabled: ((EnginePeer) -> Bool)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, presentation: Signal, filters: [ContactListFilter] = [.excludeSelf], isPeerEnabled: ((EnginePeer) -> Bool)? = nil, selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, isSearch: Bool = false, multipleSelection: Bool = false) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, presentation: Signal, filters: [ContactListFilter] = [.excludeSelf], isPeerEnabled: ((EnginePeer) -> Bool)? = nil, selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? = nil, isSearch: Bool = false, multipleSelection: Bool = false) { self.context = context self.filters = filters self.displayPermissionPlaceholder = displayPermissionPlaceholder @@ -915,7 +933,12 @@ public final class ContactListNode: ASDisplayNode { strongSelf.openPeer?(peer, action) } } - }, contextAction: contextAction) + }, contextAction: contextAction, openStories: { [weak self] peer, sourceNode in + guard let self else { + return + } + self.openStories?(peer, sourceNode) + }) self.indexNode.indexSelected = { [weak self] section in guard let strongSelf = self, let layout = strongSelf.validLayout, let entries = previousEntries.with({ $0 }) else { @@ -942,7 +965,7 @@ public final class ContactListNode: ASDisplayNode { strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if let scalar = UnicodeScalar(header.letter) { let title = "\(Character(scalar))" @@ -1190,7 +1213,7 @@ public final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact.0)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } @@ -1250,8 +1273,8 @@ public final class ContactListNode: ASDisplayNode { chatListSignal = .single([]) } - return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get()) - |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed -> Signal in + return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get()) + |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal in let signal = deferred { () -> Signal in var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { @@ -1283,7 +1306,7 @@ public final class ContactListNode: ASDisplayNode { if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } - let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons) + let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 449ca10f8d..ea7bf6078c 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -509,66 +509,59 @@ public class ContactsController: ViewController { self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - if let componentView = self.chatListHeaderView(), componentView.storyPeerAction == nil { - componentView.storyPeerAction = { [weak self] peer in + self.contactsNode.openStories = { [weak self] peer, sourceNode in + guard let self else { + return + } + + let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer.id, singlePeer: true) + let _ = (storyContent.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak sourceNode] storyContentState in guard let self else { return } - let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer?.id, singlePeer: false) - let _ = (storyContent.state - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] storyContentState in - guard let self else { - return - } - - if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil { - return - } - - var transitionIn: StoryContainerScreen.TransitionIn? - if let peer, let componentView = self.chatListHeaderView() { - if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { - transitionIn = StoryContainerScreen.TransitionIn( - sourceView: transitionView, - sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5, - sourceIsAvatar: true + var transitionIn: StoryContainerScreen.TransitionIn? + if let itemNode = sourceNode as? ContactsPeerItemNode { + transitionIn = StoryContainerScreen.TransitionIn( + sourceView: itemNode.avatarNode.view, + sourceRect: itemNode.avatarNode.view.bounds, + sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5, + sourceIsAvatar: true + ) + itemNode.avatarNode.isHidden = true + } + + let storyContainerScreen = StoryContainerScreen( + context: self.context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { _, _ in + if let itemNode = sourceNode as? ContactsPeerItemNode { + let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view) + return StoryContainerScreen.TransitionOut( + destinationView: itemNode.view, + transitionView: nil, + destinationRect: rect, + destinationCornerRadius: rect.height * 0.5, + destinationIsAvatar: true, + completed: { [weak itemNode] in + guard let itemNode else { + return + } + itemNode.avatarNode.isHidden = false + } ) } + return nil } - - let storyContainerScreen = StoryContainerScreen( - context: self.context, - content: storyContent, - transitionIn: transitionIn, - transitionOut: { [weak self] peerId, _ in - guard let self else { - return nil - } - - if let componentView = self.chatListHeaderView() { - if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { - return StoryContainerScreen.TransitionOut( - destinationView: transitionView, - transitionView: transitionContentView, - destinationRect: transitionView.bounds, - destinationCornerRadius: transitionView.bounds.height * 0.5, - destinationIsAvatar: true, - completed: {} - ) - } - } - - return nil - } - ) - self.push(storyContainerScreen) - }) - } + ) + self.push(storyContainerScreen) + }) + } - componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in + /*componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in guard let self else { return } @@ -627,7 +620,7 @@ public class ContactsController: ViewController { 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 3a6e4444f1..fd1819c371 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -63,6 +63,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { var openPeopleNearby: (() -> Void)? var openInvite: (() -> Void)? var openQrScan: (() -> Void)? + var openStories: ((EnginePeer, ASDisplayNode) -> Void)? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -113,10 +114,10 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - var contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? + var contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? - self.contactListNode = ContactListNode(context: context, presentation: presentation, displaySortOptions: true, contextAction: { peer, node, gesture, location in - contextAction?(peer, node, gesture, location) + self.contactListNode = ContactListNode(context: context, presentation: presentation, displaySortOptions: true, contextAction: { peer, node, gesture, location, isStories in + contextAction?(peer, node, gesture, location, isStories) }) super.init() @@ -159,8 +160,8 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - contextAction = { [weak self] peer, node, gesture, location in - self?.contextAction(peer: peer, node: node, gesture: gesture, location: location) + contextAction = { [weak self] peer, node, gesture, location, isStories in + self?.contextAction(peer: peer, node: node, gesture: gesture, location: location, isStories: isStories) } self.contactListNode.contentOffsetChanged = { [weak self] offset in @@ -238,30 +239,19 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } - var wasEmpty = true - if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty { - wasEmpty = false - } self.storySubscriptions = storySubscriptions - let isEmpty = storySubscriptions.items.isEmpty - - let transition: ContainedViewLayoutTransition - if self.didAppear { - transition = .animated(duration: 0.4, curve: .spring) - } else { - transition = .immediate - } - - let _ = wasEmpty - let _ = isEmpty - - //self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition - self.controller?.requestLayout(transition: transition) - //self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil + self.contactListNode.storySubscriptions.set(.single(storySubscriptions)) self.storiesReady.set(.single(true)) }) - + + self.contactListNode.openStories = { [weak self] peer, sourceNode in + guard let self else { + return + } + self.openStories?(peer, sourceNode) + } + let storiesPostingAvailability = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { view -> AppConfiguration in let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue @@ -446,7 +436,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, - storySubscriptions: self.storySubscriptions, + storySubscriptions: nil, storiesIncludeHidden: true, uploadProgress: nil, tabsNode: tabsNode, @@ -461,6 +451,8 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.contactListNode.activateSearch?() }, openStatusSetup: { _ in + }, + allowAutomaticOrder: { } )), environment: {}, @@ -532,13 +524,13 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - private func contextAction(peer: EnginePeer, node: ASDisplayNode?, gesture: ContextGesture?, location: CGPoint?) { + private func contextAction(peer: EnginePeer, node: ASDisplayNode?, gesture: ContextGesture?, location: CGPoint?, isStories: Bool) { 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) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + 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) } @@ -559,7 +551,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { requestOpenPeerFromSearch(peer) } }, contextAction: { [weak self] peer, node, gesture, location in - self?.contextAction(peer: peer, node: node, gesture: gesture, location: location) + self?.contextAction(peer: peer, node: node, gesture: gesture, location: location, isStories: false) }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index a406bb700c..66af4b82ba 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index acff0400f3..db79ce7b9c 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -20,6 +20,7 @@ import ComponentFlow import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent +import AvatarStoryIndicatorComponent public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -179,6 +180,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let arrowAction: (() -> Void)? let animationCache: AnimationCache? let animationRenderer: MultiAnimationRenderer? + let hasUnseenStories: Bool? + let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? public let selectable: Bool @@ -213,7 +216,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil, animationCache: AnimationCache? = nil, - animationRenderer: MultiAnimationRenderer? = nil + animationRenderer: MultiAnimationRenderer? = nil, + hasUnseenStories: Bool? = nil, + openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil ) { self.presentationData = presentationData self.style = style @@ -243,6 +248,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.arrowAction = arrowAction self.animationCache = animationCache self.animationRenderer = animationRenderer + self.hasUnseenStories = hasUnseenStories + self.openStories = openStories if let index = index { var letter: String = "#" @@ -391,8 +398,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var nonExtractedRect: CGRect? private let offsetContainerNode: ASDisplayNode - - private let avatarNode: AvatarNode + private let avatarNodeContainer: ASDisplayNode + public let avatarNode: AvatarNode + private var avatarStoryIndicator: ComponentView? private var avatarIconView: ComponentHostView? private var avatarIconComponent: EmojiStatusComponent? private let titleNode: TextNode @@ -493,8 +501,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.offsetContainerNode = ASDisplayNode() + self.avatarNodeContainer = ASDisplayNode() self.avatarNode = AvatarNode(font: avatarFont) - self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() + self.avatarNode.isLayerBacked = false self.titleNode = TextNode() self.statusNode = TextNode() @@ -515,7 +524,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) - self.offsetContainerNode.addSubnode(self.avatarNode) + self.avatarNodeContainer.addSubnode(self.avatarNode) + self.offsetContainerNode.addSubnode(self.avatarNodeContainer) self.offsetContainerNode.addSubnode(self.titleNode) self.offsetContainerNode.addSubnode(self.statusNode) @@ -1069,7 +1079,66 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))) + let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(), size: avatarFrame.size) + + transition.updatePosition(node: strongSelf.avatarNodeContainer, position: avatarFrame.center) + transition.updateBounds(node: strongSelf.avatarNodeContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) + + var avatarScale: CGFloat = 1.0 + + if item.hasUnseenStories != nil { + avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width + } + + transition.updateTransformScale(node: strongSelf.avatarNodeContainer, scale: CGPoint(x: avatarScale, y: avatarScale)) + + let storyIndicatorScale: CGFloat = 1.0 + + if let displayStoryIndicator = item.hasUnseenStories { + var indicatorTransition = Transition(transition) + let avatarStoryIndicator: ComponentView + if let current = strongSelf.avatarStoryIndicator { + avatarStoryIndicator = current + } else { + indicatorTransition = .immediate + avatarStoryIndicator = ComponentView() + strongSelf.avatarStoryIndicator = avatarStoryIndicator + } + + var indicatorFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + 2.0, y: avatarFrame.minY + 2.0), size: CGSize(width: avatarFrame.width - 2.0 - 2.0, height: avatarFrame.height - 2.0 - 2.0)) + indicatorFrame.origin.x -= (avatarFrame.width - avatarFrame.width * storyIndicatorScale) * 0.5 + + let _ = avatarStoryIndicator.update( + transition: indicatorTransition, + component: AnyComponent(AvatarStoryIndicatorComponent( + hasUnseen: displayStoryIndicator, + isDarkTheme: item.presentationData.theme.overallDarkAppearance, + activeLineWidth: 1.0 + UIScreenPixel, + inactiveLineWidth: 1.0 + UIScreenPixel + )), + environment: {}, + containerSize: indicatorFrame.size + ) + if let avatarStoryIndicatorView = avatarStoryIndicator.view { + if avatarStoryIndicatorView.superview == nil { + avatarStoryIndicatorView.isUserInteractionEnabled = true + avatarStoryIndicatorView.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.avatarStoryTapGesture(_:)))) + + strongSelf.offsetContainerNode.view.insertSubview(avatarStoryIndicatorView, belowSubview: strongSelf.avatarNodeContainer.view) + } + + indicatorTransition.setPosition(view: avatarStoryIndicatorView, position: indicatorFrame.center) + indicatorTransition.setBounds(view: avatarStoryIndicatorView, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size)) + indicatorTransition.setScale(view: avatarStoryIndicatorView, scale: storyIndicatorScale) + } + } else { + if let avatarStoryIndicator = strongSelf.avatarStoryIndicator { + strongSelf.avatarStoryIndicator = nil + avatarStoryIndicator.view?.removeFromSuperview() + } + } if case let .thread(_, title, icon, color) = item.peer { let animationCache = item.context.animationCache @@ -1109,12 +1178,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { ) transition.updateFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 43.0, y: floor((nodeLayout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)) - strongSelf.avatarNode.isHidden = true + strongSelf.avatarNodeContainer.isHidden = true } else if let avatarIconView = strongSelf.avatarIconView { strongSelf.avatarIconView = nil avatarIconView.removeFromSuperview() - strongSelf.avatarNode.isHidden = false + strongSelf.avatarNodeContainer.isHidden = false } let _ = titleApply() @@ -1471,6 +1540,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) { + return result + } + + return super.hitTest(point, with: event) + } + override public func headers() -> [ListViewItemHeader]? { if let (item, _, _, _, _, _) = self.layoutParams { return item.header.flatMap { [$0] } @@ -1484,4 +1561,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { item.arrowAction?() } } + + @objc private func avatarStoryTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let (item, _, _, _, _, _) = self.layoutParams { + item.openStories?(item.peer, self) + } + } + } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift index 0c19ac4462..0eef1a3457 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift @@ -47,6 +47,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { case dataUsageItem(PresentationTheme, PresentationStrings, AutomaticDownloadDataUsage, Int?, Bool) case typesHeader(PresentationTheme, String) case photos(PresentationTheme, String, String, Bool) + case stories(PresentationTheme, String, String, Bool) case videos(PresentationTheme, String, String, Bool) case files(PresentationTheme, String, String, Bool) case voiceMessagesInfo(PresentationTheme, String) @@ -57,7 +58,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { return AutodownloadMediaCategorySection.master.rawValue case .dataUsageHeader, .dataUsageItem: return AutodownloadMediaCategorySection.dataUsage.rawValue - case .typesHeader, .photos, .videos, .files, .voiceMessagesInfo: + case .typesHeader, .photos, .stories, .videos, .files, .voiceMessagesInfo: return AutodownloadMediaCategorySection.types.rawValue } } @@ -74,12 +75,14 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { return 3 case .photos: return 4 - case .videos: + case .stories: return 5 - case .files: + case .videos: return 6 - case .voiceMessagesInfo: + case .files: return 7 + case .voiceMessagesInfo: + return 8 } } @@ -115,6 +118,12 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { } else { return false } + case let .stories(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .stories(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } case let .videos(lhsTheme, lhsText, lhsValue, lhsEnabled): if case let .videos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true @@ -159,6 +168,10 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Photos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.photo) }) + case let .stories(_, text, value, enabled): + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Stories")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + arguments.customize(.story) + }) case let .videos(_, text, value, enabled): return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Videos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.video) @@ -190,6 +203,17 @@ private struct AutomaticDownloadPeers { } private func stringForAutomaticDownloadPeers(strings: PresentationStrings, decimalSeparator: String, peers: AutomaticDownloadPeers, category: AutomaticDownloadCategory) -> String { + if case .story = category { + if peers.contacts && peers.otherPrivate { + return strings.AutoDownloadSettings_OnForAll + } else if peers.contacts { + //TODO:localize + return "On for contacts" + } else { + return strings.AutoDownloadSettings_OffForAll + } + } + var size: String? if var peersSize = peers.size, category == .video || category == .file { if peersSize == Int32.max { @@ -253,6 +277,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData: let photo = AutomaticDownloadPeers(category: categories.photo) let video = AutomaticDownloadPeers(category: categories.video) let file = AutomaticDownloadPeers(category: categories.file) + let stories = AutomaticDownloadPeers(category: categories.stories) entries.append(.master(presentationData.theme, presentationData.strings.AutoDownloadSettings_AutoDownload, master)) @@ -268,6 +293,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData: entries.append(.typesHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_MediaTypes)) entries.append(.photos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Photos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: photo, category: .photo), master)) + entries.append(.stories(presentationData.theme, "Stories", stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: stories, category: .story), master)) entries.append(.videos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Videos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: video, category: .video), master)) entries.append(.files(presentationData.theme, presentationData.strings.AutoDownloadSettings_Files, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: file, category: .file), master)) entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo)) diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift index c48fb8ef89..8304511931 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift @@ -49,6 +49,7 @@ enum AutomaticDownloadCategory { case photo case video case file + case story } private enum AutomaticDownloadPeerType { @@ -243,38 +244,54 @@ private func autodownloadMediaCategoryControllerEntries(presentationData: Presen let predownload: Bool switch category { - case .photo: - peers = AutomaticDownloadPeers(category: categories.photo) - size = categories.photo.sizeLimit - predownload = categories.photo.predownload - case .video: - peers = AutomaticDownloadPeers(category: categories.video) - size = categories.video.sizeLimit - predownload = categories.video.predownload - case .file: - peers = AutomaticDownloadPeers(category: categories.file) - size = categories.file.sizeLimit - predownload = categories.file.predownload + case .photo: + peers = AutomaticDownloadPeers(category: categories.photo) + size = categories.photo.sizeLimit + predownload = categories.photo.predownload + case .video: + peers = AutomaticDownloadPeers(category: categories.video) + size = categories.video.sizeLimit + predownload = categories.video.predownload + case .file: + peers = AutomaticDownloadPeers(category: categories.file) + size = categories.file.sizeLimit + predownload = categories.file.predownload + case .story: + peers = AutomaticDownloadPeers(category: categories.stories) + size = categories.stories.sizeLimit + predownload = categories.stories.predownload } let downloadTitle: String var sizeTitle: String? switch category { - case .photo: - downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos - case .video: - downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos - sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize - case .file: - downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles - sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize + case .photo: + downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos + case .video: + downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos + sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize + case .file: + downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles + sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize + case .story: + //TODO:localize + downloadTitle = "AUTO-DOWNLOAD STORIES" + sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize } - entries.append(.peerHeader(presentationData.theme, downloadTitle)) - entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts)) - entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate)) - entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups)) - entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels)) + if case .story = category { + entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts)) + //TODO:localize + if peers.contacts { + entries.append(.peerOtherPrivate(presentationData.theme, "Hidden Contacts", peers.otherPrivate)) + } + } else { + entries.append(.peerHeader(presentationData.theme, downloadTitle)) + entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts)) + entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate)) + entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups)) + entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels)) + } switch category { case .video, .file: @@ -339,7 +356,18 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType categories.file.groups = !categories.file.groups case .channel: categories.file.channels = !categories.file.channels - } + } + case .story: + switch type { + case .contact: + categories.stories.contacts = !categories.stories.contacts + case .otherPrivate: + categories.stories.otherPrivate = !categories.stories.otherPrivate + case .group: + categories.stories.groups = !categories.stories.groups + case .channel: + categories.stories.channels = !categories.stories.channels + } } switch connectionType { case .cellular: @@ -362,6 +390,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType categories.video.sizeLimit = size case .file: categories.file.sizeLimit = size + case .story: + categories.stories.sizeLimit = size } switch connectionType { case .cellular: @@ -384,6 +414,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType categories.video.predownload = !categories.video.predownload case .file: categories.file.predownload = !categories.file.predownload + case .story: + categories.stories.predownload = !categories.stories.predownload } switch connectionType { case .cellular: @@ -437,10 +469,13 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType title = presentationData.strings.AutoDownloadSettings_VideosTitle case .file: title = presentationData.strings.AutoDownloadSettings_DocumentsTitle + case .story: + //TODO:localize + title = "Stories" } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift index 9cf4bbfb4a..7b2bdaa0da 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift @@ -192,6 +192,11 @@ private func notificationsExceptionEntries(presentationData: PresentationData, n } else { continue } + case .stories: + if case .user = peer { + } else { + continue + } } if existingPeerIds.contains(peer.id) { continue @@ -311,6 +316,8 @@ private enum NotificationExceptionEntry : ItemListNodeEntry { icon = PresentationResourcesItemList.createGroupIcon(theme) case .channels: icon = PresentationResourcesItemList.addChannelIcon(theme) + case .stories: + icon = PresentationResourcesItemList.addPersonIcon(theme) } return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: strings.Notification_Exceptions_AddException, alwaysPlain: true, sectionId: self.section, editing: editing, action: { arguments.selectPeer() @@ -617,6 +624,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { let canRemove = mode.peerIds.contains(peerId) + var isStories = false let defaultSound: PeerMessageSound switch mode { case .channels: @@ -625,9 +633,12 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { defaultSound = globalSettings.groupChats.sound._asMessageSound() case .users: defaultSound = globalSettings.privateChats.sound._asMessageSound() + case .stories: + defaultSound = globalSettings.privateChats.sound._asMessageSound() + isStories = true } - presentControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, threadId: nil, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { peerId, sound in + presentControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, threadId: nil, isStories: isStories, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { peerId, sound in _ = updatePeerSound(peer.id, sound).start(next: { _ in updateNotificationsDisposable.set(nil) _ = combineLatest(updatePeerSound(peer.id, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in @@ -698,7 +709,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { switch mode { case .groups: filter.insert(.onlyGroups) - case .users: + case .users, .stories: filter.insert(.onlyPrivateChats) filter.insert(.excludeSavedMessages) filter.insert(.excludeSecretChats) diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift index 2ad55a81a8..e0918e90e1 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift @@ -144,6 +144,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { case privateChats(PresentationTheme, String, String, String) case groupChats(PresentationTheme, String, String, String) case channels(PresentationTheme, String, String, String) + case stories(PresentationTheme, String, String, String) case inAppHeader(PresentationTheme, String) case inAppSounds(PresentationTheme, String, Bool) @@ -170,7 +171,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return NotificationsAndSoundsSection.accounts.rawValue case .permissionInfo, .permissionEnable: return NotificationsAndSoundsSection.permission.rawValue - case .categoriesHeader, .privateChats, .groupChats, .channels: + case .categoriesHeader, .privateChats, .groupChats, .channels, .stories: return NotificationsAndSoundsSection.categories.rawValue case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: return NotificationsAndSoundsSection.inApp.rawValue @@ -205,6 +206,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return 7 case .channels: return 8 + case .stories: + return 9 case .inAppHeader: return 14 case .inAppSounds: @@ -317,6 +320,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } else { return false } + case let .stories(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel): + if case let .stories(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel { + return true + } else { + return false + } case let .inAppHeader(lhsTheme, lhsText): if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -441,6 +450,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { arguments.openPeerCategory(.channel) }) + case let .stories(_, title, subtitle, label): + return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Stories"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { + arguments.openPeerCategory(.stories) + }) case let .inAppHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .inAppSounds(_, text, value): @@ -499,7 +512,7 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound } } -private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warningSuppressed: Bool, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode), presentationData: PresentationData, hasMoreThanOneAccount: Bool) -> [NotificationsAndSoundsEntry] { +private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warningSuppressed: Bool, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode), presentationData: PresentationData, hasMoreThanOneAccount: Bool) -> [NotificationsAndSoundsEntry] { var entries: [NotificationsAndSoundsEntry] = [] if hasMoreThanOneAccount { @@ -538,6 +551,8 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn entries.append(.privateChats(presentationData.theme, presentationData.strings.Notifications_PrivateChats, !exceptions.users.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.users.peerIds.count)) : "", globalSettings.privateChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) entries.append(.groupChats(presentationData.theme, presentationData.strings.Notifications_GroupChats, !exceptions.groups.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.groups.peerIds.count)) : "", globalSettings.groupChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) entries.append(.channels(presentationData.theme, presentationData.strings.Notifications_Channels, !exceptions.channels.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.channels.peerIds.count)) : "", globalSettings.channels.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) + //TODO:localize + entries.append(.stories(presentationData.theme, "Stories", !exceptions.stories.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.stories.peerIds.count)) : "", globalSettings.privateChats.storiesMuted != false ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased())) entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds)) @@ -567,9 +582,9 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? - let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)> = Promise() + let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)> = Promise() - let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)) -> Void = { value in + let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)) -> Void = { value in notificationExceptions.set(.single(value)) } @@ -603,7 +618,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions context.sharedContext.applicationBindings.openSettings() })]), nil) }, openPeerCategory: { category in - _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in let mode: NotificationExceptionMode switch category { case .privateChat: @@ -612,16 +627,20 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions mode = groups case .channel: mode = channels + case .stories: + mode = stories } pushControllerImpl?(notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { mode in - _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in switch mode { case .users: - updateNotificationExceptions((mode, groups, channels)) + updateNotificationExceptions((mode, groups, channels, stories)) case .groups: - updateNotificationExceptions((users, mode, channels)) + updateNotificationExceptions((users, mode, channels, stories)) case .channels: - updateNotificationExceptions((users, groups, mode)) + updateNotificationExceptions((users, groups, mode, stories)) + case .stories: + updateNotificationExceptions((users, groups, channels, mode)) } }) }, focusOnItemTag: nil)) @@ -711,13 +730,18 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions let exceptionsSignal = Signal.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init)) - notificationExceptions.set(exceptionsSignal |> map { list -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in + notificationExceptions.set(exceptionsSignal |> map { list -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in var users:[PeerId : NotificationExceptionWrapper] = [:] var groups: [PeerId : NotificationExceptionWrapper] = [:] - var channels:[PeerId : NotificationExceptionWrapper] = [:] + var channels: [PeerId : NotificationExceptionWrapper] = [:] + var stories: [PeerId : NotificationExceptionWrapper] = [:] if let list = list { for (key, value) in list.settings { - if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { + if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { + if value.storiesMuted != nil { + stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + } + switch value.muteState { case .default: switch value.messageSound { @@ -751,7 +775,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions } } - return (.users(users), .groups(groups), .channels(channels)) + return (.users(users), .groups(groups), .channels(channels), .stories(stories)) }) let notificationsWarningSuppressed = Promise(true) diff --git a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift index aa436ffab9..fc22468a53 100644 --- a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift +++ b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift @@ -261,14 +261,20 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor notificationSettings = globalSettings.groupChats case .channel: notificationSettings = globalSettings.channels + case .stories: + notificationSettings = globalSettings.privateChats } - entries.append(.enable(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, notificationSettings.enabled)) + if case .stories = category { + entries.append(.enable(presentationData.theme, "Show All Notifications", notificationSettings.storiesMuted == nil || notificationSettings.storiesMuted == false)) + } else { + entries.append(.enable(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, notificationSettings.enabled)) - if notificationSettings.enabled || !notificationExceptions.isEmpty { - entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) - entries.append(.previews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, notificationSettings.displayPreviews)) - entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.sound)), filteredGlobalSound(notificationSettings.sound))) + if notificationSettings.enabled || !notificationExceptions.isEmpty { + entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) + entries.append(.previews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, notificationSettings.displayPreviews)) + entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.sound)), filteredGlobalSound(notificationSettings.sound))) + } } entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased())) @@ -304,8 +310,16 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor for (_, value) in sortedExceptions { if !value.peer.isDeleted { var title: String - var muted = false - switch value.settings.muteState { + + if case .stories = category { + if value.settings.storiesMuted == true { + title = presentationData.strings.Notification_Exceptions_AlwaysOff + } else { + title = presentationData.strings.Notification_Exceptions_AlwaysOn + } + } else { + var muted = false + switch value.settings.muteState { case let .muted(until): if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { if until < Int32.max - 1 { @@ -332,18 +346,18 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor title = presentationData.strings.Notification_Exceptions_AlwaysOn default: title = "" - } - if !muted { - switch value.settings.messageSound { - case .default: - break - default: - if !title.isEmpty { - title.append(", ") - } - title.append(presentationData.strings.Notification_Exceptions_SoundCustom) } - switch value.settings.displayPreviews { + if !muted { + switch value.settings.messageSound { + case .default: + break + default: + if !title.isEmpty { + title.append(", ") + } + title.append(presentationData.strings.Notification_Exceptions_SoundCustom) + } + switch value.settings.displayPreviews { case .default: break default: @@ -355,6 +369,7 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor } else { title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff } + } } } existingPeerIds.insert(value.peer.id) @@ -374,6 +389,7 @@ public enum NotificationsPeerCategory { case privateChat case group case channel + case stories } private final class NotificationExceptionState : Equatable { @@ -429,9 +445,9 @@ public func notificationsPeerCategoryController(context: AccountContext, categor statePromise.set(NotificationExceptionState(mode: mode)) - let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)> = Promise() + let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)> = Promise() - let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)) -> Void = { value in + let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)) -> Void = { value in notificationExceptions.set(.single(value)) } @@ -526,6 +542,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor let canRemove = mode.peerIds.contains(peerId) let defaultSound: PeerMessageSound + var isStories = false switch mode { case .channels: defaultSound = globalSettings.channels.sound._asMessageSound() @@ -533,9 +550,12 @@ public func notificationsPeerCategoryController(context: AccountContext, categor defaultSound = globalSettings.groupChats.sound._asMessageSound() case .users: defaultSound = globalSettings.privateChats.sound._asMessageSound() + case .stories: + defaultSound = globalSettings.privateChats.sound._asMessageSound() + isStories = true } - pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, threadId: nil, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { peerId, sound in + pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, threadId: nil, isStories: isStories, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { peerId, sound in _ = updatePeerSound(peer.id, sound).start(next: { _ in updateNotificationsDisposable.set(nil) _ = combineLatest(updatePeerSound(peer.id, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in @@ -607,6 +627,8 @@ public func notificationsPeerCategoryController(context: AccountContext, categor settings.groupChats.enabled = value case .channel: settings.channels.enabled = value + case .stories: + settings.privateChats.storiesMuted = !value } return settings }).start() @@ -620,6 +642,8 @@ public func notificationsPeerCategoryController(context: AccountContext, categor settings.groupChats.displayPreviews = value case .channel: settings.channels.displayPreviews = value + case .stories: + break } return settings }).start() @@ -634,6 +658,8 @@ public func notificationsPeerCategoryController(context: AccountContext, categor settings.groupChats.sound = value case .channel: settings.channels.sound = value + case .stories: + break } return settings }).start() @@ -643,7 +669,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor let presentationData = context.sharedContext.currentPresentationData.with { $0 } var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] switch category { - case .privateChat: + case .privateChat, .stories: filter.insert(.onlyPrivateChats) filter.insert(.excludeSavedMessages) filter.insert(.excludeSecretChats) @@ -712,14 +738,16 @@ public func notificationsPeerCategoryController(context: AccountContext, categor }) }) }, updatedExceptionMode: { mode in - _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in switch mode { case .users: - updateNotificationExceptions((mode, groups, channels)) + updateNotificationExceptions((mode, groups, channels, stories)) case .groups: - updateNotificationExceptions((users, mode, channels)) + updateNotificationExceptions((users, mode, channels, stories)) case .channels: - updateNotificationExceptions((users, groups, mode)) + updateNotificationExceptions((users, groups, mode, stories)) + case .stories: + updateNotificationExceptions((users, groups, channels, mode)) } }) }) @@ -780,6 +808,9 @@ public func notificationsPeerCategoryController(context: AccountContext, categor title = presentationData.strings.Notifications_GroupChatsTitle case .channel: title = presentationData.strings.Notifications_ChannelsTitle + case .stories: + //TODO:localize + title = "Stories" } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ea846884af..3eabf4b700 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -361,7 +361,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2107670217] = { return Api.InputPeer.parse_inputPeerSelf($0) } dict[-571955892] = { return Api.InputPeer.parse_inputPeerUser($0) } dict[-1468331492] = { return Api.InputPeer.parse_inputPeerUserFromMessage($0) } - dict[-505078139] = { return Api.InputPeerNotifySettings.parse_inputPeerNotifySettings($0) } + dict[-892638494] = { return Api.InputPeerNotifySettings.parse_inputPeerNotifySettings($0) } dict[506920429] = { return Api.InputPhoneCall.parse_inputPhoneCall($0) } dict[1001634122] = { return Api.InputPhoto.parse_inputPhoto($0) } dict[483901197] = { return Api.InputPhoto.parse_inputPhotoEmpty($0) } @@ -619,7 +619,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-386039788] = { return Api.PeerBlocked.parse_peerBlocked($0) } dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) } dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) } - dict[1826385490] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) } + dict[-1721619444] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) } dict[-1525149427] = { return Api.PeerSettings.parse_peerSettings($0) } dict[-1770029977] = { return Api.PhoneCall.parse_phoneCall($0) } dict[912311057] = { return Api.PhoneCall.parse_phoneCallAccepted($0) } diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 5e8d704f16..eaf1f77123 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -754,13 +754,13 @@ public extension Api { } public extension Api { enum PeerNotifySettings: TypeConstructorDescription { - case peerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, iosSound: Api.NotificationSound?, androidSound: Api.NotificationSound?, otherSound: Api.NotificationSound?, storiesMuted: Api.Bool?) + case peerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, iosSound: Api.NotificationSound?, androidSound: Api.NotificationSound?, otherSound: Api.NotificationSound?, storiesMuted: Api.Bool?, storiesHideSender: Api.Bool?, storiesIosSound: Api.NotificationSound?, storiesAndroidSound: Api.NotificationSound?, storiesOtherSound: Api.NotificationSound?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .peerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let iosSound, let androidSound, let otherSound, let storiesMuted): + case .peerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let iosSound, let androidSound, let otherSound, let storiesMuted, let storiesHideSender, let storiesIosSound, let storiesAndroidSound, let storiesOtherSound): if boxed { - buffer.appendInt32(1826385490) + buffer.appendInt32(-1721619444) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {showPreviews!.serialize(buffer, true)} @@ -770,14 +770,18 @@ public extension Api { if Int(flags) & Int(1 << 4) != 0 {androidSound!.serialize(buffer, true)} if Int(flags) & Int(1 << 5) != 0 {otherSound!.serialize(buffer, true)} if Int(flags) & Int(1 << 6) != 0 {storiesMuted!.serialize(buffer, true)} + if Int(flags) & Int(1 << 7) != 0 {storiesHideSender!.serialize(buffer, true)} + if Int(flags) & Int(1 << 8) != 0 {storiesIosSound!.serialize(buffer, true)} + if Int(flags) & Int(1 << 9) != 0 {storiesAndroidSound!.serialize(buffer, true)} + if Int(flags) & Int(1 << 10) != 0 {storiesOtherSound!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .peerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let iosSound, let androidSound, let otherSound, let storiesMuted): - return ("peerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("iosSound", iosSound as Any), ("androidSound", androidSound as Any), ("otherSound", otherSound as Any), ("storiesMuted", storiesMuted as Any)]) + case .peerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let iosSound, let androidSound, let otherSound, let storiesMuted, let storiesHideSender, let storiesIosSound, let storiesAndroidSound, let storiesOtherSound): + return ("peerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("iosSound", iosSound as Any), ("androidSound", androidSound as Any), ("otherSound", otherSound as Any), ("storiesMuted", storiesMuted as Any), ("storiesHideSender", storiesHideSender as Any), ("storiesIosSound", storiesIosSound as Any), ("storiesAndroidSound", storiesAndroidSound as Any), ("storiesOtherSound", storiesOtherSound as Any)]) } } @@ -810,6 +814,22 @@ public extension Api { if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Bool } } + var _9: Api.Bool? + if Int(_1!) & Int(1 << 7) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.Bool + } } + var _10: Api.NotificationSound? + if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.NotificationSound + } } + var _11: Api.NotificationSound? + if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.NotificationSound + } } + var _12: Api.NotificationSound? + if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { + _12 = Api.parse(reader, signature: signature) as? Api.NotificationSound + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil @@ -818,8 +838,12 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.PeerNotifySettings.peerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, iosSound: _5, androidSound: _6, otherSound: _7, storiesMuted: _8) + let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 9) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 10) == 0) || _12 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { + return Api.PeerNotifySettings.peerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, iosSound: _5, androidSound: _6, otherSound: _7, storiesMuted: _8, storiesHideSender: _9, storiesIosSound: _10, storiesAndroidSound: _11, storiesOtherSound: _12) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index fbdbbf9cbe..8605e3757f 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -278,13 +278,13 @@ public extension Api { } public extension Api { enum InputPeerNotifySettings: TypeConstructorDescription { - case inputPeerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, sound: Api.NotificationSound?, storiesMuted: Api.Bool?) + case inputPeerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, sound: Api.NotificationSound?, storiesMuted: Api.Bool?, storiesHideSender: Api.Bool?, storiesSound: Api.NotificationSound?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .inputPeerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let sound, let storiesMuted): + case .inputPeerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let sound, let storiesMuted, let storiesHideSender, let storiesSound): if boxed { - buffer.appendInt32(-505078139) + buffer.appendInt32(-892638494) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {showPreviews!.serialize(buffer, true)} @@ -292,14 +292,16 @@ public extension Api { if Int(flags) & Int(1 << 2) != 0 {serializeInt32(muteUntil!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {sound!.serialize(buffer, true)} if Int(flags) & Int(1 << 6) != 0 {storiesMuted!.serialize(buffer, true)} + if Int(flags) & Int(1 << 7) != 0 {storiesHideSender!.serialize(buffer, true)} + if Int(flags) & Int(1 << 8) != 0 {storiesSound!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .inputPeerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let sound, let storiesMuted): - return ("inputPeerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("sound", sound as Any), ("storiesMuted", storiesMuted as Any)]) + case .inputPeerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let sound, let storiesMuted, let storiesHideSender, let storiesSound): + return ("inputPeerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("sound", sound as Any), ("storiesMuted", storiesMuted as Any), ("storiesHideSender", storiesHideSender as Any), ("storiesSound", storiesSound as Any)]) } } @@ -324,14 +326,24 @@ public extension Api { if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Bool } } + var _7: Api.Bool? + if Int(_1!) & Int(1 << 7) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Bool + } } + var _8: Api.NotificationSound? + if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.NotificationSound + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 6) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, sound: _5, storiesMuted: _6) + let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, sound: _5, storiesMuted: _6, storiesHideSender: _7, storiesSound: _8) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift index 09ba02cd2e..e8d76aa730 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift @@ -6,7 +6,7 @@ import TelegramApi extension TelegramPeerNotificationSettings { convenience init(apiSettings: Api.PeerNotifySettings) { switch apiSettings { - case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted): + case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, _, _, _, _): let sound: Api.NotificationSound? #if os(iOS) sound = iosSound diff --git a/submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift b/submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift index a59a986843..48b0d6ba98 100644 --- a/submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift @@ -6,7 +6,7 @@ import TelegramApi extension MessageNotificationSettings { init(apiSettings: Api.PeerNotifySettings) { switch apiSettings { - case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted): + case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, _, _, _, _): let sound: Api.NotificationSound? #if os(iOS) sound = iosSound diff --git a/submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift b/submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift index 7942519e36..aa893f7cce 100644 --- a/submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift @@ -116,7 +116,7 @@ private func fetchedNotificationSettings(network: Network) -> Signal map { chats, users, channels, contactsJoinedMuted in let chatsSettings: MessageNotificationSettings switch chats { - case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted): + case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, _, _, _, _): let sound: Api.NotificationSound? #if os(iOS) sound = iosSound @@ -147,7 +147,7 @@ private func fetchedNotificationSettings(network: Network) -> Signal Signal Signal { diff --git a/submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift b/submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift index 8cf808ffb3..1aa931a29a 100644 --- a/submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift @@ -136,7 +136,7 @@ func pushPeerNotificationSettings(postbox: Postbox, network: Network, peerId: Pe storiesMuted = storiesMutedValue ? .boolTrue : .boolFalse } - let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted) + let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted, storiesHideSender: nil, storiesSound: nil) return network.request(Api.functions.account.updateNotifySettings(peer: .inputNotifyForumTopic(peer: inputPeer, topMsgId: Int32(clamping: threadId)), settings: inputSettings)) |> `catch` { _ -> Signal in return .single(.boolFalse) @@ -184,7 +184,7 @@ func pushPeerNotificationSettings(postbox: Postbox, network: Network, peerId: Pe flags |= (1 << 6) storiesMuted = storiesMutedValue ? .boolTrue : .boolFalse } - let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted) + let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted, storiesHideSender: nil, storiesSound: nil) return network.request(Api.functions.account.updateNotifySettings(peer: .inputNotifyPeer(peer: inputPeer), settings: inputSettings)) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 7a7c646222..875ea751dd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -709,14 +709,15 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal } |> mapToSignal { result -> Signal in switch result { - case let .botApp(_, app): + case let .botApp(botAppFlags, app): switch app { case let .botApp(flags, id, accessHash, shortName, title, description, photo, document, hash): + let _ = flags var appFlags = BotApp.Flags() - if (flags & (1 << 0)) != 0 { + if (botAppFlags & (1 << 0)) != 0 { appFlags.insert(.notActivated) } - if (flags & (1 << 1)) != 0 { + if (botAppFlags & (1 << 1)) != 0 { appFlags.insert(.requiresWriteAccess) } return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: appFlags)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index f8519c044b..f4ed86f64b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -457,21 +457,27 @@ public final class EngineStorySubscriptions: Equatable { public let peer: EnginePeer public let hasUnseen: Bool public let storyCount: Int + public let unseenCount: Int public let lastTimestamp: Int32 public init( peer: EnginePeer, hasUnseen: Bool, storyCount: Int, + unseenCount: Int, lastTimestamp: Int32 ) { self.peer = peer self.hasUnseen = hasUnseen self.storyCount = storyCount + self.unseenCount = unseenCount self.lastTimestamp = lastTimestamp } public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } if lhs.peer != rhs.peer { return false } @@ -481,6 +487,9 @@ public final class EngineStorySubscriptions: Equatable { if lhs.storyCount != rhs.storyCount { return false } + if lhs.unseenCount != rhs.unseenCount { + return false + } if lhs.lastTimestamp != rhs.lastTimestamp { return false } @@ -499,6 +508,9 @@ public final class EngineStorySubscriptions: Equatable { } public static func ==(lhs: EngineStorySubscriptions, rhs: EngineStorySubscriptions) -> Bool { + if lhs === rhs { + return true + } if lhs.accountItem != rhs.accountItem { return false } @@ -1061,7 +1073,7 @@ func _internal_markStoryAsSeen(account: Account, peerId: PeerId, id: Int32, asPi account.stateManager.injectStoryUpdates(updates: [.read(peerId: peerId, maxId: id)]) - #if DEBUG && false + #if DEBUG && true if "".isEmpty { return .complete() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 58a44b0d09..e71475c14b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -680,6 +680,7 @@ public extension TelegramEngine { peer: EnginePeer(accountPeer), hasUnseen: false, storyCount: 0, + unseenCount: 0, lastTimestamp: 0 ) @@ -692,14 +693,22 @@ public extension TelegramEngine { if let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) { let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) var hasUnseen = false + var unseenCount = 0 if let peerState = peerState { hasUnseen = peerState.maxReadId < lastEntry.id + + for item in itemsView.items { + if item.id > peerState.maxReadId { + unseenCount += 1 + } + } } let item = EngineStorySubscriptions.Item( peer: EnginePeer(accountPeer), hasUnseen: hasUnseen, storyCount: itemsView.items.count, + unseenCount: unseenCount, lastTimestamp: lastEntry.timestamp ) accountItem = item @@ -726,14 +735,22 @@ public extension TelegramEngine { let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) var hasUnseen = false + var unseenCount = 0 if let peerState = peerState { hasUnseen = peerState.maxReadId < lastEntry.id + + for item in itemsView.items { + if item.id > peerState.maxReadId { + unseenCount += 1 + } + } } let item = EngineStorySubscriptions.Item( peer: EnginePeer(peer), hasUnseen: hasUnseen, storyCount: itemsView.items.count, + unseenCount: unseenCount, lastTimestamp: lastEntry.timestamp ) @@ -752,6 +769,13 @@ public extension TelegramEngine { return false } } + if lhs.peer.isService != rhs.peer.isService { + if lhs.peer.isService { + return true + } else { + return false + } + } if lhs.peer.isPremium != rhs.peer.isPremium { if lhs.peer.isPremium { return true @@ -804,12 +828,9 @@ public extension TelegramEngine { return self.account.postbox.combinedView(keys: additionalDataKeys) |> map { views -> [EngineMediaResource.Id: StoryPreloadInfo] in let _ = accountPeer - let _ = storySubscriptionsView let _ = storiesStateView - var nextPriority: Int = 0 - - var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:] + var sortedItems: [(peer: Peer, item: Stories.Item)] = [] for peerId in storySubscriptionsView.peerIds { guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else { @@ -834,36 +855,51 @@ public extension TelegramEngine { } } - if let peerReference = PeerReference(peer) { - if let nextItem = nextItem, case let .item(item) = nextItem, let media = item.media { - if let image = media as? TelegramMediaImage, let resource = image.representations.last?.resource { - let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: media), resource: resource) - resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( - resource: resource, - size: nil, - priority: .top(position: nextPriority) - ) - nextPriority += 1 - } else if let file = media as? TelegramMediaFile { - if let preview = file.previewRepresentations.last { - let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource) - resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( - resource: resource, - size: nil, - priority: .top(position: nextPriority) - ) - nextPriority += 1 - } - - let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource) - resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( - resource: resource, - size: file.preloadSize, - priority: .top(position: nextPriority) - ) - nextPriority += 1 - } + if let nextItem = nextItem, case let .item(item) = nextItem { + sortedItems.append((peer, item)) + } + } + + sortedItems.sort(by: { lhs, rhs in + return lhs.item.timestamp > rhs.item.timestamp + }) + + var nextPriority: Int = 0 + var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:] + + for itemAndPeer in sortedItems.prefix(10) { + guard let peerReference = PeerReference(itemAndPeer.peer) else { + continue + } + guard let media = itemAndPeer.item.media else { + continue + } + if let image = media as? TelegramMediaImage, let resource = image.representations.last?.resource { + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: itemAndPeer.item.id, media: media), resource: resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: nil, + priority: .top(position: nextPriority) + ) + nextPriority += 1 + } else if let file = media as? TelegramMediaFile { + if let preview = file.previewRepresentations.last { + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: itemAndPeer.item.id, media: file), resource: preview.resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: nil, + priority: .top(position: nextPriority) + ) + nextPriority += 1 } + + let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: itemAndPeer.item.id, media: file), resource: file.resource) + resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo( + resource: resource, + size: file.preloadSize, + priority: .top(position: nextPriority) + ) + nextPriority += 1 } } diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index c5d36188e9..f766710a1a 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -60,8 +60,8 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?, peerIds.removeAll(where: { $0 == updated.id }) transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) } - if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) if !peerIds.contains(updated.id) { peerIds.append(updated.id) transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) @@ -69,11 +69,11 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?, } } else { if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) peerIds.removeAll(where: { $0 == updated.id }) transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) } - if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { + if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) if !peerIds.contains(updated.id) { peerIds.append(updated.id) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index d70b7e17e5..53ffe7dbb7 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1256,24 +1256,25 @@ public struct PresentationResourcesChat { context.clip() let color: UIColor + let foregroundColor: UIColor switch type { case .incoming: - color = theme.chat.message.incoming.mediaPlaceholderColor + color = theme.chat.message.incoming.mediaActiveControlColor.withMultipliedAlpha(0.1) + foregroundColor = theme.chat.message.incoming.mediaActiveControlColor case .outgoing: - color = theme.chat.message.outgoing.mediaPlaceholderColor + color = theme.chat.message.outgoing.mediaActiveControlColor.withMultipliedAlpha(0.1) + foregroundColor = theme.chat.message.outgoing.mediaActiveControlColor case .free: - color = theme.chat.message.freeform.withWallpaper.fill[0] + color = theme.chat.message.freeform.withWallpaper.reactionActiveMediaPlaceholder + foregroundColor = theme.chat.message.freeform.withWallpaper.reactionActiveBackground } context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: .white), let icon = UIImage(bundleImageName: "Chat/Message/ExpiredStoryPlaceholder") { + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: foregroundColor) { UIGraphicsPushContext(context) - icon.draw(in: CGRect(origin: CGPoint(), size: size)) - - //image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), blendMode: .destinationOut, alpha: 1.0) image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), blendMode: .normal, alpha: 1.0) UIGraphicsPopContext() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 3ad6b0a5fd..d41d823934 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -158,8 +158,12 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } else { titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor - if let _ = storyData { - completeSourceString = strings.Message_ForwardedStoryShort(peerString) + if let storyData = storyData { + if storyData.isExpired { + completeSourceString = strings.Message_ForwardedExpiredStoryShort(peerString) + } else { + completeSourceString = strings.Message_ForwardedStoryShort(peerString) + } } else { completeSourceString = strings.Message_ForwardedMessageShort(peerString) } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 5019cf5fc3..d5d24278f7 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -13,9 +13,11 @@ import StoryPeerListComponent public final class ChatListNavigationBar: Component { public final class AnimationHint { let disableStoriesAnimations: Bool + let crossfadeStoryPeers: Bool - public init(disableStoriesAnimations: Bool) { + public init(disableStoriesAnimations: Bool, crossfadeStoryPeers: Bool) { self.disableStoriesAnimations = disableStoriesAnimations + self.crossfadeStoryPeers = crossfadeStoryPeers } } @@ -37,6 +39,7 @@ public final class ChatListNavigationBar: Component { public let accessoryPanelContainerHeight: CGFloat public let activateSearch: (NavigationBarSearchContentNode) -> Void public let openStatusSetup: (UIView) -> Void + public let allowAutomaticOrder: () -> Void public init( context: AccountContext, @@ -56,7 +59,8 @@ public final class ChatListNavigationBar: Component { accessoryPanelContainer: ASDisplayNode?, accessoryPanelContainerHeight: CGFloat, activateSearch: @escaping (NavigationBarSearchContentNode) -> Void, - openStatusSetup: @escaping (UIView) -> Void + openStatusSetup: @escaping (UIView) -> Void, + allowAutomaticOrder: @escaping () -> Void ) { self.context = context self.theme = theme @@ -76,6 +80,7 @@ public final class ChatListNavigationBar: Component { self.accessoryPanelContainerHeight = accessoryPanelContainerHeight self.activateSearch = activateSearch self.openStatusSetup = openStatusSetup + self.allowAutomaticOrder = allowAutomaticOrder } public static func ==(lhs: ChatListNavigationBar, rhs: ChatListNavigationBar) -> Bool { @@ -363,6 +368,9 @@ public final class ChatListNavigationBar: Component { } } } + if self.storiesUnlocked != storiesUnlocked, !storiesUnlocked { + component.allowAutomaticOrder() + } self.storiesUnlocked = storiesUnlocked let headerComponent = ChatListHeaderComponent( @@ -536,7 +544,8 @@ public final class ChatListNavigationBar: Component { accessoryPanelContainer: component.accessoryPanelContainer, accessoryPanelContainerHeight: component.accessoryPanelContainerHeight, activateSearch: component.activateSearch, - openStatusSetup: component.openStatusSetup + openStatusSetup: component.openStatusSetup, + allowAutomaticOrder: component.allowAutomaticOrder ) if let currentLayout = self.currentLayout, let headerComponent = self.currentHeaderComponent { let headerComponent = ChatListHeaderComponent( @@ -572,10 +581,14 @@ public final class ChatListNavigationBar: Component { let themeUpdated = self.component?.theme !== component.theme var uploadProgressUpdated = false + var storySubscriptionsUpdated = false if let previousComponent = self.component { if previousComponent.uploadProgress != component.uploadProgress { uploadProgressUpdated = true } + if previousComponent.storySubscriptions != component.storySubscriptions { + storySubscriptionsUpdated = true + } } self.component = component @@ -614,7 +627,7 @@ public final class ChatListNavigationBar: Component { self.hasDeferredScrollOffset = true - if uploadProgressUpdated { + if uploadProgressUpdated || storySubscriptionsUpdated { if let rawScrollOffset = self.rawScrollOffset { self.applyScroll(offset: rawScrollOffset, allowAvatarsExpansion: self.currentAllowAvatarsExpansion, forceUpdate: true, transition: transition) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 3299e6ccfb..87bfa8c2f1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -259,6 +259,11 @@ public final class MediaEditor { self.histogramPromise.set(.single(data)) } } + + if case let .asset(asset) = subject { + self.playerPlaybackState = (asset.duration, 0.0, false, false) + self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) + } } deinit { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5ea0fe1d83..43080e70c1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -603,11 +603,14 @@ final class MediaEditorScreenComponent: Component { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment - if self.component == nil { - if let controller = environment.controller() as? MediaEditorScreen { + var isEditingStory = false + if let controller = environment.controller() as? MediaEditorScreen { + isEditingStory = controller.isEditingStory + if self.component == nil { self.inputPanelExternalState.initialText = controller.initialCaption } } + self.component = component self.state = state @@ -957,7 +960,7 @@ final class MediaEditorScreenComponent: Component { self.state?.updated(transition: .immediate) } }, - timeoutAction: { [weak self] view in + timeoutAction: isEditingStory ? nil : { [weak self] view in guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { return } @@ -985,7 +988,8 @@ final class MediaEditorScreenComponent: Component { timeoutSelected: timeoutSelected, displayGradient: false, bottomInset: 0.0, - hideKeyboard: self.currentInputMode == .emoji + hideKeyboard: self.currentInputMode == .emoji, + disabledPlaceholder: nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) @@ -2861,6 +2865,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public var cancelled: (Bool) -> Void = { _ in } public var completion: (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy , @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _, _ in } public var dismissed: () -> Void = { } + public var willDismiss: () -> Void = { } private let hapticFeedback = HapticFeedback() @@ -3173,6 +3178,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.cancelled(saveDraft) + self.willDismiss() + self.node.animateOut(finished: false, saveDraft: saveDraft, completion: { [weak self] in self?.dismiss() self?.dismissed() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 3664227f64..b3f7fa550f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -276,7 +276,8 @@ final class StoryPreviewComponent: Component { timeoutSelected: false, displayGradient: false, bottomInset: 0.0, - hideKeyboard: false + hideKeyboard: false, + disabledPlaceholder: nil )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index ce70885505..446e65a298 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -171,6 +171,10 @@ final class VideoScrubberComponent: Component { context.fillPath() }) + self.zoneView.image = UIImage() + self.zoneView.isUserInteractionEnabled = true + self.zoneView.hitTestSlop = UIEdgeInsets(top: -8.0, left: 0.0, bottom: -8.0, right: 0.0) + self.leftHandleView.image = handleImage self.leftHandleView.isUserInteractionEnabled = true self.leftHandleView.tintColor = .white @@ -193,6 +197,7 @@ final class VideoScrubberComponent: Component { context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - borderHeight), size: CGSize(width: size.width, height: scrubberHeight))) })?.withRenderingMode(.alwaysTemplate) self.borderView.tintColor = .white + self.borderView.isUserInteractionEnabled = false self.transparentFramesContainer.alpha = 0.5 self.transparentFramesContainer.clipsToBounds = true @@ -241,7 +246,7 @@ final class VideoScrubberComponent: Component { let delta = translation.x / length let duration = component.endPosition - component.startPosition - let startValue = max(0.0, min(component.duration - duration, component.startPosition + delta)) + let startValue = max(0.0, min(component.duration - duration, component.startPosition + delta * component.duration)) let endValue = startValue + duration var transition: Transition = .immediate diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 37270d3685..49c1288b7e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -70,6 +70,7 @@ public final class MessageInputPanelComponent: Component { public let displayGradient: Bool public let bottomInset: CGFloat public let hideKeyboard: Bool + public let disabledPlaceholder: String? public init( externalState: ExternalState, @@ -102,7 +103,8 @@ public final class MessageInputPanelComponent: Component { timeoutSelected: Bool, displayGradient: Bool, bottomInset: CGFloat, - hideKeyboard: Bool + hideKeyboard: Bool, + disabledPlaceholder: String? ) { self.externalState = externalState self.context = context @@ -135,6 +137,7 @@ public final class MessageInputPanelComponent: Component { self.displayGradient = displayGradient self.bottomInset = bottomInset self.hideKeyboard = hideKeyboard + self.disabledPlaceholder = disabledPlaceholder } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -198,6 +201,9 @@ public final class MessageInputPanelComponent: Component { if lhs.hideKeyboard != rhs.hideKeyboard { return false } + if lhs.disabledPlaceholder != rhs.disabledPlaceholder { + return false + } return true } @@ -214,6 +220,7 @@ public final class MessageInputPanelComponent: Component { private let placeholder = ComponentView() private let vibrancyPlaceholder = ComponentView() + private var disabledPlaceholder: ComponentView? private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() @@ -495,10 +502,12 @@ public final class MessageInputPanelComponent: Component { transition.setPosition(view: placeholderView, position: placeholderFrame.origin) placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) - transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0) - transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0) + transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) + transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) } + transition.setAlpha(view: self.fieldBackgroundView, alpha: component.disabledPlaceholder != nil ? 0.0 : 1.0) + let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom) if let textFieldView = self.textField.view { @@ -506,7 +515,38 @@ public final class MessageInputPanelComponent: Component { self.addSubview(textFieldView) } transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)) - transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0) + transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) + } + + if let disabledPlaceholderText = component.disabledPlaceholder { + let disabledPlaceholder: ComponentView + var disabledPlaceholderTransition = transition + if let current = self.disabledPlaceholder { + disabledPlaceholder = current + } else { + disabledPlaceholderTransition = .immediate + disabledPlaceholder = ComponentView() + self.disabledPlaceholder = disabledPlaceholder + } + let disabledPlaceholderSize = disabledPlaceholder.update( + transition: .immediate, + component: AnyComponent(Text(text: disabledPlaceholderText, font: Font.regular(17.0), color: UIColor(rgb: 0xffffff, alpha: 0.3))), + environment: {}, + containerSize: CGSize(width: fieldBackgroundFrame.width - 8.0 * 2.0, height: 100.0) + ) + let disabledPlaceholderFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX + floor((fieldBackgroundFrame.width - disabledPlaceholderSize.width) * 0.5), y: fieldBackgroundFrame.minY + floor((fieldBackgroundFrame.height - disabledPlaceholderSize.height) * 0.5)), size: disabledPlaceholderSize) + if let disabledPlaceholderView = disabledPlaceholder.view { + if disabledPlaceholderView.superview == nil { + self.addSubview(disabledPlaceholderView) + } + disabledPlaceholderTransition.setPosition(view: disabledPlaceholderView, position: disabledPlaceholderFrame.center) + disabledPlaceholderView.bounds = CGRect(origin: CGPoint(), size: disabledPlaceholderFrame.size) + } + } else { + if let disabledPlaceholder = self.disabledPlaceholder { + self.disabledPlaceholder = nil + disabledPlaceholder.view?.removeFromSuperview() + } } if component.attachmentAction != nil { @@ -845,7 +885,7 @@ public final class MessageInputPanelComponent: Component { transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size)) - transition.setAlpha(view: stickerButtonView, alpha: (hasMediaRecording || hasMediaEditing || !inputModeVisible) ? 0.0 : 1.0) + transition.setAlpha(view: stickerButtonView, alpha: (hasMediaRecording || hasMediaEditing || !inputModeVisible || component.disabledPlaceholder != nil) ? 0.0 : 1.0) transition.setScale(view: stickerButtonView, scale: (hasMediaRecording || hasMediaEditing || !inputModeVisible) ? 0.1 : 1.0) if inputModeVisible { diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index d3dd2e3c1a..5572e2434e 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -466,7 +466,7 @@ public func threadNotificationExceptionsScreen(context: AccountContext, peerId: let canRemove = true let defaultSound: PeerMessageSound = globalSettings.groupChats.sound._asMessageSound() - pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, customTitle: item.info.title, threadId: item.threadId, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { _, sound in + pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, customTitle: item.info.title, threadId: item.threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { _, sound in let _ = (updateThreadSound(item.threadId, sound) |> deliverOnMainQueue).start(next: { _ in updateState { value in diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift index 16ac20934c..b6a4b3e3ff 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift @@ -46,6 +46,7 @@ public enum NotificationExceptionMode : Equatable { case users case groups case channels + case stories } public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool { @@ -68,6 +69,12 @@ public enum NotificationExceptionMode : Equatable { } else { return false } + case let .stories(lhsValue): + if case let .stories(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } } } @@ -79,12 +86,14 @@ public enum NotificationExceptionMode : Equatable { return .groups case .channels: return .channels + case .stories: + return .stories } } public var isEmpty: Bool { switch self { - case let .users(value), let .groups(value), let .channels(value): + case let .users(value), let .groups(value), let .channels(value), let .stories(value): return value.isEmpty } } @@ -92,6 +101,7 @@ public enum NotificationExceptionMode : Equatable { case users([EnginePeer.Id : NotificationExceptionWrapper]) case groups([EnginePeer.Id : NotificationExceptionWrapper]) case channels([EnginePeer.Id : NotificationExceptionWrapper]) + case stories([EnginePeer.Id : NotificationExceptionWrapper]) public func withUpdatedPeerSound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionMode { let apply:([EnginePeer.Id : NotificationExceptionWrapper], EnginePeer.Id, PeerMessageSound) -> [EnginePeer.Id : NotificationExceptionWrapper] = { values, peerId, sound in @@ -120,12 +130,14 @@ public enum NotificationExceptionMode : Equatable { } switch self { - case let .groups(values): - return .groups(apply(values, peer.id, sound)) - case let .users(values): - return .users(apply(values, peer.id, sound)) - case let .channels(values): - return .channels(apply(values, peer.id, sound)) + case let .groups(values): + return .groups(apply(values, peer.id, sound)) + case let .users(values): + return .users(apply(values, peer.id, sound)) + case let .channels(values): + return .channels(apply(values, peer.id, sound)) + case .stories: + return self } } @@ -172,12 +184,14 @@ public enum NotificationExceptionMode : Equatable { muteState = .default } switch self { - case let .groups(values): - return .groups(apply(values, peer.id, muteState)) - case let .users(values): - return .users(apply(values, peer.id, muteState)) - case let .channels(values): - return .channels(apply(values, peer.id, muteState)) + case let .groups(values): + return .groups(apply(values, peer.id, muteState)) + case let .users(values): + return .users(apply(values, peer.id, muteState)) + case let .channels(values): + return .channels(apply(values, peer.id, muteState)) + case .stories: + return self } } @@ -208,12 +222,14 @@ public enum NotificationExceptionMode : Equatable { } switch self { - case let .groups(values): - return .groups(apply(values, peer.id, displayPreviews)) - case let .users(values): - return .users(apply(values, peer.id, displayPreviews)) - case let .channels(values): - return .channels(apply(values, peer.id, displayPreviews)) + case let .groups(values): + return .groups(apply(values, peer.id, displayPreviews)) + case let .users(values): + return .users(apply(values, peer.id, displayPreviews)) + case let .channels(values): + return .channels(apply(values, peer.id, displayPreviews)) + case .stories: + return self } } @@ -254,25 +270,23 @@ public enum NotificationExceptionMode : Equatable { } switch self { - case let .groups(values): - return .groups(apply(values, peer.id, storyNotifications)) - case let .users(values): - return .users(apply(values, peer.id, storyNotifications)) - case let .channels(values): - return .channels(apply(values, peer.id, storyNotifications)) + case let .stories(values): + return .stories(apply(values, peer.id, storyNotifications)) + default: + return self } } public var peerIds: [EnginePeer.Id] { switch self { - case let .users(settings), let .groups(settings), let .channels(settings): - return settings.map {$0.key} + case let .users(settings), let .groups(settings), let .channels(settings), let .stories(settings): + return settings.map { $0.key } } } public var settings: [EnginePeer.Id : NotificationExceptionWrapper] { switch self { - case let .users(settings), let .groups(settings), let .channels(settings): + case let .users(settings), let .groups(settings), let .channels(settings), let .stories(settings): return settings } } @@ -535,7 +549,7 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { } -private func notificationPeerExceptionEntries(presentationData: PresentationData, peer: EnginePeer?, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] { +private func notificationPeerExceptionEntries(presentationData: PresentationData, peer: EnginePeer?, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState, isStories: Bool?) -> [NotificationPeerExceptionEntry] { let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList) var entries: [NotificationPeerExceptionEntry] = [] @@ -547,23 +561,26 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData index += 1 } - entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader)) - index += 1 - - - entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOn, selected: state.mode == .alwaysOn)) - index += 1 - entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOff, selected: state.mode == .alwaysOff)) - index += 1 - - if state.mode != .alwaysOff { - entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_MessagePreviewHeader)) - index += 1 - entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.displayPreviews == .alwaysOn)) - index += 1 - entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff)) + if isStories == nil || isStories == false { + entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader)) index += 1 + entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOn, selected: state.mode == .alwaysOn)) + index += 1 + entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOff, selected: state.mode == .alwaysOff)) + index += 1 + + if state.mode != .alwaysOff { + entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_MessagePreviewHeader)) + index += 1 + entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.displayPreviews == .alwaysOn)) + index += 1 + entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff)) + index += 1 + } + } + + if isStories == nil || isStories == true { if case .user = peer { //TODO:localize entries.append(.storyNotificationsHeader(index: index, theme: presentationData.theme, title: "STORY NOTIFICATIONS")) @@ -573,7 +590,9 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData entries.append(.storyNotifications(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.storyNotifications == .alwaysOff)) index += 1 } - + } + + if isStories == nil || isStories == false { entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones)) index += 1 @@ -674,7 +693,7 @@ private struct NotificationExceptionPeerState : Equatable { } } -public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, customTitle: String? = nil, threadId: Int64?, canRemove: Bool, defaultSound: PeerMessageSound, edit: Bool = false, updatePeerSound: @escaping(EnginePeer.Id, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(EnginePeer.Id, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(EnginePeer.Id, PeerNotificationDisplayPreviews) -> Void, updatePeerStoryNotifications: @escaping(EnginePeer.Id, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController { +public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, customTitle: String? = nil, threadId: Int64?, isStories: Bool?, canRemove: Bool, defaultSound: PeerMessageSound, edit: Bool = false, updatePeerSound: @escaping(EnginePeer.Id, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(EnginePeer.Id, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(EnginePeer.Id, PeerNotificationDisplayPreviews) -> Void, updatePeerStoryNotifications: @escaping(EnginePeer.Id, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController { let initialState = NotificationExceptionPeerState(canRemove: false) let statePromise = Promise(initialState) let stateValue = Atomic(value: initialState) @@ -782,7 +801,7 @@ public func notificationPeerExceptionController(context: AccountContext, updated } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(titleString), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, peer: peer, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: animated) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, peer: peer, notificationSoundList: notificationSoundList, state: state, isStories: isStories), style: .blocks, animateChanges: animated) return (controllerState, (listState, arguments)) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 603dbf7b57..d341d3ecbb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1567,7 +1567,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) - if totalCount == 0 { + if totalCount == 0 && state.loadMoreToken != nil { totalCount = 100 } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 160441f87e..54f1cec19c 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1323,13 +1323,23 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { iconColor: .blue, actionTitle: nil )) + + var contactsSubtitle = "exclude people" + if initialPrivacy.base == .contacts, initialPrivacy.additionallyIncludePeers.count > 0 { + if initialPrivacy.additionallyIncludePeers.count == 1 { + contactsSubtitle = "except 1 person" + } else { + contactsSubtitle = "except \(initialPrivacy.additionallyIncludePeers.count) people" + } + } categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( id: .contacts, title: "Contacts", icon: "Chat List/Tabs/IconContacts", iconColor: .yellow, - actionTitle: "exclude people" + actionTitle: contactsSubtitle )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( id: .closeFriends, title: "Close Friends", @@ -1337,12 +1347,21 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { iconColor: .green, actionTitle: "edit list" )) + + var selectedContactsSubtitle = "choose" + if initialPrivacy.base == .nobody, initialPrivacy.additionallyIncludePeers.count > 0 { + if initialPrivacy.additionallyIncludePeers.count == 1 { + selectedContactsSubtitle = "1 person" + } else { + selectedContactsSubtitle = "\(initialPrivacy.additionallyIncludePeers.count) people" + } + } categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( id: .selectedContacts, title: "Selected Contacts", icon: "Chat List/Filters/Group", iconColor: .violet, - actionTitle: "choose" + actionTitle: selectedContactsSubtitle )) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 32194ca7ad..ea0aefb6bd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -397,6 +397,8 @@ final class StoryContentCaptionComponent: Component { } textNode.textNode.view.addGestureRecognizer(recognizer) } + + textNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 100000.0, height: 100000.0)) } textNode.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset), size: textLayout.0.size) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 551b34ae58..36c0de7562 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1367,7 +1367,8 @@ public final class StoryItemSetContainerComponent: Component { timeoutSelected: false, displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular, bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, - hideKeyboard: self.sendMessageContext.currentInputMode == .media + hideKeyboard: self.sendMessageContext.currentInputMode == .media, + disabledPlaceholder: component.slice.peer.isService ? "You can't reply to this story" : nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) @@ -1696,7 +1697,7 @@ public final class StoryItemSetContainerComponent: Component { animateIn = true } viewListTransition.setFrame(view: viewListView, frame: viewListFrame) - viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI ? 0.0 : 1.0) + viewListTransition.setAlpha(view: viewListView, alpha: component.hideUI || self.isEditingStory ? 0.0 : 1.0) if animateIn, !transition.animation.isImmediate { viewListView.animateIn(transition: transition) @@ -1753,7 +1754,7 @@ public final class StoryItemSetContainerComponent: Component { let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0)) transition.setFrame(view: self.closeButton, frame: closeButtonFrame) transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size)) - transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) } let focusedItem: StoryContentItem? = component.slice.item @@ -1827,7 +1828,7 @@ public final class StoryItemSetContainerComponent: Component { //view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) } } @@ -1858,20 +1859,20 @@ public final class StoryItemSetContainerComponent: Component { view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } - transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) } } let gradientHeight: CGFloat = 74.0 transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight))) - transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) let itemSize = CGSize(width: contentFrame.width, height: floorToScreenPixels(contentFrame.width * 1.77778)) let itemLayout = ItemLayout(size: itemSize) //ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput)) self.itemLayout = itemLayout let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize) - var inputPanelAlpha: CGFloat = focusedItem?.isMy == true || component.hideUI ? 0.0 : 1.0 + var inputPanelAlpha: CGFloat = focusedItem?.isMy == true || component.hideUI || self.isEditingStory ? 0.0 : 1.0 if case .regular = component.metrics.widthClass { inputPanelAlpha *= component.visibilityFraction } @@ -1957,7 +1958,7 @@ public final class StoryItemSetContainerComponent: Component { self.contentContainerView.insertSubview(captionItemView, aboveSubview: self.contentDimView) } captionItemTransition.setFrame(view: captionItemView, frame: captionFrame) - captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList || self.inputPanelExternalState.isEditing) ? 0.0 : 1.0) + captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList || self.isEditingStory || self.inputPanelExternalState.isEditing) ? 0.0 : 1.0) } } @@ -2212,7 +2213,7 @@ public final class StoryItemSetContainerComponent: Component { } } var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha - if component.hideUI || self.displayViewList { + if component.hideUI || self.displayViewList || self.isEditingStory { dimAlpha = 0.0 } @@ -2256,7 +2257,7 @@ public final class StoryItemSetContainerComponent: Component { self.contentContainerView.addSubview(navigationStripView) } transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0))) - transition.setAlpha(view: navigationStripView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0) + transition.setAlpha(view: navigationStripView, alpha: (component.hideUI || self.displayViewList || self.isEditingStory) ? 0.0 : 1.0) } } @@ -2384,6 +2385,7 @@ public final class StoryItemSetContainerComponent: Component { self.isEditingStory = true self.updateIsProgressPaused() + self.state?.updated(transition: .easeInOut(duration: 0.2)) let subject: Signal // if let source { @@ -2399,7 +2401,7 @@ public final class StoryItemSetContainerComponent: Component { return .single(nil) |> then( .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) - |> delay(0.2, queue: Queue.mainQueue()) + |> delay(0.1, queue: Queue.mainQueue()) ) } else { return .single(.video(data.path, nil, nil, nil, PixelDimensions(width: 720, height: 1280), .bottomRight)) @@ -2448,6 +2450,7 @@ public final class StoryItemSetContainerComponent: Component { self.isEditingStory = false self.rewindCurrentItem() self.updateIsProgressPaused() + self.state?.updated(transition: .easeInOut(duration: 0.2)) commit({}) } @@ -2484,6 +2487,7 @@ public final class StoryItemSetContainerComponent: Component { self.isEditingStory = false self.rewindCurrentItem() self.updateIsProgressPaused() + self.state?.updated(transition: .easeInOut(duration: 0.2)) commit({}) } @@ -2501,6 +2505,7 @@ public final class StoryItemSetContainerComponent: Component { self.isEditingStory = false self.rewindCurrentItem() self.updateIsProgressPaused() + self.state?.updated(transition: .easeInOut(duration: 0.2)) } commit({}) } @@ -2512,15 +2517,18 @@ public final class StoryItemSetContainerComponent: Component { self.isEditingStory = false self.rewindCurrentItem() self.updateIsProgressPaused() + self.state?.updated(transition: .easeInOut(duration: 0.2)) commit({}) } } ) - controller.dismissed = { [weak self] in + controller.willDismiss = { [weak self] in self?.isEditingStory = false + self?.rewindCurrentItem() self?.updateIsProgressPaused() + self?.state?.updated(transition: .easeInOut(duration: 0.2)) } self.component?.controller()?.push(controller) updateProgressImpl = { [weak controller] progress in diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index aaa422da7b..165db2a6a8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -403,13 +403,15 @@ public final class StoryContentContextImpl: StoryContentContext { context: AccountContext, isHidden: Bool, focusedPeerId: EnginePeer.Id?, - singlePeer: Bool + singlePeer: Bool, + fixedOrder: [EnginePeer.Id] = [] ) { self.context = context self.isHidden = isHidden if let focusedPeerId { self.focusedItem = (focusedPeerId, nil) } + self.fixedSubscriptionOrder = fixedOrder if singlePeer { guard let focusedPeerId else { @@ -433,6 +435,7 @@ public final class StoryContentContextImpl: StoryContentContext { peer: peer, hasUnseen: state.hasUnseen, storyCount: state.items.count, + unseenCount: 0, lastTimestamp: state.items.last?.timestamp ?? 0 )], hasMoreToken: nil diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 91b9eefa01..fb00452f13 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4514,6 +4514,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let story = message.associatedStories[storyId], story.data.isEmpty { + //TODO:localize + self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: "This story is no longer available", timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) return } @@ -4696,7 +4698,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! self.avatarNode = avatarNode - avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme) + //avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme) case .feed: chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 6dab7fc27d..3a9790a1de 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -3743,6 +3743,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else { return .optionalAction(performAction) } + } else if let item = self.item, let story = item.message.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory { + if let storyItem = item.message.associatedStories[story.storyId] { + if storyItem.data.isEmpty { + return .action({ + item.controllerInteraction.navigateToStory(item.message, story.storyId) + }) + } else { + if let peer = item.message.peers[story.storyId.peerId] { + return .action({ + item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + }) + } + } + } } } loop: for contentNode in self.contentNodes { diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 8c4364964e..3358677668 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -60,7 +60,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.displayCallIcons = displayCallIcons var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 7e602158d9..383eb14dfc 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4724,7 +4724,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let canRemove = false - let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in + let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in let _ = (updatePeerSound(peer.id, sound) |> deliverOnMainQueue).start(next: { _ in }) diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index fcb337067a..ed8dbdfd8a 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -88,12 +88,14 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable { public var photo: MediaAutoDownloadCategory public var video: MediaAutoDownloadCategory public var file: MediaAutoDownloadCategory + public var stories: MediaAutoDownloadCategory - public init(basePreset: MediaAutoDownloadPreset, photo: MediaAutoDownloadCategory, video: MediaAutoDownloadCategory, file: MediaAutoDownloadCategory) { + public init(basePreset: MediaAutoDownloadPreset, photo: MediaAutoDownloadCategory, video: MediaAutoDownloadCategory, file: MediaAutoDownloadCategory, stories: MediaAutoDownloadCategory) { self.basePreset = basePreset self.photo = photo self.video = video self.file = file + self.stories = stories } public init(from decoder: Decoder) throws { @@ -103,6 +105,7 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable { self.photo = try container.decode(MediaAutoDownloadCategory.self, forKey: "photo") self.video = try container.decode(MediaAutoDownloadCategory.self, forKey: "video") self.file = try container.decode(MediaAutoDownloadCategory.self, forKey: "file") + self.stories = try container.decodeIfPresent(MediaAutoDownloadCategory.self, forKey: "stories") ?? MediaAutoDownloadSettings.defaultSettings.presets.high.stories } public func encode(to encoder: Encoder) throws { @@ -112,6 +115,7 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable { try container.encode(self.photo, forKey: "photo") try container.encode(self.video, forKey: "video") try container.encode(self.file, forKey: "file") + try container.encode(self.stories, forKey: "stories") } public static func < (lhs: MediaAutoDownloadCategories, rhs: MediaAutoDownloadCategories) -> Bool { @@ -370,15 +374,29 @@ public struct MediaAutoDownloadSettings: Codable, Equatable { public static var defaultSettings: MediaAutoDownloadSettings { let mb: Int64 = 1024 * 1024 - let presets = MediaAutoDownloadPresets(low: MediaAutoDownloadCategories(basePreset: .low, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), - video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), - file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false)), - medium: MediaAutoDownloadCategories(basePreset: .medium, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), - video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: Int64(2.5 * CGFloat(mb)), predownload: false), - file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false)), - high: MediaAutoDownloadCategories(basePreset: .high, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), - video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 10 * mb, predownload: true), - file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 3 * mb, predownload: false))) + let presets = MediaAutoDownloadPresets(low: + MediaAutoDownloadCategories( + basePreset: .low, + photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), + video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), + file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), + stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false) + ), + medium: MediaAutoDownloadCategories( + basePreset: .medium, + photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), + video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: Int64(2.5 * CGFloat(mb)), predownload: false), + file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), + stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false) + ), + high: MediaAutoDownloadCategories( + basePreset: .high, + photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), + video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 10 * mb, predownload: true), + file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 3 * mb, predownload: false), + stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false) + ) + ) return MediaAutoDownloadSettings(presets: presets, cellular: MediaAutoDownloadConnection(enabled: true, preset: .medium, custom: nil), wifi: MediaAutoDownloadConnection(enabled: true, preset: .high, custom: nil), downloadInBackground: true, energyUsageSettings: EnergyUsageSettings.default) } @@ -436,7 +454,13 @@ private func categoriesWithAutodownloadPreset(_ autodownloadPreset: Autodownload let fileEnabled = autodownloadPreset.fileSizeMax > 0 let fileSizeMax = autodownloadPreset.fileSizeMax > 0 ? autodownloadPreset.fileSizeMax : 1 * 1024 * 1024 - return MediaAutoDownloadCategories(basePreset: preset, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false), video: MediaAutoDownloadCategory(contacts: videoEnabled, otherPrivate: videoEnabled, groups: videoEnabled, channels: videoEnabled, sizeLimit: videoSizeMax, predownload: autodownloadPreset.preloadLargeVideo), file: MediaAutoDownloadCategory(contacts: fileEnabled, otherPrivate: fileEnabled, groups: fileEnabled, channels: fileEnabled, sizeLimit: fileSizeMax, predownload: false)) + return MediaAutoDownloadCategories( + basePreset: preset, + photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false), + video: MediaAutoDownloadCategory(contacts: videoEnabled, otherPrivate: videoEnabled, groups: videoEnabled, channels: videoEnabled, sizeLimit: videoSizeMax, predownload: autodownloadPreset.preloadLargeVideo), + file: MediaAutoDownloadCategory(contacts: fileEnabled, otherPrivate: fileEnabled, groups: fileEnabled, channels: fileEnabled, sizeLimit: fileSizeMax, predownload: false), + stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false) + ) } private func presetsWithAutodownloadSettings(_ autodownloadSettings: AutodownloadSettings) -> MediaAutoDownloadPresets { @@ -479,7 +503,11 @@ public func effectiveAutodownloadCategories(settings: MediaAutoDownloadSettings, } } -private func categoryAndSizeForMedia(_ media: Media?, categories: MediaAutoDownloadCategories) -> (MediaAutoDownloadCategory, Int32?)? { +private func categoryAndSizeForMedia(_ media: Media?, isStory: Bool, categories: MediaAutoDownloadCategories) -> (MediaAutoDownloadCategory, Int32?)? { + if isStory { + return (categories.stories, 0) + } + guard let media = media else { return (categories.photo, 0) } @@ -524,7 +552,7 @@ public func isAutodownloadEnabledForAnyPeerType(category: MediaAutoDownloadCateg return category.contacts || category.otherPrivate || category.groups || category.channels } -public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?) -> Bool { +public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set = Set(), media: Media?, isStory: Bool = false) -> Bool { if (networkType == .cellular && !settings.cellular.enabled) || (networkType == .wifi && !settings.wifi.enabled) { return false } @@ -537,7 +565,7 @@ public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings peerType = .contact } - if let (category, size) = categoryAndSizeForMedia(media, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) { + if let (category, size) = categoryAndSizeForMedia(media, isStory: isStory, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) { if let size = size { var sizeLimit = category.sizeLimit if let file = media as? TelegramMediaFile, file.isVoice { @@ -564,7 +592,7 @@ public func shouldPredownloadMedia(settings: MediaAutoDownloadSettings, peerType return false } - if let (category, _) = categoryAndSizeForMedia(media, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) { + if let (category, _) = categoryAndSizeForMedia(media, isStory: false, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) { guard isAutodownloadEnabledForPeerType(peerType, category: category) else { return false } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f0c925d8fe..5423270d01 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -464,6 +464,12 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else { return nil } + } else if pathComponents.count >= 3 && pathComponents[1] == "s" { + if let storyId = Int32(pathComponents[2]) { + return .peer(.name(pathComponents[0]), .story(storyId)) + } else { + return nil + } } else if pathComponents.count == 4 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]) { var timecode: Double?