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 c968ce804e..08b15c0ea9 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1754,7 +1754,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 } @@ -2409,6 +2409,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } + if peer.id == self.context.account.peerId { + if let storySubscriptions = self.storySubscriptions { + var openCamera = false + if let accountItem = storySubscriptions.accountItem { + openCamera = accountItem.storyCount == 0 + } else { + openCamera = true + } + + if openCamera { + self.openStoryCamera() + return + } + } + } + let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false) let _ = (storyContent.state |> take(1) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 508790c8b4..8cd58ccb48 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -2350,6 +2350,10 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool { + if abs(velocity) > 200.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 37d384f473..5af68a834d 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) + } + self.isPremiumDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) |> map { return $0?.isPremium ?? false @@ -419,7 +409,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, - storySubscriptions: self.storySubscriptions, + storySubscriptions: nil, storiesIncludeHidden: true, uploadProgress: nil, tabsNode: tabsNode, @@ -505,13 +495,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) } @@ -532,7 +522,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/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 155aa5352f..ecb9b0ef22 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -457,17 +457,20 @@ 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 } @@ -481,6 +484,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 } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 4169f4dcdd..d01bf17898 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -673,6 +673,7 @@ public extension TelegramEngine { peer: EnginePeer(accountPeer), hasUnseen: false, storyCount: 0, + unseenCount: 0, lastTimestamp: 0 ) @@ -685,14 +686,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 @@ -719,14 +728,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 ) @@ -745,6 +762,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 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/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 6ee138514a..4ec0812763 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -979,7 +979,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) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index f7f36d7ebd..0367a5846d 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/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 80381d742a..79121cec31 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -67,6 +67,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, @@ -99,7 +100,8 @@ public final class MessageInputPanelComponent: Component { timeoutSelected: Bool, displayGradient: Bool, bottomInset: CGFloat, - hideKeyboard: Bool + hideKeyboard: Bool, + disabledPlaceholder: String? ) { self.externalState = externalState self.context = context @@ -132,6 +134,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() @@ -488,10 +495,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 { @@ -499,7 +508,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 { @@ -833,7 +873,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/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 395ce11edc..a0515d1a4e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1294,7 +1294,8 @@ public final class StoryItemSetContainerComponent: Component { timeoutSelected: false, displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular, bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset, - hideKeyboard: false + hideKeyboard: false, + disabledPlaceholder: component.slice.peer.isService ? "You can't reply to this story" : nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index aaa422da7b..ea418bc009 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -433,6 +433,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?