mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Stories
This commit is contained in:
parent
b8130348ab
commit
d5e010b510
@ -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 **%@**";
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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?
|
||||
|
Loading…
x
Reference in New Issue
Block a user