This commit is contained in:
Ali 2023-06-23 18:13:28 +03:00
parent b8130348ab
commit d5e010b510
33 changed files with 724 additions and 325 deletions

View File

@ -9366,6 +9366,7 @@ Sorry for the inconvenience.";
"Paint.Flip" = "Flip"; "Paint.Flip" = "Flip";
"Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@"; "Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@";
"Message.ForwardedExpiredStoryShort" = "Expired Story\nFrom: %@";
"Conversation.StoryForwardTooltip.Chat.One" = "Story forwarded to **%@**"; "Conversation.StoryForwardTooltip.Chat.One" = "Story forwarded to **%@**";
"Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**"; "Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**";

View File

@ -746,7 +746,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
let canRemove = false 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) let _ = (updatePeerSound(peerId, sound)
|> deliverOnMainQueue).start(next: { _ in |> deliverOnMainQueue).start(next: { _ in
}) })

View File

@ -1754,7 +1754,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
var autodownloadEnabled = true 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 autodownloadEnabled = false
} }
@ -2409,6 +2409,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return 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 = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false)
let _ = (storyContent.state let _ = (storyContent.state
|> take(1) |> take(1)

View File

@ -2350,6 +2350,10 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
} }
private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool { private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool {
if abs(velocity) > 200.0 {
return false
}
if !isPrimary || self.inlineStackContainerNode == nil { if !isPrimary || self.inlineStackContainerNode == nil {
} else { } else {
return false return false

View File

@ -10,7 +10,7 @@ import PresentationDataUtils
import OverlayStatusController import OverlayStatusController
import LocalizedPeerData 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 let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
return context.engine.data.get( 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 |> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable -> [ContextMenuItem] in
var items: [ContextMenuItem] = [] 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 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( let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)

View File

@ -38,61 +38,18 @@ private let dropDownIcon = { () -> UIImage in
return image return image
}() }()
private enum ContactListNodeEntrySection: Int {
case stories = 0
case contacts = 1
}
private enum ContactListNodeEntryId: Hashable { private enum ContactListNodeEntryId: Hashable {
case search case search
case sort case sort
case permission(action: Bool) case permission(action: Bool)
case option(index: Int) case option(index: Int)
case peerId(Int64) case peerId(peerId: Int64, section: ContactListNodeEntrySection)
case deviceContact(DeviceContactStableId) 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 { private final class ContactListNodeInteraction {
@ -100,16 +57,18 @@ private final class ContactListNodeInteraction {
fileprivate let authorize: () -> Void fileprivate let authorize: () -> Void
fileprivate let suppressWarning: () -> Void fileprivate let suppressWarning: () -> Void
fileprivate let openPeer: (ContactListPeer, ContactListAction) -> 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() 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.activateSearch = activateSearch
self.authorize = authorize self.authorize = authorize
self.suppressWarning = suppressWarning self.suppressWarning = suppressWarning
self.openPeer = openPeer self.openPeer = openPeer
self.contextAction = contextAction self.contextAction = contextAction
self.openStories = openStories
} }
} }
@ -120,12 +79,17 @@ enum ContactListAnimation {
} }
private enum ContactListNodeEntry: Comparable, Identifiable { private enum ContactListNodeEntry: Comparable, Identifiable {
struct StoryData: Equatable {
var count: Int
var unseenCount: Int
}
case search(PresentationTheme, PresentationStrings) case search(PresentationTheme, PresentationStrings)
case sort(PresentationTheme, PresentationStrings, ContactsSortOrder) case sort(PresentationTheme, PresentationStrings, ContactsSortOrder)
case permissionInfo(PresentationTheme, String, String, Bool) case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String) case permissionEnable(PresentationTheme, String)
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) 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 { var stableId: ContactListNodeEntryId {
switch self { switch self {
@ -139,10 +103,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return .permission(action: true) return .permission(action: true)
case let .option(index, _, _, _, _): case let .option(index, _, _, _, _):
return .option(index: index) return .option(index: index)
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _): case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, storyData):
switch peer { switch peer {
case let .peer(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, _): case let .deviceContact(id, _):
return .deviceContact(id) return .deviceContact(id)
} }
@ -172,7 +136,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
}) })
case let .option(_, option, header, _, _): case let .option(_, option, header, _, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action) 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 var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer let itemPeer: ContactsPeerItemPeer
var isContextActionEnabled = false var isContextActionEnabled = false
@ -218,7 +182,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
switch itemPeer { switch itemPeer {
case let .peer(peer, _): case let .peer(peer, _):
if let peer = peer { if let peer = peer {
contextAction(peer, node, gesture, location) contextAction(peer, node, gesture, location, storyData != nil)
} }
case .deviceContact: case .deviceContact:
break 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 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) 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 { } else {
return false 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 { 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 { if lhsIndex != rhsIndex {
return false return false
} }
@ -318,6 +307,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
if lhsEnabled != rhsEnabled { if lhsEnabled != rhsEnabled {
return false return false
} }
if lhsStoryData != rhsStoryData {
return false
}
return true return true
default: default:
return false return false
@ -359,18 +351,25 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
case .peer: case .peer:
return true return true
} }
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _): case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData):
switch rhs { switch rhs {
case .search, .sort, .permissionInfo, .permissionEnable, .option: case .search, .sort, .permissionInfo, .permissionEnable, .option:
return false 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 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<EnginePeer.Id>, 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<EnginePeer.Id>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?) -> [ContactListNodeEntry] {
var entries: [ContactListNodeEntry] = [] var entries: [ContactListNodeEntry] = []
var commonHeader: ListViewItemHeader? var commonHeader: ListViewItemHeader?
@ -398,8 +397,13 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
} }
} }
if let storySubscriptions, !storySubscriptions.items.isEmpty {
addHeader = true
}
if addHeader { 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 { 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 index: Int = 0
var existingPeerIds = Set<ContactListPeerId>() var existingPeerIds = Set<ContactListPeerId>()
@ -546,7 +562,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
enabled = true 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 index += 1
} }
} }
@ -582,7 +598,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
default: default:
enabled = true 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 index += 1
} }
return entries return entries
@ -606,7 +622,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta
case .search: case .search:
//indexSections.apend(CollectionIndexNode.searchIndex) //indexSections.apend(CollectionIndexNode.searchIndex)
break break
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _): case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _):
if let header = header as? ContactListNameIndexHeader { if let header = header as? ContactListNameIndexHeader {
if !existingSections.contains(header.letter) { if !existingSections.contains(header.letter) {
existingSections.insert(header.letter) existingSections.insert(header.letter)
@ -727,6 +743,7 @@ public final class ContactListNode: ASDisplayNode {
private var didSetReady = false private var didSetReady = false
private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?)>() private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?)>()
let storySubscriptions = Promise<EngineStorySubscriptions?>(nil)
private let selectionStatePromise = Promise<ContactListNodeGroupSelectionState?>(nil) private let selectionStatePromise = Promise<ContactListNodeGroupSelectionState?>(nil)
private var selectionStateValue: ContactListNodeGroupSelectionState? { private var selectionStateValue: ContactListNodeGroupSelectionState? {
@ -803,7 +820,8 @@ public final class ContactListNode: ASDisplayNode {
public var openPeer: ((ContactListPeer, ContactListAction) -> Void)? public var openPeer: ((ContactListPeer, ContactListAction) -> Void)?
public var openPrivacyPolicy: (() -> Void)? public var openPrivacyPolicy: (() -> Void)?
public var suppressPermissionWarning: (() -> 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 previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
@ -819,7 +837,7 @@ public final class ContactListNode: ASDisplayNode {
private let isPeerEnabled: ((EnginePeer) -> Bool)? private let isPeerEnabled: ((EnginePeer) -> Bool)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, presentation: Signal<ContactListPresentation, NoError>, 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<PresentationData, NoError>)? = nil, presentation: Signal<ContactListPresentation, NoError>, 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.context = context
self.filters = filters self.filters = filters
self.displayPermissionPlaceholder = displayPermissionPlaceholder self.displayPermissionPlaceholder = displayPermissionPlaceholder
@ -915,7 +933,12 @@ public final class ContactListNode: ASDisplayNode {
strongSelf.openPeer?(peer, action) 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 self.indexNode.indexSelected = { [weak self] section in
guard let strongSelf = self, let layout = strongSelf.validLayout, let entries = previousEntries.with({ $0 }) else { 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 }) 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 break loop
} }
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _): case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _):
if let header = header as? ContactListNameIndexHeader { if let header = header as? ContactListNameIndexHeader {
if let scalar = UnicodeScalar(header.letter) { if let scalar = UnicodeScalar(header.letter) {
let title = "\(Character(scalar))" let title = "\(Character(scalar))"
@ -1190,7 +1213,7 @@ public final class ContactListNode: ASDisplayNode {
peers.append(.deviceContact(stableId, contact.0)) 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) 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)) 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([]) chatListSignal = .single([])
} }
return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get()) return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get())
|> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed -> Signal<ContactsListNodeTransition, NoError> in |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) })
for (peer, memberCount) in chatListPeers { for (peer, memberCount) in chatListPeers {
@ -1283,7 +1306,7 @@ public final class ContactListNode: ASDisplayNode {
if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty {
isEmpty = true 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 previous = previousEntries.swap(entries)
let previousSelection = previousSelectionState.swap(selectionState) let previousSelection = previousSelectionState.swap(selectionState)

View File

@ -509,66 +509,59 @@ public class ContactsController: ViewController {
self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
if let componentView = self.chatListHeaderView(), componentView.storyPeerAction == nil { self.contactsNode.openStories = { [weak self] peer, sourceNode in
componentView.storyPeerAction = { [weak self] peer 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 { guard let self else {
return return
} }
let storyContent = StoryContentContextImpl(context: self.context, isHidden: true, focusedPeerId: peer?.id, singlePeer: false) var transitionIn: StoryContainerScreen.TransitionIn?
let _ = (storyContent.state if let itemNode = sourceNode as? ContactsPeerItemNode {
|> take(1) transitionIn = StoryContainerScreen.TransitionIn(
|> deliverOnMainQueue).start(next: { [weak self] storyContentState in sourceView: itemNode.avatarNode.view,
guard let self else { sourceRect: itemNode.avatarNode.view.bounds,
return sourceCornerRadius: itemNode.avatarNode.view.bounds.height * 0.5,
} sourceIsAvatar: true
)
itemNode.avatarNode.isHidden = true
}
if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil { let storyContainerScreen = StoryContainerScreen(
return context: self.context,
} content: storyContent,
transitionIn: transitionIn,
var transitionIn: StoryContainerScreen.TransitionIn? transitionOut: { _, _ in
if let peer, let componentView = self.chatListHeaderView() { if let itemNode = sourceNode as? ContactsPeerItemNode {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) { let rect = itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: itemNode.view)
transitionIn = StoryContainerScreen.TransitionIn( return StoryContainerScreen.TransitionOut(
sourceView: transitionView, destinationView: itemNode.view,
sourceRect: transitionView.bounds, transitionView: nil,
sourceCornerRadius: transitionView.bounds.height * 0.5, destinationRect: rect,
sourceIsAvatar: true destinationCornerRadius: rect.height * 0.5,
destinationIsAvatar: true,
completed: { [weak itemNode] in
guard let itemNode else {
return
}
itemNode.avatarNode.isHidden = false
}
) )
} }
return nil
} }
)
self.push(storyContainerScreen)
})
}
let storyContainerScreen = StoryContainerScreen( /*componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
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)
})
}
componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
guard let self else { guard let self else {
return 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) 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) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
} }*/
} }
@objc private func sortPressed() { @objc private func sortPressed() {

View File

@ -63,6 +63,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
var openPeopleNearby: (() -> Void)? var openPeopleNearby: (() -> Void)?
var openInvite: (() -> Void)? var openInvite: (() -> Void)?
var openQrScan: (() -> Void)? var openQrScan: (() -> Void)?
var openStories: ((EnginePeer, ASDisplayNode) -> Void)?
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? 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 self.contactListNode = ContactListNode(context: context, presentation: presentation, displaySortOptions: true, contextAction: { peer, node, gesture, location, isStories in
contextAction?(peer, node, gesture, location) contextAction?(peer, node, gesture, location, isStories)
}) })
super.init() super.init()
@ -159,8 +160,8 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
} }
} }
contextAction = { [weak self] peer, node, gesture, location in contextAction = { [weak self] peer, node, gesture, location, isStories in
self?.contextAction(peer: peer, node: node, gesture: gesture, location: location) self?.contextAction(peer: peer, node: node, gesture: gesture, location: location, isStories: isStories)
} }
self.contactListNode.contentOffsetChanged = { [weak self] offset in self.contactListNode.contentOffsetChanged = { [weak self] offset in
@ -238,30 +239,19 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return return
} }
var wasEmpty = true
if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty {
wasEmpty = false
}
self.storySubscriptions = storySubscriptions self.storySubscriptions = storySubscriptions
let isEmpty = storySubscriptions.items.isEmpty self.contactListNode.storySubscriptions.set(.single(storySubscriptions))
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.storiesReady.set(.single(true)) 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)) self.isPremiumDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> map { |> map {
return $0?.isPremium ?? false return $0?.isPremium ?? false
@ -419,7 +409,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
primaryContent: primaryContent, primaryContent: primaryContent,
secondaryContent: nil, secondaryContent: nil,
secondaryTransition: 0.0, secondaryTransition: 0.0,
storySubscriptions: self.storySubscriptions, storySubscriptions: nil,
storiesIncludeHidden: true, storiesIncludeHidden: true,
uploadProgress: nil, uploadProgress: nil,
tabsNode: tabsNode, 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 { guard let contactsController = self.controller else {
return return
} }
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) 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) 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) contactsController.presentInGlobalOverlay(contextController)
} }
@ -532,7 +522,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
requestOpenPeerFromSearch(peer) requestOpenPeerFromSearch(peer)
} }
}, contextAction: { [weak self] peer, node, gesture, location in }, 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 }), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch { if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch() requestDeactivateSearch()

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -20,6 +20,7 @@ import ComponentFlow
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import EmojiStatusComponent import EmojiStatusComponent
import AvatarStoryIndicatorComponent
public final class ContactItemHighlighting { public final class ContactItemHighlighting {
public var chatLocation: ChatLocation? public var chatLocation: ChatLocation?
@ -179,6 +180,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let arrowAction: (() -> Void)? let arrowAction: (() -> Void)?
let animationCache: AnimationCache? let animationCache: AnimationCache?
let animationRenderer: MultiAnimationRenderer? let animationRenderer: MultiAnimationRenderer?
let hasUnseenStories: Bool?
let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)?
public let selectable: Bool public let selectable: Bool
@ -213,7 +216,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
itemHighlighting: ContactItemHighlighting? = nil, itemHighlighting: ContactItemHighlighting? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil,
animationCache: AnimationCache? = nil, animationCache: AnimationCache? = nil,
animationRenderer: MultiAnimationRenderer? = nil animationRenderer: MultiAnimationRenderer? = nil,
hasUnseenStories: Bool? = nil,
openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil
) { ) {
self.presentationData = presentationData self.presentationData = presentationData
self.style = style self.style = style
@ -243,6 +248,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.arrowAction = arrowAction self.arrowAction = arrowAction
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.hasUnseenStories = hasUnseenStories
self.openStories = openStories
if let index = index { if let index = index {
var letter: String = "#" var letter: String = "#"
@ -391,8 +398,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var nonExtractedRect: CGRect? private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode private let offsetContainerNode: ASDisplayNode
private let avatarNodeContainer: ASDisplayNode
private let avatarNode: AvatarNode public let avatarNode: AvatarNode
private var avatarStoryIndicator: ComponentView<Empty>?
private var avatarIconView: ComponentHostView<Empty>? private var avatarIconView: ComponentHostView<Empty>?
private var avatarIconComponent: EmojiStatusComponent? private var avatarIconComponent: EmojiStatusComponent?
private let titleNode: TextNode private let titleNode: TextNode
@ -493,8 +501,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.offsetContainerNode = ASDisplayNode() self.offsetContainerNode = ASDisplayNode()
self.avatarNodeContainer = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() self.avatarNode.isLayerBacked = false
self.titleNode = TextNode() self.titleNode = TextNode()
self.statusNode = TextNode() self.statusNode = TextNode()
@ -515,7 +524,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) 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.titleNode)
self.offsetContainerNode.addSubnode(self.statusNode) 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<Empty>
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 { if case let .thread(_, title, icon, color) = item.peer {
let animationCache = item.context.animationCache 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)) 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 { } else if let avatarIconView = strongSelf.avatarIconView {
strongSelf.avatarIconView = nil strongSelf.avatarIconView = nil
avatarIconView.removeFromSuperview() avatarIconView.removeFromSuperview()
strongSelf.avatarNode.isHidden = false strongSelf.avatarNodeContainer.isHidden = false
} }
let _ = titleApply() 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) 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]? { override public func headers() -> [ListViewItemHeader]? {
if let (item, _, _, _, _, _) = self.layoutParams { if let (item, _, _, _, _, _) = self.layoutParams {
return item.header.flatMap { [$0] } return item.header.flatMap { [$0] }
@ -1484,4 +1561,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
item.arrowAction?() item.arrowAction?()
} }
} }
@objc private func avatarStoryTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let (item, _, _, _, _, _) = self.layoutParams {
item.openStories?(item.peer, self)
}
}
}
} }

View File

@ -47,6 +47,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case dataUsageItem(PresentationTheme, PresentationStrings, AutomaticDownloadDataUsage, Int?, Bool) case dataUsageItem(PresentationTheme, PresentationStrings, AutomaticDownloadDataUsage, Int?, Bool)
case typesHeader(PresentationTheme, String) case typesHeader(PresentationTheme, String)
case photos(PresentationTheme, String, String, Bool) case photos(PresentationTheme, String, String, Bool)
case stories(PresentationTheme, String, String, Bool)
case videos(PresentationTheme, String, String, Bool) case videos(PresentationTheme, String, String, Bool)
case files(PresentationTheme, String, String, Bool) case files(PresentationTheme, String, String, Bool)
case voiceMessagesInfo(PresentationTheme, String) case voiceMessagesInfo(PresentationTheme, String)
@ -57,7 +58,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
return AutodownloadMediaCategorySection.master.rawValue return AutodownloadMediaCategorySection.master.rawValue
case .dataUsageHeader, .dataUsageItem: case .dataUsageHeader, .dataUsageItem:
return AutodownloadMediaCategorySection.dataUsage.rawValue return AutodownloadMediaCategorySection.dataUsage.rawValue
case .typesHeader, .photos, .videos, .files, .voiceMessagesInfo: case .typesHeader, .photos, .stories, .videos, .files, .voiceMessagesInfo:
return AutodownloadMediaCategorySection.types.rawValue return AutodownloadMediaCategorySection.types.rawValue
} }
} }
@ -74,12 +75,14 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
return 3 return 3
case .photos: case .photos:
return 4 return 4
case .videos: case .stories:
return 5 return 5
case .files: case .videos:
return 6 return 6
case .voiceMessagesInfo: case .files:
return 7 return 7
case .voiceMessagesInfo:
return 8
} }
} }
@ -115,6 +118,12 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .videos(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .videos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { if case let .videos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true 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: { 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) 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): 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: { 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) arguments.customize(.video)
@ -190,6 +203,17 @@ private struct AutomaticDownloadPeers {
} }
private func stringForAutomaticDownloadPeers(strings: PresentationStrings, decimalSeparator: String, peers: AutomaticDownloadPeers, category: AutomaticDownloadCategory) -> String { 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? var size: String?
if var peersSize = peers.size, category == .video || category == .file { if var peersSize = peers.size, category == .video || category == .file {
if peersSize == Int32.max { if peersSize == Int32.max {
@ -253,6 +277,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
let photo = AutomaticDownloadPeers(category: categories.photo) let photo = AutomaticDownloadPeers(category: categories.photo)
let video = AutomaticDownloadPeers(category: categories.video) let video = AutomaticDownloadPeers(category: categories.video)
let file = AutomaticDownloadPeers(category: categories.file) let file = AutomaticDownloadPeers(category: categories.file)
let stories = AutomaticDownloadPeers(category: categories.stories)
entries.append(.master(presentationData.theme, presentationData.strings.AutoDownloadSettings_AutoDownload, master)) 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(.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(.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(.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(.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)) entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo))

View File

@ -49,6 +49,7 @@ enum AutomaticDownloadCategory {
case photo case photo
case video case video
case file case file
case story
} }
private enum AutomaticDownloadPeerType { private enum AutomaticDownloadPeerType {
@ -243,38 +244,54 @@ private func autodownloadMediaCategoryControllerEntries(presentationData: Presen
let predownload: Bool let predownload: Bool
switch category { switch category {
case .photo: case .photo:
peers = AutomaticDownloadPeers(category: categories.photo) peers = AutomaticDownloadPeers(category: categories.photo)
size = categories.photo.sizeLimit size = categories.photo.sizeLimit
predownload = categories.photo.predownload predownload = categories.photo.predownload
case .video: case .video:
peers = AutomaticDownloadPeers(category: categories.video) peers = AutomaticDownloadPeers(category: categories.video)
size = categories.video.sizeLimit size = categories.video.sizeLimit
predownload = categories.video.predownload predownload = categories.video.predownload
case .file: case .file:
peers = AutomaticDownloadPeers(category: categories.file) peers = AutomaticDownloadPeers(category: categories.file)
size = categories.file.sizeLimit size = categories.file.sizeLimit
predownload = categories.file.predownload predownload = categories.file.predownload
case .story:
peers = AutomaticDownloadPeers(category: categories.stories)
size = categories.stories.sizeLimit
predownload = categories.stories.predownload
} }
let downloadTitle: String let downloadTitle: String
var sizeTitle: String? var sizeTitle: String?
switch category { switch category {
case .photo: case .photo:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos
case .video: case .video:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize
case .file: case .file:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
case .story:
//TODO:localize
downloadTitle = "AUTO-DOWNLOAD STORIES"
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
} }
entries.append(.peerHeader(presentationData.theme, downloadTitle)) if case .story = category {
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts)) entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate)) //TODO:localize
entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups)) if peers.contacts {
entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels)) 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 { switch category {
case .video, .file: case .video, .file:
@ -339,7 +356,18 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.file.groups = !categories.file.groups categories.file.groups = !categories.file.groups
case .channel: case .channel:
categories.file.channels = !categories.file.channels 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 { switch connectionType {
case .cellular: case .cellular:
@ -362,6 +390,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.video.sizeLimit = size categories.video.sizeLimit = size
case .file: case .file:
categories.file.sizeLimit = size categories.file.sizeLimit = size
case .story:
categories.stories.sizeLimit = size
} }
switch connectionType { switch connectionType {
case .cellular: case .cellular:
@ -384,6 +414,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.video.predownload = !categories.video.predownload categories.video.predownload = !categories.video.predownload
case .file: case .file:
categories.file.predownload = !categories.file.predownload categories.file.predownload = !categories.file.predownload
case .story:
categories.stories.predownload = !categories.stories.predownload
} }
switch connectionType { switch connectionType {
case .cellular: case .cellular:
@ -437,10 +469,13 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
title = presentationData.strings.AutoDownloadSettings_VideosTitle title = presentationData.strings.AutoDownloadSettings_VideosTitle
case .file: case .file:
title = presentationData.strings.AutoDownloadSettings_DocumentsTitle 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -192,6 +192,11 @@ private func notificationsExceptionEntries(presentationData: PresentationData, n
} else { } else {
continue continue
} }
case .stories:
if case .user = peer {
} else {
continue
}
} }
if existingPeerIds.contains(peer.id) { if existingPeerIds.contains(peer.id) {
continue continue
@ -311,6 +316,8 @@ private enum NotificationExceptionEntry : ItemListNodeEntry {
icon = PresentationResourcesItemList.createGroupIcon(theme) icon = PresentationResourcesItemList.createGroupIcon(theme)
case .channels: case .channels:
icon = PresentationResourcesItemList.addChannelIcon(theme) 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: { return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: strings.Notification_Exceptions_AddException, alwaysPlain: true, sectionId: self.section, editing: editing, action: {
arguments.selectPeer() arguments.selectPeer()
@ -617,6 +624,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode {
let canRemove = mode.peerIds.contains(peerId) let canRemove = mode.peerIds.contains(peerId)
var isStories = false
let defaultSound: PeerMessageSound let defaultSound: PeerMessageSound
switch mode { switch mode {
case .channels: case .channels:
@ -625,9 +633,12 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode {
defaultSound = globalSettings.groupChats.sound._asMessageSound() defaultSound = globalSettings.groupChats.sound._asMessageSound()
case .users: case .users:
defaultSound = globalSettings.privateChats.sound._asMessageSound() 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 _ = updatePeerSound(peer.id, sound).start(next: { _ in
updateNotificationsDisposable.set(nil) updateNotificationsDisposable.set(nil)
_ = combineLatest(updatePeerSound(peer.id, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in _ = 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 { switch mode {
case .groups: case .groups:
filter.insert(.onlyGroups) filter.insert(.onlyGroups)
case .users: case .users, .stories:
filter.insert(.onlyPrivateChats) filter.insert(.onlyPrivateChats)
filter.insert(.excludeSavedMessages) filter.insert(.excludeSavedMessages)
filter.insert(.excludeSecretChats) filter.insert(.excludeSecretChats)

View File

@ -144,6 +144,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case privateChats(PresentationTheme, String, String, String) case privateChats(PresentationTheme, String, String, String)
case groupChats(PresentationTheme, String, String, String) case groupChats(PresentationTheme, String, String, String)
case channels(PresentationTheme, String, String, String) case channels(PresentationTheme, String, String, String)
case stories(PresentationTheme, String, String, String)
case inAppHeader(PresentationTheme, String) case inAppHeader(PresentationTheme, String)
case inAppSounds(PresentationTheme, String, Bool) case inAppSounds(PresentationTheme, String, Bool)
@ -170,7 +171,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return NotificationsAndSoundsSection.accounts.rawValue return NotificationsAndSoundsSection.accounts.rawValue
case .permissionInfo, .permissionEnable: case .permissionInfo, .permissionEnable:
return NotificationsAndSoundsSection.permission.rawValue return NotificationsAndSoundsSection.permission.rawValue
case .categoriesHeader, .privateChats, .groupChats, .channels: case .categoriesHeader, .privateChats, .groupChats, .channels, .stories:
return NotificationsAndSoundsSection.categories.rawValue return NotificationsAndSoundsSection.categories.rawValue
case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews:
return NotificationsAndSoundsSection.inApp.rawValue return NotificationsAndSoundsSection.inApp.rawValue
@ -205,6 +206,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return 7 return 7
case .channels: case .channels:
return 8 return 8
case .stories:
return 9
case .inAppHeader: case .inAppHeader:
return 14 return 14
case .inAppSounds: case .inAppSounds:
@ -317,6 +320,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .inAppHeader(lhsTheme, lhsText):
if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true 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: { 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) 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): case let .inAppHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .inAppSounds(_, text, value): 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] = [] var entries: [NotificationsAndSoundsEntry] = []
if hasMoreThanOneAccount { 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(.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(.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)) 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(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased()))
entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds)) 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 presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> 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)) notificationExceptions.set(.single(value))
} }
@ -603,7 +618,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
context.sharedContext.applicationBindings.openSettings() context.sharedContext.applicationBindings.openSettings()
})]), nil) })]), nil)
}, openPeerCategory: { category in }, 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 let mode: NotificationExceptionMode
switch category { switch category {
case .privateChat: case .privateChat:
@ -612,16 +627,20 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
mode = groups mode = groups
case .channel: case .channel:
mode = channels mode = channels
case .stories:
mode = stories
} }
pushControllerImpl?(notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { mode in 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 { switch mode {
case .users: case .users:
updateNotificationExceptions((mode, groups, channels)) updateNotificationExceptions((mode, groups, channels, stories))
case .groups: case .groups:
updateNotificationExceptions((users, mode, channels)) updateNotificationExceptions((users, mode, channels, stories))
case .channels: case .channels:
updateNotificationExceptions((users, groups, mode)) updateNotificationExceptions((users, groups, mode, stories))
case .stories:
updateNotificationExceptions((users, groups, channels, mode))
} }
}) })
}, focusOnItemTag: nil)) }, focusOnItemTag: nil))
@ -711,13 +730,18 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
let exceptionsSignal = Signal<NotificationExceptionsList?, NoError>.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init)) let exceptionsSignal = Signal<NotificationExceptionsList?, NoError>.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 users:[PeerId : NotificationExceptionWrapper] = [:]
var groups: [PeerId : NotificationExceptionWrapper] = [:] var groups: [PeerId : NotificationExceptionWrapper] = [:]
var channels:[PeerId : NotificationExceptionWrapper] = [:] var channels: [PeerId : NotificationExceptionWrapper] = [:]
var stories: [PeerId : NotificationExceptionWrapper] = [:]
if let list = list { if let list = list {
for (key, value) in list.settings { 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 { switch value.muteState {
case .default: case .default:
switch value.messageSound { 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<Bool>(true) let notificationsWarningSuppressed = Promise<Bool>(true)

View File

@ -261,14 +261,20 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
notificationSettings = globalSettings.groupChats notificationSettings = globalSettings.groupChats
case .channel: case .channel:
notificationSettings = globalSettings.channels 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 { if notificationSettings.enabled || !notificationExceptions.isEmpty {
entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased()))
entries.append(.previews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, notificationSettings.displayPreviews)) 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(.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())) entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased()))
@ -304,8 +310,16 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
for (_, value) in sortedExceptions { for (_, value) in sortedExceptions {
if !value.peer.isDeleted { if !value.peer.isDeleted {
var title: String 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): case let .muted(until):
if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 { if until < Int32.max - 1 {
@ -332,18 +346,18 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
title = presentationData.strings.Notification_Exceptions_AlwaysOn title = presentationData.strings.Notification_Exceptions_AlwaysOn
default: default:
title = "" 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: case .default:
break break
default: default:
@ -355,6 +369,7 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
} else { } else {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff
} }
}
} }
} }
existingPeerIds.insert(value.peer.id) existingPeerIds.insert(value.peer.id)
@ -374,6 +389,7 @@ public enum NotificationsPeerCategory {
case privateChat case privateChat
case group case group
case channel case channel
case stories
} }
private final class NotificationExceptionState : Equatable { private final class NotificationExceptionState : Equatable {
@ -429,9 +445,9 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
statePromise.set(NotificationExceptionState(mode: mode)) 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)) notificationExceptions.set(.single(value))
} }
@ -526,6 +542,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
let canRemove = mode.peerIds.contains(peerId) let canRemove = mode.peerIds.contains(peerId)
let defaultSound: PeerMessageSound let defaultSound: PeerMessageSound
var isStories = false
switch mode { switch mode {
case .channels: case .channels:
defaultSound = globalSettings.channels.sound._asMessageSound() defaultSound = globalSettings.channels.sound._asMessageSound()
@ -533,9 +550,12 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
defaultSound = globalSettings.groupChats.sound._asMessageSound() defaultSound = globalSettings.groupChats.sound._asMessageSound()
case .users: case .users:
defaultSound = globalSettings.privateChats.sound._asMessageSound() 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 _ = updatePeerSound(peer.id, sound).start(next: { _ in
updateNotificationsDisposable.set(nil) updateNotificationsDisposable.set(nil)
_ = combineLatest(updatePeerSound(peer.id, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in _ = 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 settings.groupChats.enabled = value
case .channel: case .channel:
settings.channels.enabled = value settings.channels.enabled = value
case .stories:
settings.privateChats.storiesMuted = !value
} }
return settings return settings
}).start() }).start()
@ -620,6 +642,8 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
settings.groupChats.displayPreviews = value settings.groupChats.displayPreviews = value
case .channel: case .channel:
settings.channels.displayPreviews = value settings.channels.displayPreviews = value
case .stories:
break
} }
return settings return settings
}).start() }).start()
@ -634,6 +658,8 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
settings.groupChats.sound = value settings.groupChats.sound = value
case .channel: case .channel:
settings.channels.sound = value settings.channels.sound = value
case .stories:
break
} }
return settings return settings
}).start() }).start()
@ -643,7 +669,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch category { switch category {
case .privateChat: case .privateChat, .stories:
filter.insert(.onlyPrivateChats) filter.insert(.onlyPrivateChats)
filter.insert(.excludeSavedMessages) filter.insert(.excludeSavedMessages)
filter.insert(.excludeSecretChats) filter.insert(.excludeSecretChats)
@ -712,14 +738,16 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
}) })
}) })
}, updatedExceptionMode: { mode in }, 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 { switch mode {
case .users: case .users:
updateNotificationExceptions((mode, groups, channels)) updateNotificationExceptions((mode, groups, channels, stories))
case .groups: case .groups:
updateNotificationExceptions((users, mode, channels)) updateNotificationExceptions((users, mode, channels, stories))
case .channels: 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 title = presentationData.strings.Notifications_GroupChatsTitle
case .channel: case .channel:
title = presentationData.strings.Notifications_ChannelsTitle 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 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) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem)

View File

@ -457,17 +457,20 @@ public final class EngineStorySubscriptions: Equatable {
public let peer: EnginePeer public let peer: EnginePeer
public let hasUnseen: Bool public let hasUnseen: Bool
public let storyCount: Int public let storyCount: Int
public let unseenCount: Int
public let lastTimestamp: Int32 public let lastTimestamp: Int32
public init( public init(
peer: EnginePeer, peer: EnginePeer,
hasUnseen: Bool, hasUnseen: Bool,
storyCount: Int, storyCount: Int,
unseenCount: Int,
lastTimestamp: Int32 lastTimestamp: Int32
) { ) {
self.peer = peer self.peer = peer
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.storyCount = storyCount self.storyCount = storyCount
self.unseenCount = unseenCount
self.lastTimestamp = lastTimestamp self.lastTimestamp = lastTimestamp
} }
@ -481,6 +484,9 @@ public final class EngineStorySubscriptions: Equatable {
if lhs.storyCount != rhs.storyCount { if lhs.storyCount != rhs.storyCount {
return false return false
} }
if lhs.unseenCount != rhs.unseenCount {
return false
}
if lhs.lastTimestamp != rhs.lastTimestamp { if lhs.lastTimestamp != rhs.lastTimestamp {
return false return false
} }

View File

@ -673,6 +673,7 @@ public extension TelegramEngine {
peer: EnginePeer(accountPeer), peer: EnginePeer(accountPeer),
hasUnseen: false, hasUnseen: false,
storyCount: 0, storyCount: 0,
unseenCount: 0,
lastTimestamp: 0 lastTimestamp: 0
) )
@ -685,14 +686,22 @@ public extension TelegramEngine {
if let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) { if let lastEntry = itemsView.items.last?.value.get(Stories.StoredItem.self) {
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
var hasUnseen = false var hasUnseen = false
var unseenCount = 0
if let peerState = peerState { if let peerState = peerState {
hasUnseen = peerState.maxReadId < lastEntry.id hasUnseen = peerState.maxReadId < lastEntry.id
for item in itemsView.items {
if item.id > peerState.maxReadId {
unseenCount += 1
}
}
} }
let item = EngineStorySubscriptions.Item( let item = EngineStorySubscriptions.Item(
peer: EnginePeer(accountPeer), peer: EnginePeer(accountPeer),
hasUnseen: hasUnseen, hasUnseen: hasUnseen,
storyCount: itemsView.items.count, storyCount: itemsView.items.count,
unseenCount: unseenCount,
lastTimestamp: lastEntry.timestamp lastTimestamp: lastEntry.timestamp
) )
accountItem = item accountItem = item
@ -719,14 +728,22 @@ public extension TelegramEngine {
let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self) let peerState: Stories.PeerState? = stateView.value?.get(Stories.PeerState.self)
var hasUnseen = false var hasUnseen = false
var unseenCount = 0
if let peerState = peerState { if let peerState = peerState {
hasUnseen = peerState.maxReadId < lastEntry.id hasUnseen = peerState.maxReadId < lastEntry.id
for item in itemsView.items {
if item.id > peerState.maxReadId {
unseenCount += 1
}
}
} }
let item = EngineStorySubscriptions.Item( let item = EngineStorySubscriptions.Item(
peer: EnginePeer(peer), peer: EnginePeer(peer),
hasUnseen: hasUnseen, hasUnseen: hasUnseen,
storyCount: itemsView.items.count, storyCount: itemsView.items.count,
unseenCount: unseenCount,
lastTimestamp: lastEntry.timestamp lastTimestamp: lastEntry.timestamp
) )
@ -745,6 +762,13 @@ public extension TelegramEngine {
return false 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 != rhs.peer.isPremium {
if lhs.peer.isPremium { if lhs.peer.isPremium {
return true return true

View File

@ -60,8 +60,8 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?,
peerIds.removeAll(where: { $0 == updated.id }) peerIds.removeAll(where: { $0 == updated.id })
transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds)
} }
if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) {
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden)
if !peerIds.contains(updated.id) { if !peerIds.contains(updated.id) {
peerIds.append(updated.id) peerIds.append(updated.id)
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds)
@ -69,11 +69,11 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?,
} }
} else { } else {
if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { 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 }) peerIds.removeAll(where: { $0 == updated.id })
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) 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) var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered)
if !peerIds.contains(updated.id) { if !peerIds.contains(updated.id) {
peerIds.append(updated.id) peerIds.append(updated.id)

View File

@ -1256,24 +1256,25 @@ public struct PresentationResourcesChat {
context.clip() context.clip()
let color: UIColor let color: UIColor
let foregroundColor: UIColor
switch type { switch type {
case .incoming: 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: 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: 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.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size)) 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) 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) 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() UIGraphicsPopContext()

View File

@ -158,8 +158,12 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
} else { } else {
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor
if let _ = storyData { if let storyData = storyData {
completeSourceString = strings.Message_ForwardedStoryShort(peerString) if storyData.isExpired {
completeSourceString = strings.Message_ForwardedExpiredStoryShort(peerString)
} else {
completeSourceString = strings.Message_ForwardedStoryShort(peerString)
}
} else { } else {
completeSourceString = strings.Message_ForwardedMessageShort(peerString) completeSourceString = strings.Message_ForwardedMessageShort(peerString)
} }

View File

@ -979,7 +979,8 @@ final class MediaEditorScreenComponent: Component {
timeoutSelected: timeoutSelected, timeoutSelected: timeoutSelected,
displayGradient: false, displayGradient: false,
bottomInset: 0.0, bottomInset: 0.0,
hideKeyboard: self.currentInputMode == .emoji hideKeyboard: self.currentInputMode == .emoji,
disabledPlaceholder: nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)

View File

@ -276,7 +276,8 @@ final class StoryPreviewComponent: Component {
timeoutSelected: false, timeoutSelected: false,
displayGradient: false, displayGradient: false,
bottomInset: 0.0, bottomInset: 0.0,
hideKeyboard: false hideKeyboard: false,
disabledPlaceholder: nil
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0) containerSize: CGSize(width: availableSize.width, height: 200.0)

View File

@ -67,6 +67,7 @@ public final class MessageInputPanelComponent: Component {
public let displayGradient: Bool public let displayGradient: Bool
public let bottomInset: CGFloat public let bottomInset: CGFloat
public let hideKeyboard: Bool public let hideKeyboard: Bool
public let disabledPlaceholder: String?
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -99,7 +100,8 @@ public final class MessageInputPanelComponent: Component {
timeoutSelected: Bool, timeoutSelected: Bool,
displayGradient: Bool, displayGradient: Bool,
bottomInset: CGFloat, bottomInset: CGFloat,
hideKeyboard: Bool hideKeyboard: Bool,
disabledPlaceholder: String?
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -132,6 +134,7 @@ public final class MessageInputPanelComponent: Component {
self.displayGradient = displayGradient self.displayGradient = displayGradient
self.bottomInset = bottomInset self.bottomInset = bottomInset
self.hideKeyboard = hideKeyboard self.hideKeyboard = hideKeyboard
self.disabledPlaceholder = disabledPlaceholder
} }
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@ -198,6 +201,9 @@ public final class MessageInputPanelComponent: Component {
if lhs.hideKeyboard != rhs.hideKeyboard { if lhs.hideKeyboard != rhs.hideKeyboard {
return false return false
} }
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
return false
}
return true return true
} }
@ -214,6 +220,7 @@ public final class MessageInputPanelComponent: Component {
private let placeholder = ComponentView<Empty>() private let placeholder = ComponentView<Empty>()
private let vibrancyPlaceholder = ComponentView<Empty>() private let vibrancyPlaceholder = ComponentView<Empty>()
private var disabledPlaceholder: ComponentView<Empty>?
private let textField = ComponentView<Empty>() private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState() private let textFieldExternalState = TextFieldComponent.ExternalState()
@ -488,10 +495,12 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: placeholderView, position: placeholderFrame.origin) transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
transition.setAlpha(view: placeholderView, 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) ? 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) let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
if let textFieldView = self.textField.view { if let textFieldView = self.textField.view {
@ -499,7 +508,38 @@ public final class MessageInputPanelComponent: Component {
self.addSubview(textFieldView) self.addSubview(textFieldView)
} }
transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)) 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<Empty>
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 { if component.attachmentAction != nil {
@ -833,7 +873,7 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center) transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size)) 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) transition.setScale(view: stickerButtonView, scale: (hasMediaRecording || hasMediaEditing || !inputModeVisible) ? 0.1 : 1.0)
if inputModeVisible { if inputModeVisible {

View File

@ -466,7 +466,7 @@ public func threadNotificationExceptionsScreen(context: AccountContext, peerId:
let canRemove = true let canRemove = true
let defaultSound: PeerMessageSound = globalSettings.groupChats.sound._asMessageSound() 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) let _ = (updateThreadSound(item.threadId, sound)
|> deliverOnMainQueue).start(next: { _ in |> deliverOnMainQueue).start(next: { _ in
updateState { value in updateState { value in

View File

@ -46,6 +46,7 @@ public enum NotificationExceptionMode : Equatable {
case users case users
case groups case groups
case channels case channels
case stories
} }
public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool { public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool {
@ -68,6 +69,12 @@ public enum NotificationExceptionMode : Equatable {
} else { } else {
return false 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 return .groups
case .channels: case .channels:
return .channels return .channels
case .stories:
return .stories
} }
} }
public var isEmpty: Bool { public var isEmpty: Bool {
switch self { 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 return value.isEmpty
} }
} }
@ -92,6 +101,7 @@ public enum NotificationExceptionMode : Equatable {
case users([EnginePeer.Id : NotificationExceptionWrapper]) case users([EnginePeer.Id : NotificationExceptionWrapper])
case groups([EnginePeer.Id : NotificationExceptionWrapper]) case groups([EnginePeer.Id : NotificationExceptionWrapper])
case channels([EnginePeer.Id : NotificationExceptionWrapper]) case channels([EnginePeer.Id : NotificationExceptionWrapper])
case stories([EnginePeer.Id : NotificationExceptionWrapper])
public func withUpdatedPeerSound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionMode { public func withUpdatedPeerSound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionMode {
let apply:([EnginePeer.Id : NotificationExceptionWrapper], EnginePeer.Id, PeerMessageSound) -> [EnginePeer.Id : NotificationExceptionWrapper] = { values, peerId, sound in 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 { switch self {
case let .groups(values): case let .groups(values):
return .groups(apply(values, peer.id, sound)) return .groups(apply(values, peer.id, sound))
case let .users(values): case let .users(values):
return .users(apply(values, peer.id, sound)) return .users(apply(values, peer.id, sound))
case let .channels(values): case let .channels(values):
return .channels(apply(values, peer.id, sound)) return .channels(apply(values, peer.id, sound))
case .stories:
return self
} }
} }
@ -172,12 +184,14 @@ public enum NotificationExceptionMode : Equatable {
muteState = .default muteState = .default
} }
switch self { switch self {
case let .groups(values): case let .groups(values):
return .groups(apply(values, peer.id, muteState)) return .groups(apply(values, peer.id, muteState))
case let .users(values): case let .users(values):
return .users(apply(values, peer.id, muteState)) return .users(apply(values, peer.id, muteState))
case let .channels(values): case let .channels(values):
return .channels(apply(values, peer.id, muteState)) return .channels(apply(values, peer.id, muteState))
case .stories:
return self
} }
} }
@ -208,12 +222,14 @@ public enum NotificationExceptionMode : Equatable {
} }
switch self { switch self {
case let .groups(values): case let .groups(values):
return .groups(apply(values, peer.id, displayPreviews)) return .groups(apply(values, peer.id, displayPreviews))
case let .users(values): case let .users(values):
return .users(apply(values, peer.id, displayPreviews)) return .users(apply(values, peer.id, displayPreviews))
case let .channels(values): case let .channels(values):
return .channels(apply(values, peer.id, displayPreviews)) return .channels(apply(values, peer.id, displayPreviews))
case .stories:
return self
} }
} }
@ -254,25 +270,23 @@ public enum NotificationExceptionMode : Equatable {
} }
switch self { switch self {
case let .groups(values): case let .stories(values):
return .groups(apply(values, peer.id, storyNotifications)) return .stories(apply(values, peer.id, storyNotifications))
case let .users(values): default:
return .users(apply(values, peer.id, storyNotifications)) return self
case let .channels(values):
return .channels(apply(values, peer.id, storyNotifications))
} }
} }
public var peerIds: [EnginePeer.Id] { public var peerIds: [EnginePeer.Id] {
switch self { 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.map {$0.key} return settings.map { $0.key }
} }
} }
public var settings: [EnginePeer.Id : NotificationExceptionWrapper] { public var settings: [EnginePeer.Id : NotificationExceptionWrapper] {
switch self { 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 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) let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)
var entries: [NotificationPeerExceptionEntry] = [] var entries: [NotificationPeerExceptionEntry] = []
@ -547,23 +561,26 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
index += 1 index += 1
} }
entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader)) if isStories == nil || isStories == false {
index += 1 entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader))
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 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 { if case .user = peer {
//TODO:localize //TODO:localize
entries.append(.storyNotificationsHeader(index: index, theme: presentationData.theme, title: "STORY NOTIFICATIONS")) 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)) entries.append(.storyNotifications(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.storyNotifications == .alwaysOff))
index += 1 index += 1
} }
}
if isStories == nil || isStories == false {
entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones)) entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones))
index += 1 index += 1
@ -674,7 +693,7 @@ private struct NotificationExceptionPeerState : Equatable {
} }
} }
public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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 initialState = NotificationExceptionPeerState(canRemove: false)
let statePromise = Promise(initialState) let statePromise = Promise(initialState)
let stateValue = Atomic(value: 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 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)) return (controllerState, (listState, arguments))
} }

View File

@ -1294,7 +1294,8 @@ public final class StoryItemSetContainerComponent: Component {
timeoutSelected: false, timeoutSelected: false,
displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular, displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular,
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset, 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: {}, environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)

View File

@ -433,6 +433,7 @@ public final class StoryContentContextImpl: StoryContentContext {
peer: peer, peer: peer,
hasUnseen: state.hasUnseen, hasUnseen: state.hasUnseen,
storyCount: state.items.count, storyCount: state.items.count,
unseenCount: 0,
lastTimestamp: state.items.last?.timestamp ?? 0 lastTimestamp: state.items.last?.timestamp ?? 0
)], )],
hasMoreToken: nil hasMoreToken: nil

View File

@ -4514,6 +4514,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
if let story = message.associatedStories[storyId], story.data.isEmpty { 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 return
} }
@ -4696,7 +4698,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode self.avatarNode = avatarNode
avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme) //avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme)
case .feed: case .feed:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
} }

View File

@ -3743,6 +3743,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} else { } else {
return .optionalAction(performAction) 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 { loop: for contentNode in self.contentNodes {

View File

@ -60,7 +60,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.displayCallIcons = displayCallIcons self.displayCallIcons = displayCallIcons
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? 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) contextActionImpl?(peer, node, gesture, nil)
} : nil, multipleSelection: multipleSelection) } : nil, multipleSelection: multipleSelection)

View File

@ -4724,7 +4724,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let canRemove = false 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) let _ = (updatePeerSound(peer.id, sound)
|> deliverOnMainQueue).start(next: { _ in |> deliverOnMainQueue).start(next: { _ in
}) })

View File

@ -88,12 +88,14 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable {
public var photo: MediaAutoDownloadCategory public var photo: MediaAutoDownloadCategory
public var video: MediaAutoDownloadCategory public var video: MediaAutoDownloadCategory
public var file: 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.basePreset = basePreset
self.photo = photo self.photo = photo
self.video = video self.video = video
self.file = file self.file = file
self.stories = stories
} }
public init(from decoder: Decoder) throws { 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.photo = try container.decode(MediaAutoDownloadCategory.self, forKey: "photo")
self.video = try container.decode(MediaAutoDownloadCategory.self, forKey: "video") self.video = try container.decode(MediaAutoDownloadCategory.self, forKey: "video")
self.file = try container.decode(MediaAutoDownloadCategory.self, forKey: "file") 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 { 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.photo, forKey: "photo")
try container.encode(self.video, forKey: "video") try container.encode(self.video, forKey: "video")
try container.encode(self.file, forKey: "file") try container.encode(self.file, forKey: "file")
try container.encode(self.stories, forKey: "stories")
} }
public static func < (lhs: MediaAutoDownloadCategories, rhs: MediaAutoDownloadCategories) -> Bool { public static func < (lhs: MediaAutoDownloadCategories, rhs: MediaAutoDownloadCategories) -> Bool {
@ -370,15 +374,29 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
public static var defaultSettings: MediaAutoDownloadSettings { public static var defaultSettings: MediaAutoDownloadSettings {
let mb: Int64 = 1024 * 1024 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), let presets = MediaAutoDownloadPresets(low:
video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false), MediaAutoDownloadCategories(
file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false)), basePreset: .low,
medium: MediaAutoDownloadCategories(basePreset: .medium, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false), 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), video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false),
file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false)), file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false),
high: MediaAutoDownloadCategories(basePreset: .high, photo: 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)
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))) 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) 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 fileEnabled = autodownloadPreset.fileSizeMax > 0
let fileSizeMax = autodownloadPreset.fileSizeMax > 0 ? autodownloadPreset.fileSizeMax : 1 * 1024 * 1024 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 { 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 { guard let media = media else {
return (categories.photo, 0) return (categories.photo, 0)
} }
@ -524,7 +552,7 @@ public func isAutodownloadEnabledForAnyPeerType(category: MediaAutoDownloadCateg
return category.contacts || category.otherPrivate || category.groups || category.channels return category.contacts || category.otherPrivate || category.groups || category.channels
} }
public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set<PeerId> = Set(), media: Media?) -> Bool { public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set<PeerId> = Set(), media: Media?, isStory: Bool = false) -> Bool {
if (networkType == .cellular && !settings.cellular.enabled) || (networkType == .wifi && !settings.wifi.enabled) { if (networkType == .cellular && !settings.cellular.enabled) || (networkType == .wifi && !settings.wifi.enabled) {
return false return false
} }
@ -537,7 +565,7 @@ public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings
peerType = .contact 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 { if let size = size {
var sizeLimit = category.sizeLimit var sizeLimit = category.sizeLimit
if let file = media as? TelegramMediaFile, file.isVoice { if let file = media as? TelegramMediaFile, file.isVoice {
@ -564,7 +592,7 @@ public func shouldPredownloadMedia(settings: MediaAutoDownloadSettings, peerType
return false 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 { guard isAutodownloadEnabledForPeerType(peerType, category: category) else {
return false return false
} }

View File

@ -464,6 +464,12 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
} else { } else {
return nil 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" { } else if pathComponents.count == 4 && pathComponents[0] == "c" {
if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]) { if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]) {
var timecode: Double? var timecode: Double?