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";
"Message.ForwardedStoryShort" = "Forwarded Story\nFrom: %@";
"Message.ForwardedExpiredStoryShort" = "Expired Story\nFrom: %@";
"Conversation.StoryForwardTooltip.Chat.One" = "Story forwarded to **%@**";
"Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**";

View File

@ -746,7 +746,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
let canRemove = false
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peerId, sound)
|> deliverOnMainQueue).start(next: { _ in
})

View File

@ -1754,7 +1754,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
var autodownloadEnabled = true
if !shouldDownloadMediaAutomatically(settings: automaticMediaDownloadSettings, peerType: .contact, networkType: automaticDownloadNetworkType, authorPeerId: nil, contactsPeerIds: [], media: nil) {
if !shouldDownloadMediaAutomatically(settings: automaticMediaDownloadSettings, peerType: .contact, networkType: automaticDownloadNetworkType, authorPeerId: nil, contactsPeerIds: [], media: nil, isStory: true) {
autodownloadEnabled = false
}
@ -2409,6 +2409,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
if peer.id == self.context.account.peerId {
if let storySubscriptions = self.storySubscriptions {
var openCamera = false
if let accountItem = storySubscriptions.accountItem {
openCamera = accountItem.storyCount == 0
} else {
openCamera = true
}
if openCamera {
self.openStoryCamera()
return
}
}
}
let storyContent = StoryContentContextImpl(context: self.context, isHidden: false, focusedPeerId: peer.id, singlePeer: false)
let _ = (storyContent.state
|> take(1)

View File

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

View File

@ -10,7 +10,7 @@ import PresentationDataUtils
import OverlayStatusController
import LocalizedPeerData
func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?) -> Signal<[ContextMenuItem], NoError> {
func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, contactsController: ContactsController?, isStories: Bool) -> Signal<[ContextMenuItem], NoError> {
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
return context.engine.data.get(
@ -21,6 +21,17 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
|> map { [weak contactsController] peer, areVoiceCallsAvailable, areVideoCallsAvailable -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
if isStories {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Unhide", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
context.engine.peers.updatePeerStoriesHidden(id: peerId, isHidden: false)
})))
}
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_SendMessage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)

View File

@ -38,61 +38,18 @@ private let dropDownIcon = { () -> UIImage in
return image
}()
private enum ContactListNodeEntrySection: Int {
case stories = 0
case contacts = 1
}
private enum ContactListNodeEntryId: Hashable {
case search
case sort
case permission(action: Bool)
case option(index: Int)
case peerId(Int64)
case peerId(peerId: Int64, section: ContactListNodeEntrySection)
case deviceContact(DeviceContactStableId)
static func <(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool {
return lhs.hashValue < rhs.hashValue
}
static func ==(lhs: ContactListNodeEntryId, rhs: ContactListNodeEntryId) -> Bool {
switch lhs {
case .search:
switch rhs {
case .search:
return true
default:
return false
}
case .sort:
switch rhs {
case .sort:
return true
default:
return false
}
case let .permission(action):
if case .permission(action) = rhs {
return true
} else {
return false
}
case let .option(index):
if case .option(index) = rhs {
return true
} else {
return false
}
case let .peerId(lhsId):
switch rhs {
case let .peerId(rhsId):
return lhsId == rhsId
default:
return false
}
case let .deviceContact(id):
if case .deviceContact(id) = rhs {
return true
} else {
return false
}
}
}
}
private final class ContactListNodeInteraction {
@ -100,16 +57,18 @@ private final class ContactListNodeInteraction {
fileprivate let authorize: () -> Void
fileprivate let suppressWarning: () -> Void
fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void
fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?
fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void
let itemHighlighting = ContactItemHighlighting()
init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?) {
init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void) {
self.activateSearch = activateSearch
self.authorize = authorize
self.suppressWarning = suppressWarning
self.openPeer = openPeer
self.contextAction = contextAction
self.openStories = openStories
}
}
@ -120,12 +79,17 @@ enum ContactListAnimation {
}
private enum ContactListNodeEntry: Comparable, Identifiable {
struct StoryData: Equatable {
var count: Int
var unseenCount: Int
}
case search(PresentationTheme, PresentationStrings)
case sort(PresentationTheme, PresentationStrings, ContactsSortOrder)
case permissionInfo(PresentationTheme, String, String, Bool)
case permissionEnable(PresentationTheme, String)
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings)
case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool)
case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, StoryData?)
var stableId: ContactListNodeEntryId {
switch self {
@ -139,10 +103,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return .permission(action: true)
case let .option(index, _, _, _, _):
return .option(index: index)
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _):
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, storyData):
switch peer {
case let .peer(peer, _, _):
return .peerId(peer.id.toInt64())
return .peerId(peerId: peer.id.toInt64(), section: storyData != nil ? .stories : .contacts)
case let .deviceContact(id, _):
return .deviceContact(id)
}
@ -172,7 +136,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
})
case let .option(_, option, header, _, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action)
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled, storyData):
var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer
var isContextActionEnabled = false
@ -218,7 +182,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
switch itemPeer {
case let .peer(peer, _):
if let peer = peer {
contextAction(peer, node, gesture, location)
contextAction(peer, node, gesture, location, storyData != nil)
}
case .deviceContact:
break
@ -237,9 +201,34 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
})]
}
var hasUnseenStories: Bool?
if let storyData = storyData {
let text: String
//TODO:localize
if storyData.unseenCount != 0 {
if storyData.unseenCount == 1 {
text = "1 unseen story"
} else {
text = "\(storyData.unseenCount) unseen stories"
}
} else {
if storyData.count == 1 {
text = "1 story"
} else {
text = "\(storyData.count) stories"
}
}
status = .custom(string: text, multiline: false, isActive: false, icon: nil)
hasUnseenStories = storyData.unseenCount != 0
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
interaction.openPeer(peer, .generic)
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction)
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, hasUnseenStories: hasUnseenStories, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue {
interaction.openStories(peerValue, sourceNode)
}
})
}
}
@ -275,9 +264,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled):
case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled, lhsStoryData):
switch rhs {
case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled):
case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled, rhsStoryData):
if lhsIndex != rhsIndex {
return false
}
@ -318,6 +307,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
if lhsEnabled != rhsEnabled {
return false
}
if lhsStoryData != rhsStoryData {
return false
}
return true
default:
return false
@ -359,18 +351,25 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
case .peer:
return true
}
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _):
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData):
switch rhs {
case .search, .sort, .permissionInfo, .permissionEnable, .option:
return false
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _):
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData):
if (lhsStoryData == nil) != (rhsStoryData == nil) {
if lhsStoryData != nil {
return true
} else {
return false
}
}
return lhsIndex < rhsIndex
}
}
}
}
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<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 commonHeader: ListViewItemHeader?
@ -398,8 +397,13 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
}
}
if let storySubscriptions, !storySubscriptions.items.isEmpty {
addHeader = true
}
if addHeader {
commonHeader = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
//TODO:localize
commonHeader = ChatListSearchItemHeader(type: .text("SORTED BY LAST SEEN TIME", AnyHashable(1)), theme: theme, strings: strings, actionTitle: nil, action: nil)
}
switch presentation {
@ -522,6 +526,18 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
}
if let storySubscriptions {
var index: Int = 0
//TODO:localize
let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text("HIDDEN STORIES", AnyHashable(0)), theme: theme, strings: strings)
for item in storySubscriptions.items {
entries.append(.peer(index, .peer(peer: item.peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount)))
index += 1
}
}
var index: Int = 0
var existingPeerIds = Set<ContactListPeerId>()
@ -546,7 +562,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
enabled = true
}
entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil))
index += 1
}
}
@ -582,7 +598,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
default:
enabled = true
}
entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil))
index += 1
}
return entries
@ -606,7 +622,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta
case .search:
//indexSections.apend(CollectionIndexNode.searchIndex)
break
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _):
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _):
if let header = header as? ContactListNameIndexHeader {
if !existingSections.contains(header.letter) {
existingSections.insert(header.letter)
@ -727,6 +743,7 @@ public final class ContactListNode: ASDisplayNode {
private var didSetReady = false
private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?)>()
let storySubscriptions = Promise<EngineStorySubscriptions?>(nil)
private let selectionStatePromise = Promise<ContactListNodeGroupSelectionState?>(nil)
private var selectionStateValue: ContactListNodeGroupSelectionState? {
@ -803,7 +820,8 @@ public final class ContactListNode: ASDisplayNode {
public var openPeer: ((ContactListPeer, ContactListAction) -> Void)?
public var openPrivacyPolicy: (() -> Void)?
public var suppressPermissionWarning: (() -> Void)?
private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?
public var openStories: ((EnginePeer, ASDisplayNode) -> Void)?
private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
private let disposable = MetaDisposable()
@ -819,7 +837,7 @@ public final class ContactListNode: ASDisplayNode {
private let isPeerEnabled: ((EnginePeer) -> Bool)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<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.filters = filters
self.displayPermissionPlaceholder = displayPermissionPlaceholder
@ -915,7 +933,12 @@ public final class ContactListNode: ASDisplayNode {
strongSelf.openPeer?(peer, action)
}
}
}, contextAction: contextAction)
}, contextAction: contextAction, openStories: { [weak self] peer, sourceNode in
guard let self else {
return
}
self.openStories?(peer, sourceNode)
})
self.indexNode.indexSelected = { [weak self] section in
guard let strongSelf = self, let layout = strongSelf.validLayout, let entries = previousEntries.with({ $0 }) else {
@ -942,7 +965,7 @@ public final class ContactListNode: ASDisplayNode {
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
break loop
}
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _):
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _):
if let header = header as? ContactListNameIndexHeader {
if let scalar = UnicodeScalar(header.letter) {
let title = "\(Character(scalar))"
@ -1190,7 +1213,7 @@ public final class ContactListNode: ASDisplayNode {
peers.append(.deviceContact(stableId, contact.0))
}
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons)
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil)
let previous = previousEntries.swap(entries)
return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch))
}
@ -1250,8 +1273,8 @@ public final class ContactListNode: ASDisplayNode {
chatListSignal = .single([])
}
return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get())
|> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed -> Signal<ContactsListNodeTransition, NoError> in
return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get())
|> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal<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) })
for (peer, memberCount) in chatListPeers {
@ -1283,7 +1306,7 @@ public final class ContactListNode: ASDisplayNode {
if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty {
isEmpty = true
}
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons)
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions)
let previous = previousEntries.swap(entries)
let previousSelection = previousSelectionState.swap(selectionState)

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

View File

@ -63,6 +63,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
var openPeopleNearby: (() -> Void)?
var openInvite: (() -> Void)?
var openQrScan: (() -> Void)?
var openStories: ((EnginePeer, ASDisplayNode) -> Void)?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
@ -113,10 +114,10 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
var contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
var contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?
self.contactListNode = ContactListNode(context: context, presentation: presentation, displaySortOptions: true, contextAction: { peer, node, gesture, location in
contextAction?(peer, node, gesture, location)
self.contactListNode = ContactListNode(context: context, presentation: presentation, displaySortOptions: true, contextAction: { peer, node, gesture, location, isStories in
contextAction?(peer, node, gesture, location, isStories)
})
super.init()
@ -159,8 +160,8 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
contextAction = { [weak self] peer, node, gesture, location in
self?.contextAction(peer: peer, node: node, gesture: gesture, location: location)
contextAction = { [weak self] peer, node, gesture, location, isStories in
self?.contextAction(peer: peer, node: node, gesture: gesture, location: location, isStories: isStories)
}
self.contactListNode.contentOffsetChanged = { [weak self] offset in
@ -238,30 +239,19 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return
}
var wasEmpty = true
if let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.isEmpty {
wasEmpty = false
}
self.storySubscriptions = storySubscriptions
let isEmpty = storySubscriptions.items.isEmpty
let transition: ContainedViewLayoutTransition
if self.didAppear {
transition = .animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
let _ = wasEmpty
let _ = isEmpty
//self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
self.controller?.requestLayout(transition: transition)
//self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
self.contactListNode.storySubscriptions.set(.single(storySubscriptions))
self.storiesReady.set(.single(true))
})
self.contactListNode.openStories = { [weak self] peer, sourceNode in
guard let self else {
return
}
self.openStories?(peer, sourceNode)
}
self.isPremiumDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> map {
return $0?.isPremium ?? false
@ -419,7 +409,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
primaryContent: primaryContent,
secondaryContent: nil,
secondaryTransition: 0.0,
storySubscriptions: self.storySubscriptions,
storySubscriptions: nil,
storiesIncludeHidden: true,
uploadProgress: nil,
tabsNode: tabsNode,
@ -505,13 +495,13 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
private func contextAction(peer: EnginePeer, node: ASDisplayNode?, gesture: ContextGesture?, location: CGPoint?) {
private func contextAction(peer: EnginePeer, node: ASDisplayNode?, gesture: ContextGesture?, location: CGPoint?, isStories: Bool) {
guard let contactsController = self.controller else {
return
}
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController, isStories: isStories) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
contactsController.presentInGlobalOverlay(contextController)
}
@ -532,7 +522,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
requestOpenPeerFromSearch(peer)
}
}, contextAction: { [weak self] peer, node, gesture, location in
self?.contextAction(peer: peer, node: node, gesture: gesture, location: location)
self?.contextAction(peer: peer, node: node, gesture: gesture, location: location, isStories: false)
}), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()

View File

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

View File

@ -20,6 +20,7 @@ import ComponentFlow
import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
import AvatarStoryIndicatorComponent
public final class ContactItemHighlighting {
public var chatLocation: ChatLocation?
@ -179,6 +180,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let arrowAction: (() -> Void)?
let animationCache: AnimationCache?
let animationRenderer: MultiAnimationRenderer?
let hasUnseenStories: Bool?
let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)?
public let selectable: Bool
@ -213,7 +216,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
itemHighlighting: ContactItemHighlighting? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil,
animationCache: AnimationCache? = nil,
animationRenderer: MultiAnimationRenderer? = nil
animationRenderer: MultiAnimationRenderer? = nil,
hasUnseenStories: Bool? = nil,
openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil
) {
self.presentationData = presentationData
self.style = style
@ -243,6 +248,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.arrowAction = arrowAction
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.hasUnseenStories = hasUnseenStories
self.openStories = openStories
if let index = index {
var letter: String = "#"
@ -391,8 +398,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode
private let avatarNode: AvatarNode
private let avatarNodeContainer: ASDisplayNode
public let avatarNode: AvatarNode
private var avatarStoryIndicator: ComponentView<Empty>?
private var avatarIconView: ComponentHostView<Empty>?
private var avatarIconComponent: EmojiStatusComponent?
private let titleNode: TextNode
@ -493,8 +501,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.offsetContainerNode = ASDisplayNode()
self.avatarNodeContainer = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
self.avatarNode.isLayerBacked = false
self.titleNode = TextNode()
self.statusNode = TextNode()
@ -515,7 +524,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.avatarNode)
self.avatarNodeContainer.addSubnode(self.avatarNode)
self.offsetContainerNode.addSubnode(self.avatarNodeContainer)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.statusNode)
@ -1069,7 +1079,66 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(), size: avatarFrame.size)
transition.updatePosition(node: strongSelf.avatarNodeContainer, position: avatarFrame.center)
transition.updateBounds(node: strongSelf.avatarNodeContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
var avatarScale: CGFloat = 1.0
if item.hasUnseenStories != nil {
avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width
}
transition.updateTransformScale(node: strongSelf.avatarNodeContainer, scale: CGPoint(x: avatarScale, y: avatarScale))
let storyIndicatorScale: CGFloat = 1.0
if let displayStoryIndicator = item.hasUnseenStories {
var indicatorTransition = Transition(transition)
let avatarStoryIndicator: ComponentView<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 {
let animationCache = item.context.animationCache
@ -1109,12 +1178,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
)
transition.updateFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 43.0, y: floor((nodeLayout.contentSize.height - iconSize.height) / 2.0)), size: iconSize))
strongSelf.avatarNode.isHidden = true
strongSelf.avatarNodeContainer.isHidden = true
} else if let avatarIconView = strongSelf.avatarIconView {
strongSelf.avatarIconView = nil
avatarIconView.removeFromSuperview()
strongSelf.avatarNode.isHidden = false
strongSelf.avatarNodeContainer.isHidden = false
}
let _ = titleApply()
@ -1471,6 +1540,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let avatarStoryIndicatorView = self.avatarStoryIndicator?.view, let result = avatarStoryIndicatorView.hitTest(self.view.convert(point, to: avatarStoryIndicatorView), with: event) {
return result
}
return super.hitTest(point, with: event)
}
override public func headers() -> [ListViewItemHeader]? {
if let (item, _, _, _, _, _) = self.layoutParams {
return item.header.flatMap { [$0] }
@ -1484,4 +1561,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
item.arrowAction?()
}
}
@objc private func avatarStoryTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let (item, _, _, _, _, _) = self.layoutParams {
item.openStories?(item.peer, self)
}
}
}
}

View File

@ -47,6 +47,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
case dataUsageItem(PresentationTheme, PresentationStrings, AutomaticDownloadDataUsage, Int?, Bool)
case typesHeader(PresentationTheme, String)
case photos(PresentationTheme, String, String, Bool)
case stories(PresentationTheme, String, String, Bool)
case videos(PresentationTheme, String, String, Bool)
case files(PresentationTheme, String, String, Bool)
case voiceMessagesInfo(PresentationTheme, String)
@ -57,7 +58,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
return AutodownloadMediaCategorySection.master.rawValue
case .dataUsageHeader, .dataUsageItem:
return AutodownloadMediaCategorySection.dataUsage.rawValue
case .typesHeader, .photos, .videos, .files, .voiceMessagesInfo:
case .typesHeader, .photos, .stories, .videos, .files, .voiceMessagesInfo:
return AutodownloadMediaCategorySection.types.rawValue
}
}
@ -74,12 +75,14 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
return 3
case .photos:
return 4
case .videos:
case .stories:
return 5
case .files:
case .videos:
return 6
case .voiceMessagesInfo:
case .files:
return 7
case .voiceMessagesInfo:
return 8
}
}
@ -115,6 +118,12 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
} else {
return false
}
case let .stories(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .stories(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .videos(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .videos(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
@ -159,6 +168,10 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Photos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.photo)
})
case let .stories(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Stories")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.story)
})
case let .videos(_, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Videos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: {
arguments.customize(.video)
@ -190,6 +203,17 @@ private struct AutomaticDownloadPeers {
}
private func stringForAutomaticDownloadPeers(strings: PresentationStrings, decimalSeparator: String, peers: AutomaticDownloadPeers, category: AutomaticDownloadCategory) -> String {
if case .story = category {
if peers.contacts && peers.otherPrivate {
return strings.AutoDownloadSettings_OnForAll
} else if peers.contacts {
//TODO:localize
return "On for contacts"
} else {
return strings.AutoDownloadSettings_OffForAll
}
}
var size: String?
if var peersSize = peers.size, category == .video || category == .file {
if peersSize == Int32.max {
@ -253,6 +277,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
let photo = AutomaticDownloadPeers(category: categories.photo)
let video = AutomaticDownloadPeers(category: categories.video)
let file = AutomaticDownloadPeers(category: categories.file)
let stories = AutomaticDownloadPeers(category: categories.stories)
entries.append(.master(presentationData.theme, presentationData.strings.AutoDownloadSettings_AutoDownload, master))
@ -268,6 +293,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
entries.append(.typesHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_MediaTypes))
entries.append(.photos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Photos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: photo, category: .photo), master))
entries.append(.stories(presentationData.theme, "Stories", stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: stories, category: .story), master))
entries.append(.videos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Videos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: video, category: .video), master))
entries.append(.files(presentationData.theme, presentationData.strings.AutoDownloadSettings_Files, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: file, category: .file), master))
entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo))

View File

@ -49,6 +49,7 @@ enum AutomaticDownloadCategory {
case photo
case video
case file
case story
}
private enum AutomaticDownloadPeerType {
@ -243,38 +244,54 @@ private func autodownloadMediaCategoryControllerEntries(presentationData: Presen
let predownload: Bool
switch category {
case .photo:
peers = AutomaticDownloadPeers(category: categories.photo)
size = categories.photo.sizeLimit
predownload = categories.photo.predownload
case .video:
peers = AutomaticDownloadPeers(category: categories.video)
size = categories.video.sizeLimit
predownload = categories.video.predownload
case .file:
peers = AutomaticDownloadPeers(category: categories.file)
size = categories.file.sizeLimit
predownload = categories.file.predownload
case .photo:
peers = AutomaticDownloadPeers(category: categories.photo)
size = categories.photo.sizeLimit
predownload = categories.photo.predownload
case .video:
peers = AutomaticDownloadPeers(category: categories.video)
size = categories.video.sizeLimit
predownload = categories.video.predownload
case .file:
peers = AutomaticDownloadPeers(category: categories.file)
size = categories.file.sizeLimit
predownload = categories.file.predownload
case .story:
peers = AutomaticDownloadPeers(category: categories.stories)
size = categories.stories.sizeLimit
predownload = categories.stories.predownload
}
let downloadTitle: String
var sizeTitle: String?
switch category {
case .photo:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos
case .video:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize
case .file:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
case .photo:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadPhotos
case .video:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadVideos
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxVideoSize
case .file:
downloadTitle = presentationData.strings.AutoDownloadSettings_AutodownloadFiles
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
case .story:
//TODO:localize
downloadTitle = "AUTO-DOWNLOAD STORIES"
sizeTitle = presentationData.strings.AutoDownloadSettings_MaxFileSize
}
entries.append(.peerHeader(presentationData.theme, downloadTitle))
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate))
entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups))
entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels))
if case .story = category {
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
//TODO:localize
if peers.contacts {
entries.append(.peerOtherPrivate(presentationData.theme, "Hidden Contacts", peers.otherPrivate))
}
} else {
entries.append(.peerHeader(presentationData.theme, downloadTitle))
entries.append(.peerContacts(presentationData.theme, presentationData.strings.AutoDownloadSettings_Contacts, peers.contacts))
entries.append(.peerOtherPrivate(presentationData.theme, presentationData.strings.AutoDownloadSettings_PrivateChats, peers.otherPrivate))
entries.append(.peerGroups(presentationData.theme, presentationData.strings.AutoDownloadSettings_GroupChats, peers.groups))
entries.append(.peerChannels(presentationData.theme, presentationData.strings.AutoDownloadSettings_Channels, peers.channels))
}
switch category {
case .video, .file:
@ -339,7 +356,18 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.file.groups = !categories.file.groups
case .channel:
categories.file.channels = !categories.file.channels
}
}
case .story:
switch type {
case .contact:
categories.stories.contacts = !categories.stories.contacts
case .otherPrivate:
categories.stories.otherPrivate = !categories.stories.otherPrivate
case .group:
categories.stories.groups = !categories.stories.groups
case .channel:
categories.stories.channels = !categories.stories.channels
}
}
switch connectionType {
case .cellular:
@ -362,6 +390,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.video.sizeLimit = size
case .file:
categories.file.sizeLimit = size
case .story:
categories.stories.sizeLimit = size
}
switch connectionType {
case .cellular:
@ -384,6 +414,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
categories.video.predownload = !categories.video.predownload
case .file:
categories.file.predownload = !categories.file.predownload
case .story:
categories.stories.predownload = !categories.stories.predownload
}
switch connectionType {
case .cellular:
@ -437,10 +469,13 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType
title = presentationData.strings.AutoDownloadSettings_VideosTitle
case .file:
title = presentationData.strings.AutoDownloadSettings_DocumentsTitle
case .story:
//TODO:localize
title = "Stories"
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true)
return (controllerState, (listState, arguments))
}

View File

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

View File

@ -144,6 +144,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
case privateChats(PresentationTheme, String, String, String)
case groupChats(PresentationTheme, String, String, String)
case channels(PresentationTheme, String, String, String)
case stories(PresentationTheme, String, String, String)
case inAppHeader(PresentationTheme, String)
case inAppSounds(PresentationTheme, String, Bool)
@ -170,7 +171,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return NotificationsAndSoundsSection.accounts.rawValue
case .permissionInfo, .permissionEnable:
return NotificationsAndSoundsSection.permission.rawValue
case .categoriesHeader, .privateChats, .groupChats, .channels:
case .categoriesHeader, .privateChats, .groupChats, .channels, .stories:
return NotificationsAndSoundsSection.categories.rawValue
case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews:
return NotificationsAndSoundsSection.inApp.rawValue
@ -205,6 +206,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return 7
case .channels:
return 8
case .stories:
return 9
case .inAppHeader:
return 14
case .inAppSounds:
@ -317,6 +320,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
} else {
return false
}
case let .stories(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel):
if case let .stories(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .inAppHeader(lhsTheme, lhsText):
if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -441,6 +450,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry {
return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.channel)
})
case let .stories(_, title, subtitle, label):
return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Stories"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: {
arguments.openPeerCategory(.stories)
})
case let .inAppHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .inAppSounds(_, text, value):
@ -499,7 +512,7 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound
}
}
private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warningSuppressed: Bool, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode), presentationData: PresentationData, hasMoreThanOneAccount: Bool) -> [NotificationsAndSoundsEntry] {
private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warningSuppressed: Bool, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode), presentationData: PresentationData, hasMoreThanOneAccount: Bool) -> [NotificationsAndSoundsEntry] {
var entries: [NotificationsAndSoundsEntry] = []
if hasMoreThanOneAccount {
@ -538,6 +551,8 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn
entries.append(.privateChats(presentationData.theme, presentationData.strings.Notifications_PrivateChats, !exceptions.users.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.users.peerIds.count)) : "", globalSettings.privateChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.groupChats(presentationData.theme, presentationData.strings.Notifications_GroupChats, !exceptions.groups.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.groups.peerIds.count)) : "", globalSettings.groupChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.channels(presentationData.theme, presentationData.strings.Notifications_Channels, !exceptions.channels.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.channels.peerIds.count)) : "", globalSettings.channels.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
//TODO:localize
entries.append(.stories(presentationData.theme, "Stories", !exceptions.stories.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.stories.peerIds.count)) : "", globalSettings.privateChats.storiesMuted != false ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off))
entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased()))
entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds))
@ -567,9 +582,9 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)> = Promise()
let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)> = Promise()
let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)) -> Void = { value in
let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)) -> Void = { value in
notificationExceptions.set(.single(value))
}
@ -603,7 +618,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
context.sharedContext.applicationBindings.openSettings()
})]), nil)
}, openPeerCategory: { category in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in
let mode: NotificationExceptionMode
switch category {
case .privateChat:
@ -612,16 +627,20 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
mode = groups
case .channel:
mode = channels
case .stories:
mode = stories
}
pushControllerImpl?(notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { mode in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in
_ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in
switch mode {
case .users:
updateNotificationExceptions((mode, groups, channels))
updateNotificationExceptions((mode, groups, channels, stories))
case .groups:
updateNotificationExceptions((users, mode, channels))
updateNotificationExceptions((users, mode, channels, stories))
case .channels:
updateNotificationExceptions((users, groups, mode))
updateNotificationExceptions((users, groups, mode, stories))
case .stories:
updateNotificationExceptions((users, groups, channels, mode))
}
})
}, focusOnItemTag: nil))
@ -711,13 +730,18 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
let exceptionsSignal = Signal<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 groups: [PeerId : NotificationExceptionWrapper] = [:]
var channels:[PeerId : NotificationExceptionWrapper] = [:]
var channels: [PeerId : NotificationExceptionWrapper] = [:]
var stories: [PeerId : NotificationExceptionWrapper] = [:]
if let list = list {
for (key, value) in list.settings {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if value.storiesMuted != nil {
stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
}
switch value.muteState {
case .default:
switch value.messageSound {
@ -751,7 +775,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
}
}
return (.users(users), .groups(groups), .channels(channels))
return (.users(users), .groups(groups), .channels(channels), .stories(stories))
})
let notificationsWarningSuppressed = Promise<Bool>(true)

View File

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

View File

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

View File

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

View File

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

View File

@ -1256,24 +1256,25 @@ public struct PresentationResourcesChat {
context.clip()
let color: UIColor
let foregroundColor: UIColor
switch type {
case .incoming:
color = theme.chat.message.incoming.mediaPlaceholderColor
color = theme.chat.message.incoming.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.incoming.mediaActiveControlColor
case .outgoing:
color = theme.chat.message.outgoing.mediaPlaceholderColor
color = theme.chat.message.outgoing.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.outgoing.mediaActiveControlColor
case .free:
color = theme.chat.message.freeform.withWallpaper.fill[0]
color = theme.chat.message.freeform.withWallpaper.reactionActiveMediaPlaceholder
foregroundColor = theme.chat.message.freeform.withWallpaper.reactionActiveBackground
}
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: .white), let icon = UIImage(bundleImageName: "Chat/Message/ExpiredStoryPlaceholder") {
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: foregroundColor) {
UIGraphicsPushContext(context)
icon.draw(in: CGRect(origin: CGPoint(), size: size))
//image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), blendMode: .destinationOut, alpha: 1.0)
image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), blendMode: .normal, alpha: 1.0)
UIGraphicsPopContext()

View File

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

View File

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

View File

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

View File

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

View File

@ -466,7 +466,7 @@ public func threadNotificationExceptionsScreen(context: AccountContext, peerId:
let canRemove = true
let defaultSound: PeerMessageSound = globalSettings.groupChats.sound._asMessageSound()
pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, customTitle: item.info.title, threadId: item.threadId, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { _, sound in
pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, customTitle: item.info.title, threadId: item.threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, updatePeerSound: { _, sound in
let _ = (updateThreadSound(item.threadId, sound)
|> deliverOnMainQueue).start(next: { _ in
updateState { value in

View File

@ -46,6 +46,7 @@ public enum NotificationExceptionMode : Equatable {
case users
case groups
case channels
case stories
}
public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool {
@ -68,6 +69,12 @@ public enum NotificationExceptionMode : Equatable {
} else {
return false
}
case let .stories(lhsValue):
if case let .stories(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
}
}
@ -79,12 +86,14 @@ public enum NotificationExceptionMode : Equatable {
return .groups
case .channels:
return .channels
case .stories:
return .stories
}
}
public var isEmpty: Bool {
switch self {
case let .users(value), let .groups(value), let .channels(value):
case let .users(value), let .groups(value), let .channels(value), let .stories(value):
return value.isEmpty
}
}
@ -92,6 +101,7 @@ public enum NotificationExceptionMode : Equatable {
case users([EnginePeer.Id : NotificationExceptionWrapper])
case groups([EnginePeer.Id : NotificationExceptionWrapper])
case channels([EnginePeer.Id : NotificationExceptionWrapper])
case stories([EnginePeer.Id : NotificationExceptionWrapper])
public func withUpdatedPeerSound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionMode {
let apply:([EnginePeer.Id : NotificationExceptionWrapper], EnginePeer.Id, PeerMessageSound) -> [EnginePeer.Id : NotificationExceptionWrapper] = { values, peerId, sound in
@ -120,12 +130,14 @@ public enum NotificationExceptionMode : Equatable {
}
switch self {
case let .groups(values):
return .groups(apply(values, peer.id, sound))
case let .users(values):
return .users(apply(values, peer.id, sound))
case let .channels(values):
return .channels(apply(values, peer.id, sound))
case let .groups(values):
return .groups(apply(values, peer.id, sound))
case let .users(values):
return .users(apply(values, peer.id, sound))
case let .channels(values):
return .channels(apply(values, peer.id, sound))
case .stories:
return self
}
}
@ -172,12 +184,14 @@ public enum NotificationExceptionMode : Equatable {
muteState = .default
}
switch self {
case let .groups(values):
return .groups(apply(values, peer.id, muteState))
case let .users(values):
return .users(apply(values, peer.id, muteState))
case let .channels(values):
return .channels(apply(values, peer.id, muteState))
case let .groups(values):
return .groups(apply(values, peer.id, muteState))
case let .users(values):
return .users(apply(values, peer.id, muteState))
case let .channels(values):
return .channels(apply(values, peer.id, muteState))
case .stories:
return self
}
}
@ -208,12 +222,14 @@ public enum NotificationExceptionMode : Equatable {
}
switch self {
case let .groups(values):
return .groups(apply(values, peer.id, displayPreviews))
case let .users(values):
return .users(apply(values, peer.id, displayPreviews))
case let .channels(values):
return .channels(apply(values, peer.id, displayPreviews))
case let .groups(values):
return .groups(apply(values, peer.id, displayPreviews))
case let .users(values):
return .users(apply(values, peer.id, displayPreviews))
case let .channels(values):
return .channels(apply(values, peer.id, displayPreviews))
case .stories:
return self
}
}
@ -254,25 +270,23 @@ public enum NotificationExceptionMode : Equatable {
}
switch self {
case let .groups(values):
return .groups(apply(values, peer.id, storyNotifications))
case let .users(values):
return .users(apply(values, peer.id, storyNotifications))
case let .channels(values):
return .channels(apply(values, peer.id, storyNotifications))
case let .stories(values):
return .stories(apply(values, peer.id, storyNotifications))
default:
return self
}
}
public var peerIds: [EnginePeer.Id] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
return settings.map {$0.key}
case let .users(settings), let .groups(settings), let .channels(settings), let .stories(settings):
return settings.map { $0.key }
}
}
public var settings: [EnginePeer.Id : NotificationExceptionWrapper] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
case let .users(settings), let .groups(settings), let .channels(settings), let .stories(settings):
return settings
}
}
@ -535,7 +549,7 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry {
}
private func notificationPeerExceptionEntries(presentationData: PresentationData, peer: EnginePeer?, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] {
private func notificationPeerExceptionEntries(presentationData: PresentationData, peer: EnginePeer?, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState, isStories: Bool?) -> [NotificationPeerExceptionEntry] {
let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)
var entries: [NotificationPeerExceptionEntry] = []
@ -547,23 +561,26 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
index += 1
}
entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader))
index += 1
entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOn, selected: state.mode == .alwaysOn))
index += 1
entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOff, selected: state.mode == .alwaysOff))
index += 1
if state.mode != .alwaysOff {
entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_MessagePreviewHeader))
index += 1
entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.displayPreviews == .alwaysOn))
index += 1
entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff))
if isStories == nil || isStories == false {
entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader))
index += 1
entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOn, selected: state.mode == .alwaysOn))
index += 1
entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOff, selected: state.mode == .alwaysOff))
index += 1
if state.mode != .alwaysOff {
entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_MessagePreviewHeader))
index += 1
entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.displayPreviews == .alwaysOn))
index += 1
entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff))
index += 1
}
}
if isStories == nil || isStories == true {
if case .user = peer {
//TODO:localize
entries.append(.storyNotificationsHeader(index: index, theme: presentationData.theme, title: "STORY NOTIFICATIONS"))
@ -573,7 +590,9 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
entries.append(.storyNotifications(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.storyNotifications == .alwaysOff))
index += 1
}
}
if isStories == nil || isStories == false {
entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones))
index += 1
@ -674,7 +693,7 @@ private struct NotificationExceptionPeerState : Equatable {
}
}
public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<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 statePromise = Promise(initialState)
let stateValue = Atomic(value: initialState)
@ -782,7 +801,7 @@ public func notificationPeerExceptionController(context: AccountContext, updated
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(titleString), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, peer: peer, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: animated)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, peer: peer, notificationSoundList: notificationSoundList, state: state, isStories: isStories), style: .blocks, animateChanges: animated)
return (controllerState, (listState, arguments))
}

View File

@ -1294,7 +1294,8 @@ public final class StoryItemSetContainerComponent: Component {
timeoutSelected: false,
displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular,
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset,
hideKeyboard: false
hideKeyboard: false,
disabledPlaceholder: component.slice.peer.isService ? "You can't reply to this story" : nil
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)

View File

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

View File

@ -4514,6 +4514,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if let story = message.associatedStories[storyId], story.data.isEmpty {
//TODO:localize
self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: "This story is no longer available", timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
return
}
@ -4696,7 +4698,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode
avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme)
//avatarNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme)
case .feed:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}

View File

@ -3743,6 +3743,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} else {
return .optionalAction(performAction)
}
} else if let item = self.item, let story = item.message.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory {
if let storyItem = item.message.associatedStories[story.storyId] {
if storyItem.data.isEmpty {
return .action({
item.controllerInteraction.navigateToStory(item.message, story.storyId)
})
} else {
if let peer = item.message.peers[story.storyId.peerId] {
return .action({
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
})
}
}
}
}
}
loop: for contentNode in self.contentNodes {

View File

@ -60,7 +60,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.displayCallIcons = displayCallIcons
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _ in
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
contextActionImpl?(peer, node, gesture, nil)
} : nil, multipleSelection: multipleSelection)

View File

@ -4724,7 +4724,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let canRemove = false
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peer.id, sound)
|> deliverOnMainQueue).start(next: { _ in
})

View File

@ -88,12 +88,14 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable {
public var photo: MediaAutoDownloadCategory
public var video: MediaAutoDownloadCategory
public var file: MediaAutoDownloadCategory
public var stories: MediaAutoDownloadCategory
public init(basePreset: MediaAutoDownloadPreset, photo: MediaAutoDownloadCategory, video: MediaAutoDownloadCategory, file: MediaAutoDownloadCategory) {
public init(basePreset: MediaAutoDownloadPreset, photo: MediaAutoDownloadCategory, video: MediaAutoDownloadCategory, file: MediaAutoDownloadCategory, stories: MediaAutoDownloadCategory) {
self.basePreset = basePreset
self.photo = photo
self.video = video
self.file = file
self.stories = stories
}
public init(from decoder: Decoder) throws {
@ -103,6 +105,7 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable {
self.photo = try container.decode(MediaAutoDownloadCategory.self, forKey: "photo")
self.video = try container.decode(MediaAutoDownloadCategory.self, forKey: "video")
self.file = try container.decode(MediaAutoDownloadCategory.self, forKey: "file")
self.stories = try container.decodeIfPresent(MediaAutoDownloadCategory.self, forKey: "stories") ?? MediaAutoDownloadSettings.defaultSettings.presets.high.stories
}
public func encode(to encoder: Encoder) throws {
@ -112,6 +115,7 @@ public struct MediaAutoDownloadCategories: Codable, Equatable, Comparable {
try container.encode(self.photo, forKey: "photo")
try container.encode(self.video, forKey: "video")
try container.encode(self.file, forKey: "file")
try container.encode(self.stories, forKey: "stories")
}
public static func < (lhs: MediaAutoDownloadCategories, rhs: MediaAutoDownloadCategories) -> Bool {
@ -370,15 +374,29 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
public static var defaultSettings: MediaAutoDownloadSettings {
let mb: Int64 = 1024 * 1024
let presets = MediaAutoDownloadPresets(low: MediaAutoDownloadCategories(basePreset: .low, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false),
file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false)),
medium: MediaAutoDownloadCategories(basePreset: .medium, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: Int64(2.5 * CGFloat(mb)), predownload: false),
file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false)),
high: MediaAutoDownloadCategories(basePreset: .high, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 10 * mb, predownload: true),
file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 3 * mb, predownload: false)))
let presets = MediaAutoDownloadPresets(low:
MediaAutoDownloadCategories(
basePreset: .low,
photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false),
file: MediaAutoDownloadCategory(contacts: false, otherPrivate: false, groups: false, channels: false, sizeLimit: 1 * mb, predownload: false),
stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false)
),
medium: MediaAutoDownloadCategories(
basePreset: .medium,
photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: Int64(2.5 * CGFloat(mb)), predownload: false),
file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false)
),
high: MediaAutoDownloadCategories(
basePreset: .high,
photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 1 * mb, predownload: false),
video: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 10 * mb, predownload: true),
file: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 3 * mb, predownload: false),
stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false)
)
)
return MediaAutoDownloadSettings(presets: presets, cellular: MediaAutoDownloadConnection(enabled: true, preset: .medium, custom: nil), wifi: MediaAutoDownloadConnection(enabled: true, preset: .high, custom: nil), downloadInBackground: true, energyUsageSettings: EnergyUsageSettings.default)
}
@ -436,7 +454,13 @@ private func categoriesWithAutodownloadPreset(_ autodownloadPreset: Autodownload
let fileEnabled = autodownloadPreset.fileSizeMax > 0
let fileSizeMax = autodownloadPreset.fileSizeMax > 0 ? autodownloadPreset.fileSizeMax : 1 * 1024 * 1024
return MediaAutoDownloadCategories(basePreset: preset, photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false), video: MediaAutoDownloadCategory(contacts: videoEnabled, otherPrivate: videoEnabled, groups: videoEnabled, channels: videoEnabled, sizeLimit: videoSizeMax, predownload: autodownloadPreset.preloadLargeVideo), file: MediaAutoDownloadCategory(contacts: fileEnabled, otherPrivate: fileEnabled, groups: fileEnabled, channels: fileEnabled, sizeLimit: fileSizeMax, predownload: false))
return MediaAutoDownloadCategories(
basePreset: preset,
photo: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false),
video: MediaAutoDownloadCategory(contacts: videoEnabled, otherPrivate: videoEnabled, groups: videoEnabled, channels: videoEnabled, sizeLimit: videoSizeMax, predownload: autodownloadPreset.preloadLargeVideo),
file: MediaAutoDownloadCategory(contacts: fileEnabled, otherPrivate: fileEnabled, groups: fileEnabled, channels: fileEnabled, sizeLimit: fileSizeMax, predownload: false),
stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: autodownloadPreset.photoSizeMax, predownload: false)
)
}
private func presetsWithAutodownloadSettings(_ autodownloadSettings: AutodownloadSettings) -> MediaAutoDownloadPresets {
@ -479,7 +503,11 @@ public func effectiveAutodownloadCategories(settings: MediaAutoDownloadSettings,
}
}
private func categoryAndSizeForMedia(_ media: Media?, categories: MediaAutoDownloadCategories) -> (MediaAutoDownloadCategory, Int32?)? {
private func categoryAndSizeForMedia(_ media: Media?, isStory: Bool, categories: MediaAutoDownloadCategories) -> (MediaAutoDownloadCategory, Int32?)? {
if isStory {
return (categories.stories, 0)
}
guard let media = media else {
return (categories.photo, 0)
}
@ -524,7 +552,7 @@ public func isAutodownloadEnabledForAnyPeerType(category: MediaAutoDownloadCateg
return category.contacts || category.otherPrivate || category.groups || category.channels
}
public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings, peerType: MediaAutoDownloadPeerType, networkType: MediaAutoDownloadNetworkType, authorPeerId: PeerId? = nil, contactsPeerIds: Set<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) {
return false
}
@ -537,7 +565,7 @@ public func shouldDownloadMediaAutomatically(settings: MediaAutoDownloadSettings
peerType = .contact
}
if let (category, size) = categoryAndSizeForMedia(media, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) {
if let (category, size) = categoryAndSizeForMedia(media, isStory: isStory, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) {
if let size = size {
var sizeLimit = category.sizeLimit
if let file = media as? TelegramMediaFile, file.isVoice {
@ -564,7 +592,7 @@ public func shouldPredownloadMedia(settings: MediaAutoDownloadSettings, peerType
return false
}
if let (category, _) = categoryAndSizeForMedia(media, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) {
if let (category, _) = categoryAndSizeForMedia(media, isStory: false, categories: effectiveAutodownloadCategories(settings: settings, networkType: networkType)) {
guard isAutodownloadEnabledForPeerType(peerType, category: category) else {
return false
}

View File

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