[WIP] Topics

This commit is contained in:
Ali 2022-10-18 16:56:27 +04:00
parent 02b28ee6fc
commit 6847dbb4c3
51 changed files with 2598 additions and 1614 deletions

View File

@ -44,8 +44,8 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
var hasUnread = false
var fixedCombinedReadStates: MessageHistoryViewReadState?
if let readState = readState {
hasUnread = readState.count != 0
fixedCombinedReadStates = .peer([index.messageIndex.id.peerId: readState])
hasUnread = readState.state.count != 0
fixedCombinedReadStates = .peer([index.messageIndex.id.peerId: readState.state])
}
if !isMuted && hasUnread {
@ -56,7 +56,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
for entry in view.0.entries {
var isRead = true
if let readState = readState {
isRead = readState.isIncomingMessageIndexRead(entry.message.index)
isRead = readState.state.isIncomingMessageIndexRead(entry.message.index)
}
if !isRead {

View File

@ -80,6 +80,8 @@ swift_library(
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen",
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/TelegramUI/Components/ChatTimerScreen",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/AnimationUI:AnimationUI",
"//submodules/PeerInfoUI",
],

View File

@ -12,6 +12,10 @@ import AlertUI
import PresentationDataUtils
import UndoUI
import PremiumUI
import TelegramPresentationData
import TelegramStringFormatting
import ChatTimerScreen
import NotificationPeerExceptionController
func archiveContextMenuItems(context: AccountContext, groupId: PeerGroupId, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
@ -523,16 +527,196 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
if case .muted = threadData.notificationSettings.muteState {
isMuted = true
}
items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in
let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: threadId)
|> deliverOnMainQueue).start(completed: {
f(.default)
})
items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak chatListController] c, f in
if isMuted {
let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: threadId)
|> deliverOnMainQueue).start(completed: {
f(.default)
})
} else {
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteFor, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Mute2d"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
var subItems: [ContextMenuItem] = []
/*subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c.popItems()
})))
subItems.append(.separator)*/
let presetValues: [Int32] = [
1 * 60 * 60,
8 * 60 * 60,
1 * 24 * 60 * 60,
7 * 24 * 60 * 60
]
for value in presetValues {
subItems.append(.action(ContextMenuActionItem(text: muteForIntervalString(strings: presentationData.strings, value: value), icon: { _ in
return nil
}, action: { _, f in
f(.default)
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: value).start()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_mute_for", scale: 0.066, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedFor(mutedForTimeIntervalString(strings: presentationData.strings, value: value)).string, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
}
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteForCustom, icon: { _ in
return nil
}, action: { _, f in
f(.default)
if let chatListController = chatListController {
openCustomMute(context: context, peerId: peerId, threadId: threadId, baseController: chatListController)
}
})))
//c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
c.setItems(.single(ContextController.Items(content: .list(subItems))), minHeight: nil)
})))
items.append(.separator)
var isSoundEnabled = true
switch threadData.notificationSettings.messageSound {
case .none:
isSoundEnabled = false
default:
break
}
if case .muted = threadData.notificationSettings.muteState {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonUnmute, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: nil).start()
let iconColor: UIColor = .white
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: presentationData.strings.PeerInfo_TooltipUnmuted, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
} else if !isSoundEnabled {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_EnableSound, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: .default).start()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipSoundEnabled, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_DisableSound, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOff"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: .none).start()
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_sound_off", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipSoundDisabled, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_NotificationsCustomize, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.NotificationSettings.Global()
)
|> deliverOnMainQueue).start(next: { globalSettings in
let updatePeerSound: (PeerId, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: threadId, sound: sound) |> deliverOnMainQueue
}
let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal<Void, NoError> = { peerId, muteInterval in
return context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: muteInterval) |> deliverOnMainQueue
}
let updatePeerDisplayPreviews: (PeerId, PeerNotificationDisplayPreviews) -> Signal<Void, NoError> = {
peerId, displayPreviews in
return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) |> deliverOnMainQueue
}
let defaultSound: PeerMessageSound
if case .broadcast = channel.info {
defaultSound = globalSettings.channels.sound._asMessageSound()
} else {
defaultSound = globalSettings.groupChats.sound._asMessageSound()
}
let canRemove = false
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: channel, threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peerId, sound)
|> deliverOnMainQueue).start(next: { _ in
})
}, updatePeerNotificationInterval: { peerId, muteInterval in
let _ = (updatePeerNotificationInterval(peerId, muteInterval)
|> deliverOnMainQueue).start(next: { _ in
if let muteInterval = muteInterval, muteInterval == Int32.max {
let iconColor: UIColor = .white
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
}
})
}, updatePeerDisplayPreviews: { peerId, displayPreviews in
let _ = (updatePeerDisplayPreviews(peerId, displayPreviews)
|> deliverOnMainQueue).start(next: { _ in
})
}, removePeerFromExceptions: {
}, modifiedPeer: {
})
exceptionController.navigationPresentation = .modal
chatListController?.push(exceptionController)
})
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_MuteForever, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Muted"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.default)
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: Int32.max).start()
let iconColor: UIColor = .white
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedForever, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
c.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
}
})))
if canManage || threadData.isOwnedByMe {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? "Restart" : "Close", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? "Restart" : "Close", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
let _ = context.engine.peers.setForumChannelTopicClosed(id: peerId, threadId: threadId, isClosed: !threadData.isClosed).start()
@ -548,3 +732,21 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
return .single(items)
}
}
private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) {
let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, peerId: peerId, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if value <= 0 {
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: nil).start()
} else {
let _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: value).start()
let timeString = stringForPreciseRelativeTimestamp(strings: presentationData.strings, relativeTimestamp: Int32(Date().timeIntervalSince1970) + value, relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: presentationData.dateTimeFormat)
baseController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_mute_for", scale: 0.056, colors: [:], title: nil, text: presentationData.strings.PeerInfo_TooltipMutedUntil(timeString).string, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
}
})
baseController.view.endEditing(true)
baseController.present(controller, in: .window(.root))
}

View File

@ -1438,7 +1438,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let searchSignal = context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
|> map { result, updatedState -> ChatListSearchMessagesResult in
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
}
let loadMore = searchContext.get()
@ -1447,7 +1447,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let _ = searchContext.loadMoreIndex {
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: searchContext.result.state, limit: 80)
|> map { result, updatedState -> ChatListSearchMessagesResult in
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues(EnginePeerReadCounters.init), threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
}
|> mapToSignal { foundMessages -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool), NoError> in
updateSearchContext { previous in

View File

@ -347,7 +347,7 @@ private func forumRevealOptions(strings: PresentationStrings, theme: Presentatio
if !isClosed {
options.append(ItemListRevealOption(key: RevealOptionKey.close.rawValue, title: strings.ChatList_CloseAction, icon: closeIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: closeIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
}
}
}
@ -376,7 +376,14 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation
if isUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor))
var canMarkUnread = true
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
canMarkUnread = false
}
if canMarkUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor))
}
}
if !isEditing {
if isPinned {
@ -531,6 +538,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var dustNode: InvisibleInkDustNode?
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
var dateStatusIconNode: ASImageNode?
let separatorNode: ASDisplayNode
let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode
@ -997,7 +1005,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.highlightedBackgroundNode.layer.removeAllAnimations()
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
if let item = self.item {
if let item = self.item, case .chatList = item.index {
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil, transition: transition)
}
} else {
@ -1011,9 +1019,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
})
}
if let item = self.item {
if let item = self.item, case let .chatList(index) = item.index {
let onlineIcon: UIImage?
if case let .chatList(index) = item.index, index.pinningIndex != nil {
if index.pinningIndex != nil {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat)
} else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat)
@ -1094,7 +1102,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
contentPeer = .chat(peerValue)
combinedReadState = combinedReadStateValue
if let combinedReadState = combinedReadState, promoInfoValue == nil && !ignoreUnreadBadge {
unreadCount = (combinedReadState.count, combinedReadState.isUnread, isRemovedFromTotalUnreadCountValue, nil)
unreadCount = (combinedReadState.count, combinedReadState.isUnread, isRemovedFromTotalUnreadCountValue || combinedReadState.isMuted, nil)
} else {
unreadCount = (0, false, false, nil)
}
@ -1652,13 +1660,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.presentationData.theme)
}
let statusWidth: CGFloat
var statusWidth: CGFloat
if case .none = statusState {
statusWidth = 0.0
} else {
statusWidth = 24.0
}
var dateIconImage: UIImage?
if let threadInfo, threadInfo.isClosed {
dateIconImage = PresentationResourcesChatList.statusLockIcon(item.presentationData.theme)
}
if let dateIconImage {
statusWidth += dateIconImage.size.width + 4.0
}
var titleIconsWidth: CGFloat = 0.0
if let currentMutedIconImage = currentMutedIconImage {
if titleIconsWidth.isZero {
@ -1824,13 +1841,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
online = true
}
animateOnline = true
} else if case let .channel(channel) = renderedPeer.peer {
} else if case let .channel(channel) = renderedPeer.peer, case .chatList = item.index {
onlineIsVoiceChat = true
if channel.flags.contains(.hasActiveVoiceChat) && item.interaction.searchTextHighightState == nil {
online = true
}
animateOnline = true
} else if case let .legacyGroup(group) = renderedPeer.peer {
} else if case let .legacyGroup(group) = renderedPeer.peer, case .chatList = item.index {
onlineIsVoiceChat = true
if group.flags.contains(.hasActiveVoiceChat) && item.interaction.searchTextHighightState == nil {
online = true
@ -1984,6 +2001,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.dateNode, alpha: 0.0)
if let dateStatusIconNode = strongSelf.dateStatusIconNode {
transition.updateAlpha(node: dateStatusIconNode, alpha: 0.0)
}
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 0.0)
@ -1999,6 +2019,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
reorderControlNode?.removeFromSupernode()
})
transition.updateAlpha(node: strongSelf.dateNode, alpha: 1.0)
if let dateStatusIconNode = strongSelf.dateStatusIconNode {
transition.updateAlpha(node: dateStatusIconNode, alpha: 1.0)
}
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 1.0)
@ -2093,8 +2116,40 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size))
var statusOffset: CGFloat = 0.0
if let dateIconImage {
statusOffset += 2.0 + dateIconImage.size.width + 4.0
let dateStatusIconNode: ASImageNode
if let current = strongSelf.dateStatusIconNode {
dateStatusIconNode = current
} else {
dateStatusIconNode = ASImageNode()
strongSelf.dateStatusIconNode = dateStatusIconNode
strongSelf.contextContainer.addSubnode(dateStatusIconNode)
}
dateStatusIconNode.image = dateIconImage
var dateStatusX: CGFloat = contentRect.origin.x
dateStatusX += contentRect.size.width
dateStatusX += -dateLayout.size.width - 4.0 - dateIconImage.size.width
var dateStatusY: CGFloat = contentRect.origin.y + 2.0 + UIScreenPixel
dateStatusY += -UIScreenPixel + floor((dateLayout.size.height - dateIconImage.size.height) / 2.0)
transition.updateFrame(node: dateStatusIconNode, frame: CGRect(origin: CGPoint(x: dateStatusX, y: dateStatusY), size: dateIconImage.size))
} else if let dateStatusIconNode = strongSelf.dateStatusIconNode {
strongSelf.dateStatusIconNode = nil
dateStatusIconNode.removeFromSupernode()
}
let statusSize = CGSize(width: 24.0, height: 24.0)
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - statusSize.width, y: contentRect.origin.y + 2.0 - UIScreenPixel + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
var statusX: CGFloat = contentRect.origin.x
statusX += contentRect.size.width
statusX += -dateLayout.size.width - statusSize.width - statusOffset
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: statusX, y: contentRect.origin.y + 2.0 - UIScreenPixel + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
@ -2597,7 +2652,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size))
let statusFrame = self.statusNode.frame
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size))
var statusOffset: CGFloat = 0.0
if let dateStatusIconNode = self.dateStatusIconNode, let dateIconImage = dateStatusIconNode.image {
statusOffset += 2.0 + dateIconImage.size.width + 4.0
var dateStatusX: CGFloat = contentRect.origin.x
dateStatusX += contentRect.size.width
dateStatusX += -dateFrame.size.width - 4.0 - dateIconImage.size.width
let dateStatusY: CGFloat = dateStatusIconNode.frame.minY
transition.updateFrame(node: dateStatusIconNode, frame: CGRect(origin: CGPoint(x: dateStatusX, y: dateStatusY), size: dateIconImage.size))
}
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - statusFrame.size.width - statusOffset, y: statusFrame.minY), size: statusFrame.size))
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + titleOffset

View File

@ -232,11 +232,13 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat
pinnedIndex = .none
}
let readCounters = EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))]), isMuted: false)
items.append(EngineChatList.Item(
id: .forum(item.id),
index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id),
messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [],
readCounters: EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))])),
readCounters: readCounters,
isMuted: isMuted,
draft: nil,
threadData: data,

View File

@ -218,7 +218,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
link: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor),
linkAttribute: { _ in return nil }
)
)

View File

@ -54,7 +54,7 @@ public final class HashtagSearchController: TelegramBaseController {
|> map { result, presentationData in
let result = result.0
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) })
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap { EnginePeerReadCounters(state: $0, isMuted: false) }, nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) })
}
let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {
}, peerSelected: { _, _, _, _ in

View File

@ -101,7 +101,7 @@ public struct ChatListGroupReferenceEntry: Equatable {
}
public enum ChatListEntry: Comparable {
case MessageEntry(index: ChatListIndex, messages: [Message], readState: CombinedPeerReadState?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, summaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: StoredMessageHistoryThreadInfo?, hasFailed: Bool, isContact: Bool)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, summaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: StoredMessageHistoryThreadInfo?, hasFailed: Bool, isContact: Bool)
case HoleEntry(ChatListHole)
public var index: ChatListIndex {
@ -184,7 +184,7 @@ public enum ChatListEntry: Comparable {
enum MutableChatListEntry: Equatable {
case IntermediateMessageEntry(index: ChatListIndex, messageIndex: MessageIndex?)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: StoredMessageHistoryThreadInfo?, hasFailedMessages: Bool, isContact: Bool)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, notificationSettings: PeerNotificationSettings?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: StoredMessageHistoryThreadInfo?, hasFailedMessages: Bool, isContact: Bool)
case HoleEntry(ChatListHole)
init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
@ -375,6 +375,16 @@ func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [PeerId: Peer])
return result
}
public struct ChatListViewReadState: Equatable {
public var state: CombinedPeerReadState
public var isMuted: Bool
public init(state: CombinedPeerReadState, isMuted: Bool) {
self.state = state
self.isMuted = isMuted
}
}
final class MutableChatListView {
let groupId: PeerGroupId
let filterPredicate: ChatListFilterPredicate?
@ -661,12 +671,22 @@ final class MutableChatListView {
forumTopicData = postbox.messageHistoryThreadIndexTable.get(peerId: message.id.peerId, threadId: threadId)
}
let readState: CombinedPeerReadState?
let readState: ChatListViewReadState?
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
let count = postbox.peerThreadsSummaryTable.get(peerId: index.messageIndex.id.peerId)?.totalUnreadCount ?? 0
readState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))])
let summary = postbox.peerThreadsSummaryTable.get(peerId: index.messageIndex.id.peerId)
var count: Int32 = 0
var isMuted: Bool = false
if let summary = summary {
count = summary.totalUnreadCount
if count > 0 {
isMuted = !summary.hasUnmutedUnread
}
}
readState = ChatListViewReadState(state: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))]), isMuted: isMuted)
} else {
readState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)
readState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId).flatMap { state -> ChatListViewReadState in
return ChatListViewReadState(state: state, isMuted: false)
}
}
return .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers)), presence: presence, tagSummaryInfo: [:], forumTopicData: forumTopicData, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact)

View File

@ -913,10 +913,22 @@ private final class ChatListViewSpaceState {
var updatedReadState = readState
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
let count = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.totalUnreadCount ?? 0
updatedReadState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))])
let summary = postbox.peerThreadsSummaryTable.get(peerId: peer.id)
var count: Int32 = 0
var isMuted: Bool = false
if let summary = summary {
count = summary.totalUnreadCount
if count > 0 {
isMuted = !summary.hasUnmutedUnread
}
}
updatedReadState = ChatListViewReadState(state: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))]), isMuted: isMuted)
} else {
updatedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)
updatedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId).flatMap { state in
return ChatListViewReadState(state: state, isMuted: false)
}
}
if updatedReadState != readState {
@ -1514,12 +1526,24 @@ struct ChatListViewState {
forumTopicData = postbox.messageHistoryThreadIndexTable.get(peerId: message.id.peerId, threadId: threadId)
}
let readState: CombinedPeerReadState?
let readState: ChatListViewReadState?
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
let count = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.totalUnreadCount ?? 0
readState = CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))])
let summary = postbox.peerThreadsSummaryTable.get(peerId: peer.id)
var count: Int32 = 0
var isMuted: Bool = false
if let summary = summary {
count = summary.totalUnreadCount
if count > 0 {
isMuted = !summary.hasUnmutedUnread
}
}
readState = ChatListViewReadState(state: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))]), isMuted: isMuted)
} else {
readState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)
readState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId).flatMap { state in
return ChatListViewReadState(state: state, isMuted: false)
}
}
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, forumTopicData: forumTopicData, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId))

View File

@ -1153,9 +1153,14 @@ public final class Transaction {
self.postbox?.removePeerTimeoutAttributeEntry(peerId: peerId, timestamp: timestamp)
}
public func getMessageHistoryThreadIndex(peerId: PeerId) -> [(threadId: Int64, index: MessageIndex, info: StoredMessageHistoryThreadInfo)] {
public func getMessageHistoryThreadIndex(peerId: PeerId, limit: Int) -> [(threadId: Int64, index: MessageIndex, info: StoredMessageHistoryThreadInfo)] {
assert(!self.disposed)
return self.postbox!.messageHistoryThreadIndexTable.getAll(peerId: peerId)
return self.postbox!.messageHistoryThreadIndexTable.fetch(peerId: peerId, namespace: 0, start: .upperBound, end: .lowerBound, limit: limit)
}
public func getMessageHistoryThreadTopMessage(peerId: PeerId, threadId: Int64, namespaces: Set<MessageId.Namespace>) -> MessageIndex? {
assert(!self.disposed)
return self.postbox!.messageHistoryThreadsTable.getTop(peerId: peerId, threadId: threadId, namespaces: namespaces)
}
public func getMessageHistoryThreadInfo(peerId: PeerId, threadId: Int64) -> StoredMessageHistoryThreadInfo? {

View File

@ -106,6 +106,7 @@ swift_library(
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
],
visibility = [
"//visibility:public",

View File

@ -19,6 +19,7 @@ import ChatListSearchItemHeader
import ChatListUI
import ItemListPeerActionItem
import TelegramStringFormatting
import NotificationPeerExceptionController
private final class NotificationExceptionState : Equatable {
let mode: NotificationExceptionMode
@ -66,227 +67,6 @@ private final class NotificationExceptionState : Equatable {
}
}
public struct NotificationExceptionWrapper : Equatable {
let settings: TelegramPeerNotificationSettings
let date: TimeInterval?
let peer: Peer
init(settings: TelegramPeerNotificationSettings, peer: Peer, date: TimeInterval? = nil) {
self.settings = settings
self.date = date
self.peer = peer
}
public static func ==(lhs: NotificationExceptionWrapper, rhs: NotificationExceptionWrapper) -> Bool {
return lhs.settings == rhs.settings && lhs.date == rhs.date
}
func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: settings, peer: self.peer, date: self.date)
}
func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: f(self.settings), peer: self.peer, date: self.date)
}
func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: self.settings, peer: self.peer, date: date)
}
}
public enum NotificationExceptionMode : Equatable {
fileprivate enum Mode {
case users
case groups
case channels
}
public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool {
switch lhs {
case let .users(lhsValue):
if case let .users(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
case let .groups(lhsValue):
if case let .groups(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
case let .channels(lhsValue):
if case let .channels(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
}
}
fileprivate var mode: Mode {
switch self {
case .users:
return .users
case .groups:
return .groups
case .channels:
return .channels
}
}
var isEmpty: Bool {
switch self {
case let .users(value), let .groups(value), let .channels(value):
return value.isEmpty
}
}
case users([PeerId : NotificationExceptionWrapper])
case groups([PeerId : NotificationExceptionWrapper])
case channels([PeerId : NotificationExceptionWrapper])
func withUpdatedPeerSound(_ peer: Peer, _ sound: PeerMessageSound) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in
var values = values
if let value = values[peerId] {
switch sound {
case .default:
switch value.settings.muteState {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch sound {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound, displayPreviews: .default), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
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))
}
}
func withUpdatedPeerMuteInterval(_ peer: Peer, _ muteInterval: Int32?) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in
var values = values
if let value = values[peerId] {
switch muteState {
case .default:
switch value.settings.messageSound {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch muteState {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default, displayPreviews: .default), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
let muteState: PeerMuteState
if let muteInterval = muteInterval {
if muteInterval == 0 {
muteState = .unmuted
} else {
let absoluteUntil: Int32
if muteInterval == Int32.max {
absoluteUntil = Int32.max
} else {
absoluteUntil = muteInterval
}
muteState = .muted(until: absoluteUntil)
}
} else {
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))
}
}
func withUpdatedPeerDisplayPreviews(_ peer: Peer, _ displayPreviews: PeerNotificationDisplayPreviews) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerNotificationDisplayPreviews) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, displayPreviews in
var values = values
if let value = values[peerId] {
switch displayPreviews {
case .default:
switch value.settings.displayPreviews {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedDisplayPreviews(displayPreviews)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedDisplayPreviews(displayPreviews)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch displayPreviews {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .unmuted, messageSound: .default, displayPreviews: displayPreviews), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
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))
}
}
var peerIds: [PeerId] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
return settings.map {$0.key}
}
}
var settings: [PeerId : NotificationExceptionWrapper] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
return settings
}
}
}
private func notificationsExceptionEntries(presentationData: PresentationData, notificationSoundList: NotificationSoundList?, state: NotificationExceptionState, query: String? = nil, foundPeers: [RenderedPeer] = []) -> [NotificationExceptionEntry] {
var entries: [NotificationExceptionEntry] = []

View File

@ -1,548 +1 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import LocalizedPeerData
import TelegramStringFormatting
import NotificationSoundSelectionUI
private enum NotificationPeerExceptionSection: Int32 {
case remove
case switcher
case displayPreviews
case soundCloud
case soundModern
case soundClassic
}
private enum NotificationPeerExceptionSwitcher: Hashable {
case alwaysOn
case alwaysOff
}
private enum NotificationPeerExceptionEntryId: Hashable {
case remove
case switcher(NotificationPeerExceptionSwitcher)
case sound(PeerMessageSound.Id)
case switcherHeader
case displayPreviews(NotificationPeerExceptionSwitcher)
case displayPreviewsHeader
case soundModernHeader
case soundClassicHeader
case none
case uploadSound
case cloudHeader
case cloudInfo
case `default`
}
private final class NotificationPeerExceptionArguments {
let account: Account
let selectSound: (PeerMessageSound) -> Void
let selectMode: (NotificationPeerExceptionSwitcher) -> Void
let selectDisplayPreviews: (NotificationPeerExceptionSwitcher) -> Void
let removeFromExceptions: () -> Void
let complete: () -> Void
let cancel: () -> Void
let upload: () -> Void
let deleteSound: (PeerMessageSound, String) -> Void
init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, selectDisplayPreviews: @escaping (NotificationPeerExceptionSwitcher) -> Void, removeFromExceptions: @escaping () -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void, upload: @escaping () -> Void, deleteSound: @escaping (PeerMessageSound, String) -> Void) {
self.account = account
self.selectSound = selectSound
self.selectMode = selectMode
self.selectDisplayPreviews = selectDisplayPreviews
self.removeFromExceptions = removeFromExceptions
self.complete = complete
self.cancel = cancel
self.upload = upload
self.deleteSound = deleteSound
}
}
private enum NotificationPeerExceptionEntry: ItemListNodeEntry {
typealias ItemGenerationArguments = NotificationPeerExceptionArguments
case remove(index:Int32, theme: PresentationTheme, strings: PresentationStrings)
case switcher(index:Int32, theme: PresentationTheme, strings: PresentationStrings, mode: NotificationPeerExceptionSwitcher, selected: Bool)
case switcherHeader(index:Int32, theme: PresentationTheme, title: String)
case displayPreviews(index:Int32, theme: PresentationTheme, strings: PresentationStrings, value: NotificationPeerExceptionSwitcher, selected: Bool)
case displayPreviewsHeader(index:Int32, theme: PresentationTheme, title: String)
case soundModernHeader(index:Int32, theme: PresentationTheme, title: String)
case soundClassicHeader(index:Int32, theme: PresentationTheme, title: String)
case none(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool)
case `default`(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool)
case sound(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool, canBeDeleted: Bool)
case cloudHeader(index: Int32, text: String)
case uploadSound(index: Int32, text: String)
case cloudInfo(index: Int32, text: String)
var index: Int32 {
switch self {
case let .remove(index, _, _):
return index
case let .switcherHeader(index, _, _):
return index
case let .switcher(index, _, _, _, _):
return index
case let .displayPreviewsHeader(index, _, _):
return index
case let .displayPreviews(index, _, _, _, _):
return index
case let .soundModernHeader(index, _, _):
return index
case let .soundClassicHeader(index, _, _):
return index
case let .none(index, _, _, _, _):
return index
case let .default(index, _, _, _, _):
return index
case let .sound(index, _, _, _, _, _, _):
return index
case let .cloudHeader(index, _):
return index
case let .cloudInfo(index, _):
return index
case let .uploadSound(index, _):
return index
}
}
var section: ItemListSectionId {
switch self {
case .remove:
return NotificationPeerExceptionSection.remove.rawValue
case .switcher, .switcherHeader:
return NotificationPeerExceptionSection.switcher.rawValue
case .displayPreviews, .displayPreviewsHeader:
return NotificationPeerExceptionSection.displayPreviews.rawValue
case .cloudInfo, .cloudHeader, .uploadSound:
return NotificationPeerExceptionSection.soundCloud.rawValue
case .soundModernHeader:
return NotificationPeerExceptionSection.soundModern.rawValue
case .soundClassicHeader:
return NotificationPeerExceptionSection.soundClassic.rawValue
case let .none(_, section, _, _, _):
return section.rawValue
case let .default(_, section, _, _, _):
return section.rawValue
case let .sound(_, section, _, _, _, _, _):
return section.rawValue
}
}
var stableId: NotificationPeerExceptionEntryId {
switch self {
case .remove:
return .remove
case let .switcher(_, _, _, mode, _):
return .switcher(mode)
case .switcherHeader:
return .switcherHeader
case let .displayPreviews(_, _, _, mode, _):
return .displayPreviews(mode)
case .displayPreviewsHeader:
return .displayPreviewsHeader
case .soundModernHeader:
return .soundModernHeader
case .soundClassicHeader:
return .soundClassicHeader
case .none:
return .none
case .default:
return .default
case let .sound(_, _, _, _, sound, _, _):
return .sound(sound.id)
case .uploadSound:
return .uploadSound
case .cloudHeader:
return .cloudHeader
case .cloudInfo:
return .cloudInfo
}
}
static func <(lhs: NotificationPeerExceptionEntry, rhs: NotificationPeerExceptionEntry) -> Bool {
return lhs.index < rhs.index
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationPeerExceptionArguments
switch self {
case let .remove(_, _, strings):
return ItemListActionItem(presentationData: presentationData, title: strings.Notification_Exceptions_RemoveFromExceptions, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.removeFromExceptions()
})
case let .switcher(_, _, strings, mode, selected):
let title: String
switch mode {
case .alwaysOn:
title = strings.Notification_Exceptions_AlwaysOn
case .alwaysOff:
title = strings.Notification_Exceptions_AlwaysOff
}
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectMode(mode)
})
case let .switcherHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .displayPreviews(_, _, strings, value, selected):
let title: String
switch value {
case .alwaysOn:
title = strings.Notification_Exceptions_MessagePreviewAlwaysOn
case .alwaysOff:
title = strings.Notification_Exceptions_MessagePreviewAlwaysOff
}
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectDisplayPreviews(value)
})
case let .cloudHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .cloudInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .uploadSound(_, text):
let icon = PresentationResourcesItemList.uploadToneIcon(presentationData.theme)
return ItemListCheckboxItem(presentationData: presentationData, icon: icon, iconSize: nil, iconPlacement: .check, title: text, style: .left, textColor: .accent, checked: false, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.upload()
})
case let .displayPreviewsHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .soundModernHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .soundClassicHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .none(_, _, _, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: {
arguments.selectSound(.none)
})
case let .default(_, _, _, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(.default)
})
case let .sound(_, _, _, text, sound, selected, canBeDeleted):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(sound)
}, deleteAction: canBeDeleted ? {
arguments.deleteSound(sound, text)
} : nil)
}
}
}
private func notificationPeerExceptionEntries(presentationData: PresentationData, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] {
let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)
var entries: [NotificationPeerExceptionEntry] = []
var index: Int32 = 0
if state.canRemove {
entries.append(.remove(index: index, theme: presentationData.theme, strings: presentationData.strings))
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))
index += 1
entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones))
index += 1
index = 1000
if let notificationSoundList = notificationSoundList {
let cloudSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == nil })
let modernSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == .modern })
let classicSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == .classic })
for listSound in cloudSounds {
let sound: PeerMessageSound = .cloud(fileId: listSound.file.fileId.id)
if state.removedSounds.contains(where: { $0.id == sound.id }) {
continue
}
entries.append(.sound(index: index, section: .soundCloud, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: selectedSound.id == sound.id, canBeDeleted: true))
index += 1
}
index = 2000
entries.append(.uploadSound(index: index, text: presentationData.strings.Notifications_UploadSound))
index += 1
entries.append(.cloudInfo(index: index, text: presentationData.strings.Notifications_MessageSoundInfo))
index += 1
entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones))
index = 3000
entries.append(.default(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: state.defaultSound), selected: selectedSound == .default))
index += 1
entries.append(.none(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: selectedSound == .none))
index += 1
for i in 0 ..< modernSounds.count {
let sound: PeerMessageSound = .cloud(fileId: modernSounds[i].file.fileId.id)
entries.append(.sound(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false))
index += 1
}
entries.append(.soundClassicHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_ClassicTones))
index += 1
for i in 0 ..< classicSounds.count {
let sound: PeerMessageSound = .cloud(fileId: classicSounds[i].file.fileId.id)
entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false))
index += 1
}
}
}
return entries
}
private struct NotificationExceptionPeerState : Equatable {
var canRemove: Bool
var selectedSound: PeerMessageSound
var mode: NotificationPeerExceptionSwitcher
var defaultSound: PeerMessageSound
var displayPreviews: NotificationPeerExceptionSwitcher
var removedSounds: [PeerMessageSound]
init(canRemove: Bool, notifications: TelegramPeerNotificationSettings? = nil) {
self.canRemove = canRemove
if let notifications = notifications {
self.selectedSound = notifications.messageSound
switch notifications.muteState {
case let .muted(until) where until >= Int32.max - 1:
self.mode = .alwaysOff
default:
self.mode = .alwaysOn
}
self.displayPreviews = notifications.displayPreviews == .hide ? .alwaysOff : .alwaysOn
} else {
self.selectedSound = .default
self.mode = .alwaysOn
self.displayPreviews = .alwaysOn
}
self.defaultSound = .default
self.removedSounds = []
}
init(canRemove: Bool, selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound, displayPreviews: NotificationPeerExceptionSwitcher, removedSounds: [PeerMessageSound]) {
self.canRemove = canRemove
self.selectedSound = selectedSound
self.mode = mode
self.defaultSound = defaultSound
self.displayPreviews = displayPreviews
self.removedSounds = removedSounds
}
}
public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: Peer, customTitle: String? = nil, threadId: Int64?, canRemove: Bool, defaultSound: PeerMessageSound, edit: Bool = false, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController {
let initialState = NotificationExceptionPeerState(canRemove: false)
let statePromise = Promise(initialState)
let stateValue = Atomic(value: initialState)
let updateState: ((NotificationExceptionPeerState) -> NotificationExceptionPeerState) -> Void = { f in
statePromise.set(.single(stateValue.modify { f($0) }))
}
var completeImpl: (() -> Void)?
var removeFromExceptionsImpl: (() -> Void)?
var cancelImpl: (() -> Void)?
let playSoundDisposable = MetaDisposable()
var presentFilePicker: (() -> Void)?
var deleteSoundImpl: ((PeerMessageSound, String) -> Void)?
let soundActionDisposable = MetaDisposable()
let arguments = NotificationPeerExceptionArguments(account: context.account, selectSound: { sound in
updateState { state in
let _ = (context.engine.peers.notificationSoundList()
|> take(1)
|> deliverOnMainQueue).start(next: { notificationSoundList in
playSoundDisposable.set(playSound(context: context, notificationSoundList: notificationSoundList, sound: sound, defaultSound: state.defaultSound).start())
})
var state = state
state.selectedSound = sound
return state
}
}, selectMode: { mode in
updateState { state in
var state = state
state.mode = mode
return state
}
}, selectDisplayPreviews: { value in
updateState { state in
var state = state
state.displayPreviews = value
return state
}
}, removeFromExceptions: {
removeFromExceptionsImpl?()
}, complete: {
completeImpl?()
}, cancel: {
cancelImpl?()
}, upload: {
presentFilePicker?()
}, deleteSound: { sound, title in
deleteSoundImpl?(sound, title)
})
statePromise.set(context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id),
EngineDataOptional(threadId.flatMap { TelegramEngine.EngineData.Item.Peer.ThreadNotificationSettings(id: peer.id, threadId: $0) }),
TelegramEngine.EngineData.Item.NotificationSettings.Global()
)
|> map { peerNotificationSettings, threadNotificationSettings, globalNotificationSettings -> NotificationExceptionPeerState in
let effectiveSettings = threadNotificationSettings ?? peerNotificationSettings
var state = NotificationExceptionPeerState(canRemove: canRemove, notifications: effectiveSettings._asNotificationSettings())
state.defaultSound = defaultSound
let _ = stateValue.swap(state)
return state
})
let previousSoundIds = Atomic<Set<Int64>>(value: Set())
let signal = combineLatest(queue: .mainQueue(), (updatedPresentationData?.signal ?? context.sharedContext.presentationData), context.engine.peers.notificationSoundList(), statePromise.get() |> distinctUntilChanged)
|> map { presentationData, notificationSoundList, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
arguments.cancel()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(state.canRemove || edit ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: {
arguments.complete()
})
var updatedSoundIds = Set<Int64>()
if let notificationSoundList = notificationSoundList {
for sound in notificationSoundList.sounds {
if state.removedSounds.contains(.cloud(fileId: sound.file.fileId.id)) {
continue
}
updatedSoundIds.insert(sound.file.fileId.id)
}
}
var animated = false
if previousSoundIds.swap(updatedSoundIds) != updatedSoundIds {
animated = true
}
let titleString: String
if let customTitle = customTitle {
titleString = customTitle
} else {
titleString = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
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, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: animated)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal |> afterDisposed {
playSoundDisposable.dispose()
soundActionDisposable.dispose()
})
controller.enableInteractiveDismiss = true
completeImpl = { [weak controller] in
controller?.dismiss()
modifiedPeer()
let _ = (context.engine.peers.notificationSoundList()
|> take(1)
|> deliverOnMainQueue).start(next: { notificationSoundList in
updateState { state in
updatePeerSound(peer.id, resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList))
updatePeerNotificationInterval(peer.id, state.mode == .alwaysOn ? 0 : Int32.max)
updatePeerDisplayPreviews(peer.id, state.displayPreviews == .alwaysOn ? .show : .hide)
return state
}
})
}
removeFromExceptionsImpl = { [weak controller] in
controller?.dismiss()
removePeerFromExceptions()
}
cancelImpl = { [weak controller] in
controller?.dismiss()
}
presentFilePicker = { [weak controller] in
guard let controller = controller else {
return
}
presentCustomNotificationSoundFilePicker(context: context, controller: controller, disposable: soundActionDisposable)
}
deleteSoundImpl = { [weak controller] sound, title in
guard let controller = controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
updateState { state in
var state = state
state.removedSounds.append(sound)
if state.selectedSound.id == sound.id {
state.selectedSound = defaultCloudPeerNotificationSound
}
return state
}
switch sound {
case let .cloud(id):
soundActionDisposable.set((context.engine.peers.deleteNotificationSound(fileId: id)
|> deliverOnMainQueue).start(completed: {
}))
default:
break
}
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
})
], parseMarkdown: true), in: .window(.root))
}
return controller
}

View File

@ -7,6 +7,7 @@ import TelegramCore
import TelegramPresentationData
import AccountContext
import SearchUI
import NotificationPeerExceptionController
public class NotificationExceptionsController: ViewController {
private let context: AccountContext

View File

@ -15,6 +15,7 @@ import PresentationDataUtils
import TelegramNotices
import NotificationSoundSelectionUI
import TelegramStringFormatting
import NotificationPeerExceptionController
private struct CounterTagSettings: OptionSet {
var rawValue: Int32

View File

@ -18,6 +18,7 @@ import NotificationSoundSelectionUI
import TelegramStringFormatting
import ItemListPeerItem
import ItemListPeerActionItem
import NotificationPeerExceptionController
private extension EnginePeer.NotificationSettings.MuteState {
var timeInterval: Int32? {

View File

@ -346,7 +346,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
arguments.openGroupsPrivacy()
})
case let .voiceMessagePrivacy(_, text, value, locked):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: locked ? .textWithIcon(UIImage(bundleImageName: "Notification/SecretLock")!.precomposed()) : .text, sectionId: self.section, style: .blocks, action: {
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: locked ? .textWithIcon(UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon")!.precomposed()) : .text, sectionId: self.section, style: .blocks, action: {
arguments.openVoiceMessagePrivacy()
})
case let .selectivePrivacyInfo(_, text):

View File

@ -17,6 +17,7 @@ import PresentationDataUtils
import PhoneNumberFormat
import AccountUtils
import InstantPageCache
import NotificationPeerExceptionController
enum SettingsSearchableItemIcon {
case profile

View File

@ -135,7 +135,7 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode {
self.iconNode.image = icon
self.textNode.attributedText = title
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
self.lockNode.image = locked ? generateTintedImage(image: UIImage(bundleImageName: "Notification/SecretLock"), color: color) : nil
self.lockNode.image = locked ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: color) : nil
self.action = {
action()
}

View File

@ -14,11 +14,13 @@ public final class AdMessageAttribute: MessageAttribute {
public let opaqueId: Data
public let messageType: MessageType
public let displayAvatar: Bool
public let target: MessageTarget
public init(opaqueId: Data, messageType: MessageType, target: MessageTarget) {
public init(opaqueId: Data, messageType: MessageType, displayAvatar: Bool, target: MessageTarget) {
self.opaqueId = opaqueId
self.messageType = messageType
self.displayAvatar = displayAvatar
self.target = target
}

View File

@ -20,14 +20,22 @@ public extension EngineTotalReadCounters {
}
public struct EnginePeerReadCounters: Equatable {
fileprivate let state: CombinedPeerReadState?
fileprivate var state: CombinedPeerReadState?
public var isMuted: Bool
public init(state: CombinedPeerReadState?) {
public init(state: CombinedPeerReadState?, isMuted: Bool) {
self.state = state
self.isMuted = isMuted
}
public init(state: ChatListViewReadState?) {
self.state = state?.state
self.isMuted = state?.isMuted ?? false
}
public init() {
self.state = CombinedPeerReadState(states: [])
self.isMuted = false
}
public var count: Int32 {
@ -68,7 +76,7 @@ public struct EnginePeerReadCounters: Equatable {
public extension EnginePeerReadCounters {
init(incomingReadId: EngineMessage.Id.Id, outgoingReadId: EngineMessage.Id.Id, count: Int32, markedUnread: Bool) {
self.init(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: incomingReadId, maxOutgoingReadId: outgoingReadId, maxKnownId: max(incomingReadId, outgoingReadId), count: count, markedUnread: markedUnread))]))
self.init(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: incomingReadId, maxOutgoingReadId: outgoingReadId, maxKnownId: max(incomingReadId, outgoingReadId), count: count, markedUnread: markedUnread))]), isMuted: false)
}
func _asReadCounters() -> CombinedPeerReadState? {
@ -173,7 +181,7 @@ public extension TelegramEngine.EngineData.Item {
preconditionFailure()
}
return EnginePeerReadCounters(state: view.state)
return EnginePeerReadCounters(state: view.state, isMuted: false)
}
}

View File

@ -8,6 +8,7 @@ private class AdMessagesHistoryContextImpl {
enum CodingKeys: String, CodingKey {
case opaqueId
case messageType
case displayAvatar
case text
case textEntities
case media
@ -65,6 +66,7 @@ private class AdMessagesHistoryContextImpl {
public let opaqueId: Data
public let messageType: MessageType
public let displayAvatar: Bool
public let text: String
public let textEntities: [MessageTextEntity]
public let media: [Media]
@ -75,6 +77,7 @@ private class AdMessagesHistoryContextImpl {
public init(
opaqueId: Data,
messageType: MessageType,
displayAvatar: Bool,
text: String,
textEntities: [MessageTextEntity],
media: [Media],
@ -84,6 +87,7 @@ private class AdMessagesHistoryContextImpl {
) {
self.opaqueId = opaqueId
self.messageType = messageType
self.displayAvatar = displayAvatar
self.text = text
self.textEntities = textEntities
self.media = media
@ -103,6 +107,8 @@ private class AdMessagesHistoryContextImpl {
self.messageType = .sponsored
}
self.displayAvatar = try container.decodeIfPresent(Bool.self, forKey: .displayAvatar) ?? false
self.text = try container.decode(String.self, forKey: .text)
self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities)
@ -121,6 +127,7 @@ private class AdMessagesHistoryContextImpl {
try container.encode(self.opaqueId, forKey: .opaqueId)
try container.encode(self.messageType.rawValue, forKey: .messageType)
try container.encode(self.displayAvatar, forKey: .displayAvatar)
try container.encode(self.text, forKey: .text)
try container.encode(self.textEntities, forKey: .textEntities)
@ -186,7 +193,7 @@ private class AdMessagesHistoryContextImpl {
case .recommended:
mappedMessageType = .recommended
}
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, target: target))
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, displayAvatar: self.displayAvatar, target: target))
if !self.textEntities.isEmpty {
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(attribute)
@ -431,6 +438,7 @@ private class AdMessagesHistoryContextImpl {
}
let isRecommended = (flags & (1 << 5)) != 0
let displayAvatar = (flags & (1 << 6)) != 0
let _ = chatInvite
let _ = chatInviteHash
@ -479,6 +487,7 @@ private class AdMessagesHistoryContextImpl {
parsedMessages.append(CachedMessage(
opaqueId: randomId.makeData(),
messageType: isRecommended ? .recommended : .sponsored,
displayAvatar: displayAvatar,
text: message,
textEntities: parsedEntities,
media: [],

View File

@ -116,42 +116,66 @@ func _internal_togglePeerUnreadMarkInteractively(postbox: Postbox, viewTracker:
}
func _internal_togglePeerUnreadMarkInteractively(transaction: Transaction, viewTracker: AccountViewTracker, peerId: PeerId, setToValue: Bool? = nil) {
let principalNamespace: MessageId.Namespace
if peerId.namespace == Namespaces.Peer.SecretChat {
principalNamespace = Namespaces.Message.SecretIncoming
} else {
principalNamespace = Namespaces.Message.Cloud
guard let peer = transaction.getPeer(peerId) else {
return
}
var hasUnread = false
if let states = transaction.getPeerReadStates(peerId) {
for state in states {
if state.1.isUnread {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 20) {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else {
continue
}
guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else {
continue
}
if data.incomingUnreadCount != 0 {
data.incomingUnreadCount = 0
data.maxIncomingReadId = max(messageIndex.id.id, data.maxIncomingReadId)
data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id)
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: item.threadId, info: entry)
}
}
}
} else {
let principalNamespace: MessageId.Namespace
if peerId.namespace == Namespaces.Peer.SecretChat {
principalNamespace = Namespaces.Message.SecretIncoming
} else {
principalNamespace = Namespaces.Message.Cloud
}
var hasUnread = false
if let states = transaction.getPeerReadStates(peerId) {
for state in states {
if state.1.isUnread {
hasUnread = true
break
}
}
}
if !hasUnread && peerId.namespace == Namespaces.Peer.SecretChat {
let unseenSummary = transaction.getMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud)
let actionSummary = transaction.getPendingMessageActionsSummary(peerId: peerId, type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)
if (unseenSummary?.count ?? 0) - (actionSummary ?? 0) > 0 {
hasUnread = true
break
}
}
}
if !hasUnread && peerId.namespace == Namespaces.Peer.SecretChat {
let unseenSummary = transaction.getMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud)
let actionSummary = transaction.getPendingMessageActionsSummary(peerId: peerId, type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)
if (unseenSummary?.count ?? 0) - (actionSummary ?? 0) > 0 {
hasUnread = true
}
}
if hasUnread {
if setToValue == nil || !(setToValue!) {
if let index = transaction.getTopPeerMessageIndex(peerId: peerId) {
let _ = transaction.applyInteractiveReadMaxIndex(index)
} else {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: false, interactive: true)
if hasUnread {
if setToValue == nil || !(setToValue!) {
if let index = transaction.getTopPeerMessageIndex(peerId: peerId) {
let _ = transaction.applyInteractiveReadMaxIndex(index)
} else {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: false, interactive: true)
}
viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: nil)
}
} else {
if setToValue == nil || setToValue! {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: true, interactive: true)
}
viewTracker.updateMarkAllMentionsSeen(peerId: peerId, threadId: nil)
}
} else {
if setToValue == nil || setToValue! {
transaction.applyMarkUnread(peerId: peerId, namespace: principalNamespace, value: true, interactive: true)
}
}
}

View File

@ -94,6 +94,7 @@ public enum PresentationResourceKey: Int32 {
case chatListFakeOutgoingIcon
case chatListFakeServiceIcon
case chatListSecretIcon
case chatListStatusLockIcon
case chatListRecentStatusOnlineIcon
case chatListRecentStatusOnlineHighlightedIcon
case chatListRecentStatusOnlinePinnedIcon
@ -105,6 +106,7 @@ public enum PresentationResourceKey: Int32 {
case chatTitleLockIcon
case chatTitleMuteIcon
case chatPanelLockIcon
case chatBubbleVerticalLineIncomingImage
case chatBubbleVerticalLineOutgoingImage

View File

@ -75,6 +75,12 @@ public struct PresentationResourcesChat {
})
}
public static func chatPanelLockIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatPanelLockIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.rootController.navigationBar.controlColor)
})
}
public static func chatTitleMuteIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatTitleMuteIcon.rawValue, { theme in
return generateImage(CGSize(width: 9.0, height: 9.0), rotatedContext: { size, context in

View File

@ -354,4 +354,10 @@ public struct PresentationResourcesChatList {
})
})
}
public static func statusLockIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListStatusLockIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StatusLockIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
})
}
}

View File

@ -301,6 +301,7 @@ swift_library(
"//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen",
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTimerScreen",
module_name = "ChatTimerScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,748 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import SolidRoundedButtonNode
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
public enum ChatTimerScreenStyle {
case `default`
case media
}
public enum ChatTimerScreenMode {
case sendTimer
case autoremove
case mute
}
public final class ChatTimerScreen: ViewController {
private var controllerNode: ChatTimerScreenNode {
return self.displayNode as! ChatTimerScreenNode
}
private var animatedIn = false
private let context: AccountContext
private let peerId: PeerId
private let style: ChatTimerScreenStyle
private let mode: ChatTimerScreenMode
private let currentTime: Int32?
private let dismissByTapOutside: Bool
private let completion: (Int32) -> Void
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
self.context = context
self.peerId = peerId
self.style = style
self.mode = mode
self.currentTime = currentTime
self.dismissByTapOutside = dismissByTapOutside
self.completion = completion
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.statusBar.statusBarStyle = .Ignore
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, mode: self.mode, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside)
self.controllerNode.completion = { [weak self] time in
guard let strongSelf = self else {
return
}
strongSelf.completion(time)
strongSelf.dismiss()
}
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.cancel = { [weak self] in
self?.dismiss()
}
}
override public func loadView() {
super.loadView()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: completion)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private protocol TimerPickerView: UIView {
}
private class TimerCustomPickerView: UIPickerView, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
}
private class TimerDatePickerView: UIDatePicker, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
}
private class TimerPickerItemView: UIView {
let valueLabel = UILabel()
let unitLabel = UILabel()
var textColor: UIColor? = nil {
didSet {
self.valueLabel.textColor = self.textColor
self.unitLabel.textColor = self.textColor
}
}
var value: (Int32, String)? {
didSet {
if let (_, string) = self.value {
let components = string.components(separatedBy: " ")
if components.count > 1 {
self.valueLabel.text = components[0]
self.unitLabel.text = components[1]
}
}
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
self.valueLabel.backgroundColor = nil
self.valueLabel.isOpaque = false
self.valueLabel.font = Font.regular(24.0)
self.unitLabel.backgroundColor = nil
self.unitLabel.isOpaque = false
self.unitLabel.font = Font.medium(16.0)
super.init(frame: frame)
self.addSubview(self.valueLabel)
self.addSubview(self.unitLabel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.valueLabel.sizeToFit()
self.unitLabel.sizeToFit()
self.valueLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 20.0 - self.valueLabel.frame.size.width, y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size)
self.unitLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 12.0, y: floor((self.frame.height - self.unitLabel.frame.height) / 2.0) + 2.0), size: self.unitLabel.frame.size)
}
}
private var timerValues: [Int32] = {
var values: [Int32] = []
for i in 1 ..< 20 {
values.append(Int32(i))
}
for i in 0 ..< 9 {
values.append(Int32(20 + i * 5))
}
return values
}()
class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
private let context: AccountContext
private let controllerStyle: ChatTimerScreenStyle
private var presentationData: PresentationData
private let dismissByTapOutside: Bool
private let mode: ChatTimerScreenMode
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
private let effectNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let titleNode: ASTextNode
private let textNode: ImmediateTextNode
private let cancelButton: HighlightableButtonNode
private let doneButton: SolidRoundedButtonNode
private let disableButton: HighlightableButtonNode
private let disableButtonTitle: ImmediateTextNode
private var initialTime: Int32?
private var pickerView: TimerPickerView?
private let autoremoveTimerValues: [Int32]
private var containerLayout: (ContainerViewLayout, CGFloat)?
var completion: ((Int32) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode, currentTime: Int32?, dismissByTapOutside: Bool) {
self.context = context
self.controllerStyle = style
self.presentationData = presentationData
self.dismissByTapOutside = dismissByTapOutside
self.mode = mode
self.initialTime = currentTime
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 16.0
let backgroundColor: UIColor
let textColor: UIColor
let accentColor: UIColor
let blurStyle: UIBlurEffect.Style
switch style {
case .default:
backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
textColor = self.presentationData.theme.actionSheet.primaryTextColor
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
case .media:
backgroundColor = UIColor(rgb: 0x1c1c1e)
textColor = .white
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
blurStyle = .dark
}
self.effectNode = ASDisplayNode(viewBlock: {
return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
})
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
let title: String
switch self.mode {
case .sendTimer:
title = self.presentationData.strings.Conversation_Timer_Title
case .autoremove:
title = self.presentationData.strings.Conversation_DeleteTimer_SetupTitle
case .mute:
title = self.presentationData.strings.Conversation_Mute_SetupTitle
}
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
self.textNode = ImmediateTextNode()
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal)
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
self.disableButton = HighlightableButtonNode()
self.disableButtonTitle = ImmediateTextNode()
self.disableButton.addSubnode(self.disableButtonTitle)
self.disableButtonTitle.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_DeleteTimer_Disable, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
self.disableButton.isHidden = true
switch self.mode {
case .autoremove:
if self.initialTime != nil {
self.disableButton.isHidden = false
}
default:
break
}
self.autoremoveTimerValues = [
1 * 24 * 60 * 60 as Int32,
2 * 24 * 60 * 60 as Int32,
3 * 24 * 60 * 60 as Int32,
4 * 24 * 60 * 60 as Int32,
5 * 24 * 60 * 60 as Int32,
6 * 24 * 60 * 60 as Int32,
1 * 7 * 24 * 60 * 60 as Int32,
2 * 7 * 24 * 60 * 60 as Int32,
3 * 7 * 24 * 60 * 60 as Int32,
1 * 31 * 24 * 60 * 60 as Int32,
2 * 30 * 24 * 60 * 60 as Int32,
3 * 31 * 24 * 60 * 60 as Int32,
4 * 30 * 24 * 60 * 60 as Int32,
5 * 31 * 24 * 60 * 60 as Int32,
6 * 30 * 24 * 60 * 60 as Int32,
365 * 24 * 60 * 60 as Int32
]
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.backgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.backgroundNode.addSubnode(self.effectNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.textNode)
self.contentContainerNode.addSubnode(self.cancelButton)
self.contentContainerNode.addSubnode(self.doneButton)
self.contentContainerNode.addSubnode(self.disableButton)
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.doneButton.pressed = { [weak self] in
if let strongSelf = self, let pickerView = strongSelf.pickerView {
strongSelf.doneButton.isUserInteractionEnabled = false
if let pickerView = pickerView as? TimerCustomPickerView {
switch strongSelf.mode {
case .sendTimer:
strongSelf.completion?(timerValues[pickerView.selectedRow(inComponent: 0)])
case .autoremove:
let timeInterval = strongSelf.autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]
strongSelf.completion?(Int32(timeInterval))
case .mute:
break
}
} else if let pickerView = pickerView as? TimerDatePickerView {
switch strongSelf.mode {
case .mute:
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
strongSelf.completion?(timeInterval)
default:
break
}
}
}
}
self.disableButton.addTarget(self, action: #selector(self.disableButtonPressed), forControlEvents: .touchUpInside)
self.setupPickerView(currentTime: currentTime)
}
@objc private func disableButtonPressed() {
self.completion?(0)
}
func setupPickerView(currentTime: Int32? = nil) {
if let pickerView = self.pickerView {
pickerView.removeFromSuperview()
}
switch self.mode {
case .sendTimer:
let pickerView = TimerCustomPickerView()
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.dataSource = self
pickerView.delegate = self
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
case .autoremove:
let pickerView = TimerCustomPickerView()
pickerView.dataSource = self
pickerView.delegate = self
pickerView.selectorColor = self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.18)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
if let value = self.initialTime {
var selectedRowIndex = 0
for i in 0 ..< self.autoremoveTimerValues.count {
if self.autoremoveTimerValues[i] <= value {
selectedRowIndex = i
}
}
pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false)
}
case .mute:
let pickerView = TimerDatePickerView()
pickerView.locale = localeWithStrings(self.presentationData.strings)
pickerView.datePickerMode = .dateAndTime
pickerView.minimumDate = Date()
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(self.presentationData.theme.list.itemPrimaryTextColor, forKey: "textColor")
pickerView.setValue(false, forKey: "highlightsToday")
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
}
}
@objc private func dataPickerChanged() {
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
switch self.mode {
case .sendTimer:
return 1
case .autoremove:
return 1
case .mute:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch self.mode {
case .sendTimer:
return timerValues.count
case .autoremove:
return self.autoremoveTimerValues.count
case .mute:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
switch self.mode {
case .sendTimer:
let value = timerValues[row]
let string = timeIntervalString(strings: self.presentationData.strings, value: value)
if let view = view as? TimerPickerItemView {
view.value = (value, string)
return view
}
let view = TimerPickerItemView()
view.value = (value, string)
view.textColor = .white
return view
case .autoremove:
let itemView: TimerPickerItemView
if let current = view as? TimerPickerItemView {
itemView = current
} else {
itemView = TimerPickerItemView()
itemView.textColor = self.presentationData.theme.list.itemPrimaryTextColor
}
let value = self.autoremoveTimerValues[row]
let string: String
string = timeIntervalString(strings: self.presentationData.strings, value: value)
itemView.value = (value, string)
return itemView
case .mute:
preconditionFailure()
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.dataPickerChanged()
}
func updatePresentationData(_ presentationData: PresentationData) {
let previousTheme = self.presentationData.theme
self.presentationData = presentationData
guard case .default = self.controllerStyle else {
return
}
if let effectView = self.effectNode.view as? UIVisualEffectView {
effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
}
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
self.setupPickerView()
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if self.dismissByTapOutside, case .ended = recognizer.state {
self.cancelButtonPressed()
}
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetBounds = self.bounds
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset)
transition.animateView({
self.bounds = targetBounds
self.dimNode.position = dimPosition
})
}
func animateOut(completion: (() -> Void)? = nil) {
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
return self.dimNode.view
}
}
return super.hitTest(point, with: event)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
var buttonOffset: CGFloat = 0.0
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
let titleHeight: CGFloat = 54.0
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight)
if !self.disableButton.isHidden {
buttonOffset += 52.0
}
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset = floor((layout.size.width - width) / 2.0)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
let contentFrame = contentContainerFrame
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
switch self.mode {
case .sendTimer:
break
case .autoremove:
self.doneButton.title = self.presentationData.strings.Conversation_DeleteTimer_Apply
case .mute:
if let pickerView = self.pickerView as? TimerDatePickerView {
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
if timeInterval > 0 {
let timeString = stringForPreciseRelativeTimestamp(strings: self.presentationData.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
self.doneButton.title = self.presentationData.strings.Conversation_Mute_ApplyMuteUntil(timeString).string
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
}
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight)
transition.updateFrame(node: self.doneButton, frame: doneButtonFrame)
let disableButtonTitleSize = self.disableButtonTitle.updateLayout(CGSize(width: contentFrame.width, height: doneButtonHeight))
let disableButtonFrame = CGRect(origin: CGPoint(x: doneButtonFrame.minX, y: doneButtonFrame.maxY), size: CGSize(width: contentFrame.width - buttonInset * 2.0, height: doneButtonHeight))
transition.updateFrame(node: self.disableButton, frame: disableButtonFrame)
transition.updateFrame(node: self.disableButtonTitle, frame: CGRect(origin: CGPoint(x: floor((disableButtonFrame.width - disableButtonTitleSize.width) / 2.0), y: floor((disableButtonFrame.height - disableButtonTitleSize.height) / 2.0)), size: disableButtonTitleSize))
self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
}
}

View File

@ -17,6 +17,7 @@ import TelegramStringFormatting
import ItemListPeerItem
import ItemListPeerActionItem
import SettingsUI
import NotificationPeerExceptionController
private extension EnginePeer.NotificationSettings.MuteState {
var timeInterval: Int32? {

View File

@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "NotificationPeerExceptionController",
module_name = "NotificationPeerExceptionController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ItemListUI:ItemListUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/NotificationSoundSelectionUI:NotificationSoundSelectionUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,768 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import LocalizedPeerData
import TelegramStringFormatting
import NotificationSoundSelectionUI
public struct NotificationExceptionWrapper : Equatable {
public let settings: TelegramPeerNotificationSettings
public let date: TimeInterval?
public let peer: Peer
public init(settings: TelegramPeerNotificationSettings, peer: Peer, date: TimeInterval? = nil) {
self.settings = settings
self.date = date
self.peer = peer
}
public static func ==(lhs: NotificationExceptionWrapper, rhs: NotificationExceptionWrapper) -> Bool {
return lhs.settings == rhs.settings && lhs.date == rhs.date
}
public func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: settings, peer: self.peer, date: self.date)
}
public func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: f(self.settings), peer: self.peer, date: self.date)
}
public func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper {
return NotificationExceptionWrapper(settings: self.settings, peer: self.peer, date: date)
}
}
public enum NotificationExceptionMode : Equatable {
public enum Mode {
case users
case groups
case channels
}
public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool {
switch lhs {
case let .users(lhsValue):
if case let .users(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
case let .groups(lhsValue):
if case let .groups(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
case let .channels(lhsValue):
if case let .channels(rhsValue) = rhs {
return lhsValue == rhsValue
} else {
return false
}
}
}
public var mode: Mode {
switch self {
case .users:
return .users
case .groups:
return .groups
case .channels:
return .channels
}
}
public var isEmpty: Bool {
switch self {
case let .users(value), let .groups(value), let .channels(value):
return value.isEmpty
}
}
case users([PeerId : NotificationExceptionWrapper])
case groups([PeerId : NotificationExceptionWrapper])
case channels([PeerId : NotificationExceptionWrapper])
public func withUpdatedPeerSound(_ peer: Peer, _ sound: PeerMessageSound) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in
var values = values
if let value = values[peerId] {
switch sound {
case .default:
switch value.settings.muteState {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch sound {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound, displayPreviews: .default), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
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))
}
}
public func withUpdatedPeerMuteInterval(_ peer: Peer, _ muteInterval: Int32?) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in
var values = values
if let value = values[peerId] {
switch muteState {
case .default:
switch value.settings.messageSound {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch muteState {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default, displayPreviews: .default), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
let muteState: PeerMuteState
if let muteInterval = muteInterval {
if muteInterval == 0 {
muteState = .unmuted
} else {
let absoluteUntil: Int32
if muteInterval == Int32.max {
absoluteUntil = Int32.max
} else {
absoluteUntil = muteInterval
}
muteState = .muted(until: absoluteUntil)
}
} else {
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))
}
}
public func withUpdatedPeerDisplayPreviews(_ peer: Peer, _ displayPreviews: PeerNotificationDisplayPreviews) -> NotificationExceptionMode {
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerNotificationDisplayPreviews) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, displayPreviews in
var values = values
if let value = values[peerId] {
switch displayPreviews {
case .default:
switch value.settings.displayPreviews {
case .default:
values.removeValue(forKey: peerId)
default:
values[peerId] = value.updateSettings({$0.withUpdatedDisplayPreviews(displayPreviews)}).withUpdatedDate(Date().timeIntervalSince1970)
}
default:
values[peerId] = value.updateSettings({$0.withUpdatedDisplayPreviews(displayPreviews)}).withUpdatedDate(Date().timeIntervalSince1970)
}
} else {
switch displayPreviews {
case .default:
break
default:
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .unmuted, messageSound: .default, displayPreviews: displayPreviews), peer: peer, date: Date().timeIntervalSince1970)
}
}
return values
}
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))
}
}
public var peerIds: [PeerId] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
return settings.map {$0.key}
}
}
public var settings: [PeerId : NotificationExceptionWrapper] {
switch self {
case let .users(settings), let .groups(settings), let .channels(settings):
return settings
}
}
}
private enum NotificationPeerExceptionSection: Int32 {
case remove
case switcher
case displayPreviews
case soundCloud
case soundModern
case soundClassic
}
private enum NotificationPeerExceptionSwitcher: Hashable {
case alwaysOn
case alwaysOff
}
private enum NotificationPeerExceptionEntryId: Hashable {
case remove
case switcher(NotificationPeerExceptionSwitcher)
case sound(PeerMessageSound.Id)
case switcherHeader
case displayPreviews(NotificationPeerExceptionSwitcher)
case displayPreviewsHeader
case soundModernHeader
case soundClassicHeader
case none
case uploadSound
case cloudHeader
case cloudInfo
case `default`
}
private final class NotificationPeerExceptionArguments {
let account: Account
let selectSound: (PeerMessageSound) -> Void
let selectMode: (NotificationPeerExceptionSwitcher) -> Void
let selectDisplayPreviews: (NotificationPeerExceptionSwitcher) -> Void
let removeFromExceptions: () -> Void
let complete: () -> Void
let cancel: () -> Void
let upload: () -> Void
let deleteSound: (PeerMessageSound, String) -> Void
init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, selectDisplayPreviews: @escaping (NotificationPeerExceptionSwitcher) -> Void, removeFromExceptions: @escaping () -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void, upload: @escaping () -> Void, deleteSound: @escaping (PeerMessageSound, String) -> Void) {
self.account = account
self.selectSound = selectSound
self.selectMode = selectMode
self.selectDisplayPreviews = selectDisplayPreviews
self.removeFromExceptions = removeFromExceptions
self.complete = complete
self.cancel = cancel
self.upload = upload
self.deleteSound = deleteSound
}
}
private enum NotificationPeerExceptionEntry: ItemListNodeEntry {
typealias ItemGenerationArguments = NotificationPeerExceptionArguments
case remove(index:Int32, theme: PresentationTheme, strings: PresentationStrings)
case switcher(index:Int32, theme: PresentationTheme, strings: PresentationStrings, mode: NotificationPeerExceptionSwitcher, selected: Bool)
case switcherHeader(index:Int32, theme: PresentationTheme, title: String)
case displayPreviews(index:Int32, theme: PresentationTheme, strings: PresentationStrings, value: NotificationPeerExceptionSwitcher, selected: Bool)
case displayPreviewsHeader(index:Int32, theme: PresentationTheme, title: String)
case soundModernHeader(index:Int32, theme: PresentationTheme, title: String)
case soundClassicHeader(index:Int32, theme: PresentationTheme, title: String)
case none(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool)
case `default`(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool)
case sound(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool, canBeDeleted: Bool)
case cloudHeader(index: Int32, text: String)
case uploadSound(index: Int32, text: String)
case cloudInfo(index: Int32, text: String)
var index: Int32 {
switch self {
case let .remove(index, _, _):
return index
case let .switcherHeader(index, _, _):
return index
case let .switcher(index, _, _, _, _):
return index
case let .displayPreviewsHeader(index, _, _):
return index
case let .displayPreviews(index, _, _, _, _):
return index
case let .soundModernHeader(index, _, _):
return index
case let .soundClassicHeader(index, _, _):
return index
case let .none(index, _, _, _, _):
return index
case let .default(index, _, _, _, _):
return index
case let .sound(index, _, _, _, _, _, _):
return index
case let .cloudHeader(index, _):
return index
case let .cloudInfo(index, _):
return index
case let .uploadSound(index, _):
return index
}
}
var section: ItemListSectionId {
switch self {
case .remove:
return NotificationPeerExceptionSection.remove.rawValue
case .switcher, .switcherHeader:
return NotificationPeerExceptionSection.switcher.rawValue
case .displayPreviews, .displayPreviewsHeader:
return NotificationPeerExceptionSection.displayPreviews.rawValue
case .cloudInfo, .cloudHeader, .uploadSound:
return NotificationPeerExceptionSection.soundCloud.rawValue
case .soundModernHeader:
return NotificationPeerExceptionSection.soundModern.rawValue
case .soundClassicHeader:
return NotificationPeerExceptionSection.soundClassic.rawValue
case let .none(_, section, _, _, _):
return section.rawValue
case let .default(_, section, _, _, _):
return section.rawValue
case let .sound(_, section, _, _, _, _, _):
return section.rawValue
}
}
var stableId: NotificationPeerExceptionEntryId {
switch self {
case .remove:
return .remove
case let .switcher(_, _, _, mode, _):
return .switcher(mode)
case .switcherHeader:
return .switcherHeader
case let .displayPreviews(_, _, _, mode, _):
return .displayPreviews(mode)
case .displayPreviewsHeader:
return .displayPreviewsHeader
case .soundModernHeader:
return .soundModernHeader
case .soundClassicHeader:
return .soundClassicHeader
case .none:
return .none
case .default:
return .default
case let .sound(_, _, _, _, sound, _, _):
return .sound(sound.id)
case .uploadSound:
return .uploadSound
case .cloudHeader:
return .cloudHeader
case .cloudInfo:
return .cloudInfo
}
}
static func <(lhs: NotificationPeerExceptionEntry, rhs: NotificationPeerExceptionEntry) -> Bool {
return lhs.index < rhs.index
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationPeerExceptionArguments
switch self {
case let .remove(_, _, strings):
return ItemListActionItem(presentationData: presentationData, title: strings.Notification_Exceptions_RemoveFromExceptions, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.removeFromExceptions()
})
case let .switcher(_, _, strings, mode, selected):
let title: String
switch mode {
case .alwaysOn:
title = strings.Notification_Exceptions_AlwaysOn
case .alwaysOff:
title = strings.Notification_Exceptions_AlwaysOff
}
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectMode(mode)
})
case let .switcherHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .displayPreviews(_, _, strings, value, selected):
let title: String
switch value {
case .alwaysOn:
title = strings.Notification_Exceptions_MessagePreviewAlwaysOn
case .alwaysOff:
title = strings.Notification_Exceptions_MessagePreviewAlwaysOff
}
return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectDisplayPreviews(value)
})
case let .cloudHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .cloudInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .uploadSound(_, text):
let icon = PresentationResourcesItemList.uploadToneIcon(presentationData.theme)
return ItemListCheckboxItem(presentationData: presentationData, icon: icon, iconSize: nil, iconPlacement: .check, title: text, style: .left, textColor: .accent, checked: false, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.upload()
})
case let .displayPreviewsHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .soundModernHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .soundClassicHeader(_, _, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .none(_, _, _, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: {
arguments.selectSound(.none)
})
case let .default(_, _, _, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(.default)
})
case let .sound(_, _, _, text, sound, selected, canBeDeleted):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectSound(sound)
}, deleteAction: canBeDeleted ? {
arguments.deleteSound(sound, text)
} : nil)
}
}
}
private func notificationPeerExceptionEntries(presentationData: PresentationData, notificationSoundList: NotificationSoundList?, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] {
let selectedSound = resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList)
var entries: [NotificationPeerExceptionEntry] = []
var index: Int32 = 0
if state.canRemove {
entries.append(.remove(index: index, theme: presentationData.theme, strings: presentationData.strings))
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))
index += 1
entries.append(.cloudHeader(index: index, text: presentationData.strings.Notifications_TelegramTones))
index += 1
index = 1000
if let notificationSoundList = notificationSoundList {
let cloudSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == nil })
let modernSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == .modern })
let classicSounds = notificationSoundList.sounds.filter({ CloudSoundBuiltinCategory(id: $0.file.fileId.id) == .classic })
for listSound in cloudSounds {
let sound: PeerMessageSound = .cloud(fileId: listSound.file.fileId.id)
if state.removedSounds.contains(where: { $0.id == sound.id }) {
continue
}
entries.append(.sound(index: index, section: .soundCloud, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: selectedSound.id == sound.id, canBeDeleted: true))
index += 1
}
index = 2000
entries.append(.uploadSound(index: index, text: presentationData.strings.Notifications_UploadSound))
index += 1
entries.append(.cloudInfo(index: index, text: presentationData.strings.Notifications_MessageSoundInfo))
index += 1
entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones))
index = 3000
entries.append(.default(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .default, default: state.defaultSound), selected: selectedSound == .default))
index += 1
entries.append(.none(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: .none), selected: selectedSound == .none))
index += 1
for i in 0 ..< modernSounds.count {
let sound: PeerMessageSound = .cloud(fileId: modernSounds[i].file.fileId.id)
entries.append(.sound(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false))
index += 1
}
entries.append(.soundClassicHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_ClassicTones))
index += 1
for i in 0 ..< classicSounds.count {
let sound: PeerMessageSound = .cloud(fileId: classicSounds[i].file.fileId.id)
entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: sound), sound: sound, selected: sound.id == selectedSound.id, canBeDeleted: false))
index += 1
}
}
}
return entries
}
private struct NotificationExceptionPeerState : Equatable {
var canRemove: Bool
var selectedSound: PeerMessageSound
var mode: NotificationPeerExceptionSwitcher
var defaultSound: PeerMessageSound
var displayPreviews: NotificationPeerExceptionSwitcher
var removedSounds: [PeerMessageSound]
init(canRemove: Bool, notifications: TelegramPeerNotificationSettings? = nil) {
self.canRemove = canRemove
if let notifications = notifications {
self.selectedSound = notifications.messageSound
switch notifications.muteState {
case let .muted(until) where until >= Int32.max - 1:
self.mode = .alwaysOff
default:
self.mode = .alwaysOn
}
self.displayPreviews = notifications.displayPreviews == .hide ? .alwaysOff : .alwaysOn
} else {
self.selectedSound = .default
self.mode = .alwaysOn
self.displayPreviews = .alwaysOn
}
self.defaultSound = .default
self.removedSounds = []
}
init(canRemove: Bool, selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound, displayPreviews: NotificationPeerExceptionSwitcher, removedSounds: [PeerMessageSound]) {
self.canRemove = canRemove
self.selectedSound = selectedSound
self.mode = mode
self.defaultSound = defaultSound
self.displayPreviews = displayPreviews
self.removedSounds = removedSounds
}
}
public func notificationPeerExceptionController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: Peer, customTitle: String? = nil, threadId: Int64?, canRemove: Bool, defaultSound: PeerMessageSound, edit: Bool = false, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController {
let initialState = NotificationExceptionPeerState(canRemove: false)
let statePromise = Promise(initialState)
let stateValue = Atomic(value: initialState)
let updateState: ((NotificationExceptionPeerState) -> NotificationExceptionPeerState) -> Void = { f in
statePromise.set(.single(stateValue.modify { f($0) }))
}
var completeImpl: (() -> Void)?
var removeFromExceptionsImpl: (() -> Void)?
var cancelImpl: (() -> Void)?
let playSoundDisposable = MetaDisposable()
var presentFilePicker: (() -> Void)?
var deleteSoundImpl: ((PeerMessageSound, String) -> Void)?
let soundActionDisposable = MetaDisposable()
let arguments = NotificationPeerExceptionArguments(account: context.account, selectSound: { sound in
updateState { state in
let _ = (context.engine.peers.notificationSoundList()
|> take(1)
|> deliverOnMainQueue).start(next: { notificationSoundList in
playSoundDisposable.set(playSound(context: context, notificationSoundList: notificationSoundList, sound: sound, defaultSound: state.defaultSound).start())
})
var state = state
state.selectedSound = sound
return state
}
}, selectMode: { mode in
updateState { state in
var state = state
state.mode = mode
return state
}
}, selectDisplayPreviews: { value in
updateState { state in
var state = state
state.displayPreviews = value
return state
}
}, removeFromExceptions: {
removeFromExceptionsImpl?()
}, complete: {
completeImpl?()
}, cancel: {
cancelImpl?()
}, upload: {
presentFilePicker?()
}, deleteSound: { sound, title in
deleteSoundImpl?(sound, title)
})
statePromise.set(context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id),
EngineDataOptional(threadId.flatMap { TelegramEngine.EngineData.Item.Peer.ThreadNotificationSettings(id: peer.id, threadId: $0) }),
TelegramEngine.EngineData.Item.NotificationSettings.Global()
)
|> map { peerNotificationSettings, threadNotificationSettings, globalNotificationSettings -> NotificationExceptionPeerState in
let effectiveSettings = threadNotificationSettings ?? peerNotificationSettings
var state = NotificationExceptionPeerState(canRemove: canRemove, notifications: effectiveSettings._asNotificationSettings())
state.defaultSound = defaultSound
let _ = stateValue.swap(state)
return state
})
let previousSoundIds = Atomic<Set<Int64>>(value: Set())
let signal = combineLatest(queue: .mainQueue(), (updatedPresentationData?.signal ?? context.sharedContext.presentationData), context.engine.peers.notificationSoundList(), statePromise.get() |> distinctUntilChanged)
|> map { presentationData, notificationSoundList, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
arguments.cancel()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(state.canRemove || edit ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: {
arguments.complete()
})
var updatedSoundIds = Set<Int64>()
if let notificationSoundList = notificationSoundList {
for sound in notificationSoundList.sounds {
if state.removedSounds.contains(.cloud(fileId: sound.file.fileId.id)) {
continue
}
updatedSoundIds.insert(sound.file.fileId.id)
}
}
var animated = false
if previousSoundIds.swap(updatedSoundIds) != updatedSoundIds {
animated = true
}
let titleString: String
if let customTitle = customTitle {
titleString = customTitle
} else {
titleString = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
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, notificationSoundList: notificationSoundList, state: state), style: .blocks, animateChanges: animated)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal |> afterDisposed {
playSoundDisposable.dispose()
soundActionDisposable.dispose()
})
controller.enableInteractiveDismiss = true
completeImpl = { [weak controller] in
controller?.dismiss()
modifiedPeer()
let _ = (context.engine.peers.notificationSoundList()
|> take(1)
|> deliverOnMainQueue).start(next: { notificationSoundList in
updateState { state in
updatePeerSound(peer.id, resolvedNotificationSound(sound: state.selectedSound, notificationSoundList: notificationSoundList))
updatePeerNotificationInterval(peer.id, state.mode == .alwaysOn ? 0 : Int32.max)
updatePeerDisplayPreviews(peer.id, state.displayPreviews == .alwaysOn ? .show : .hide)
return state
}
})
}
removeFromExceptionsImpl = { [weak controller] in
controller?.dismiss()
removePeerFromExceptions()
}
cancelImpl = { [weak controller] in
controller?.dismiss()
}
presentFilePicker = { [weak controller] in
guard let controller = controller else {
return
}
presentCustomNotificationSoundFilePicker(context: context, controller: controller, disposable: soundActionDisposable)
}
deleteSoundImpl = { [weak controller] sound, title in
guard let controller = controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
updateState { state in
var state = state
state.removedSounds.append(sound)
if state.selectedSound.id == sound.id {
state.selectedSound = defaultCloudPeerNotificationSound
}
return state
}
switch sound {
case let .cloud(id):
soundActionDisposable.set((context.engine.peers.deleteNotificationSound(fileId: id)
|> deliverOnMainQueue).start(completed: {
}))
default:
break
}
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
})
], parseMarkdown: true), in: .window(.root))
}
return controller
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "lockedtopic.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,91 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
4.000196 9.799805 m
2.453798 9.799805 1.200195 8.546202 1.200195 6.999804 c
1.200195 5.833116 l
0.726350 5.626386 0.350451 5.243887 0.152241 4.765367 c
0.000000 4.397825 0.000000 3.931883 0.000000 3.000000 c
0.000000 2.068117 0.000000 1.602176 0.152241 1.234633 c
0.355229 0.744577 0.744577 0.355229 1.234633 0.152241 c
1.602175 0.000000 2.068117 0.000000 3.000000 0.000000 c
5.000000 0.000000 l
5.931883 0.000000 6.397825 0.000000 6.765367 0.152241 c
7.255423 0.355229 7.644771 0.744577 7.847759 1.234633 c
8.000000 1.602176 8.000000 2.068117 8.000000 3.000000 c
8.000000 3.931883 8.000000 4.397825 7.847759 4.765367 c
7.649604 5.243756 7.273857 5.626175 6.800196 5.832945 c
6.800196 6.999804 l
6.800196 8.546202 5.546593 9.799805 4.000196 9.799805 c
h
5.200195 5.999938 m
5.200195 6.999804 l
5.200195 7.662546 4.662937 8.199804 4.000196 8.199804 c
3.337454 8.199804 2.800195 7.662546 2.800195 6.999804 c
2.800195 5.999938 l
2.864334 6.000000 2.930885 6.000000 3.000000 6.000000 c
5.000000 6.000000 l
5.069255 6.000000 5.135936 6.000000 5.200195 5.999938 c
h
f*
n
Q
endstream
endobj
3 0 obj
1186
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 8.000000 10.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001276 00000 n
0000001299 00000 n
0000001471 00000 n
0000001545 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1604
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "closetopic_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.335022 3.334961 cm
0.000000 0.000000 0.000000 scn
8.665000 16.000078 m
4.613991 16.000078 1.330000 12.716087 1.330000 8.665078 c
1.330000 4.614070 4.613991 1.330078 8.665000 1.330078 c
12.716008 1.330078 16.000000 4.614070 16.000000 8.665078 c
16.000000 12.716087 12.716008 16.000078 8.665000 16.000078 c
h
0.000000 8.665078 m
0.000000 13.450625 3.879453 17.330078 8.665000 17.330078 c
13.450547 17.330078 17.330002 13.450625 17.330002 8.665078 c
17.330002 3.879531 13.450547 0.000076 8.665000 0.000076 c
3.879453 0.000076 0.000000 3.879531 0.000000 8.665078 c
h
6.265000 13.330078 m
6.632269 13.330078 6.930000 13.032348 6.930000 12.665078 c
6.930000 4.665078 l
6.930000 4.297809 6.632269 4.000078 6.265000 4.000078 c
5.897730 4.000078 5.600000 4.297809 5.600000 4.665078 c
5.600000 12.665078 l
5.600000 13.032348 5.897730 13.330078 6.265000 13.330078 c
h
11.930015 12.665039 m
11.930015 13.032309 11.632284 13.330039 11.265015 13.330039 c
10.897745 13.330039 10.600015 13.032309 10.600015 12.665039 c
10.600015 4.665039 l
10.600015 4.297770 10.897745 4.000039 11.265015 4.000039 c
11.632284 4.000039 11.930015 4.297770 11.930015 4.665039 c
11.930015 12.665039 l
h
f*
n
Q
endstream
endobj
3 0 obj
1242
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001332 00000 n
0000001355 00000 n
0000001528 00000 n
0000001602 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1661
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "restarttopic_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.468018 3.201904 cm
0.000000 0.000000 0.000000 scn
1.196982 8.798096 m
1.196982 12.849104 4.480974 16.133095 8.531982 16.133095 c
12.582991 16.133095 15.866982 12.849104 15.866982 8.798096 c
15.866982 4.747087 12.582991 1.463096 8.531982 1.463096 c
4.480974 1.463096 1.196982 4.747087 1.196982 8.798096 c
h
8.531982 17.463097 m
3.746435 17.463097 -0.133018 13.583643 -0.133018 8.798096 c
-0.133018 4.012548 3.746435 0.133095 8.531982 0.133095 c
13.317530 0.133095 17.196983 4.012548 17.196983 8.798096 c
17.196983 13.583643 13.317530 17.463097 8.531982 17.463097 c
h
6.484432 13.362015 m
6.279433 13.490139 6.021051 13.496923 5.809611 13.379733 c
5.598171 13.262543 5.466982 13.039840 5.466982 12.798096 c
5.466982 4.798096 l
5.466982 4.556352 5.598171 4.333649 5.809611 4.216458 c
6.021051 4.099268 6.279433 4.106052 6.484432 4.234177 c
12.884432 8.234177 l
13.078865 8.355698 13.196982 8.568810 13.196982 8.798096 c
13.196982 9.027381 13.078865 9.240494 12.884432 9.362015 c
6.484432 13.362015 l
h
11.277263 8.798096 m
6.796982 5.997920 l
6.796982 11.598270 l
11.277263 8.798096 l
h
f*
n
Q
endstream
endobj
3 0 obj
1160
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001250 00000 n
0000001273 00000 n
0000001446 00000 n
0000001520 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1579
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "locksettings (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,123 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 3.800000 m
0.000000 4.920105 0.000000 5.480157 0.217987 5.907981 c
0.409734 6.284305 0.715695 6.590266 1.092019 6.782013 c
1.519843 7.000000 2.079895 7.000000 3.200000 7.000000 c
5.800000 7.000000 l
6.920105 7.000000 7.480157 7.000000 7.907981 6.782013 c
8.284306 6.590266 8.590266 6.284305 8.782013 5.907981 c
9.000000 5.480157 9.000000 4.920105 9.000000 3.800000 c
9.000000 3.200000 l
9.000000 2.079895 9.000000 1.519843 8.782013 1.092019 c
8.590266 0.715695 8.284306 0.409734 7.907981 0.217987 c
7.480157 0.000000 6.920105 0.000000 5.800000 0.000000 c
3.200000 0.000000 l
2.079895 0.000000 1.519843 0.000000 1.092019 0.217987 c
0.715695 0.409734 0.409734 0.715695 0.217987 1.092019 c
0.000000 1.519843 0.000000 2.079895 0.000000 3.200000 c
0.000000 3.800000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 2.000000 2.400391 cm
0.000000 0.000000 0.000000 scn
4.200000 1.599609 m
4.200000 1.157782 4.558172 0.799609 5.000000 0.799609 c
5.441828 0.799609 5.800000 1.157782 5.800000 1.599609 c
4.200000 1.599609 l
h
-0.800000 1.599609 m
-0.800000 1.157782 -0.441828 0.799609 -0.000000 0.799609 c
0.441828 0.799609 0.800000 1.157782 0.800000 1.599609 c
-0.800000 1.599609 l
h
4.200000 6.099609 m
4.200000 1.599609 l
5.800000 1.599609 l
5.800000 6.099609 l
4.200000 6.099609 l
h
0.800000 1.599609 m
0.800000 6.099609 l
-0.800000 6.099609 l
-0.800000 1.599609 l
0.800000 1.599609 l
h
2.500000 7.799609 m
3.438884 7.799609 4.200000 7.038493 4.200000 6.099609 c
5.800000 6.099609 l
5.800000 7.922149 4.322540 9.399610 2.500000 9.399610 c
2.500000 7.799609 l
h
2.500000 9.399610 m
0.677460 9.399610 -0.800000 7.922149 -0.800000 6.099609 c
0.800000 6.099609 l
0.800000 7.038493 1.561116 7.799609 2.500000 7.799609 c
2.500000 9.399610 l
h
f
n
Q
endstream
endobj
3 0 obj
1865
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 9.000000 12.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001955 00000 n
0000001978 00000 n
0000002150 00000 n
0000002224 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2283
%%EOF

View File

@ -83,6 +83,7 @@ import TextNodeWithEntities
import EntityKeyboard
import ChatTitleView
import EmojiStatusComponent
import ChatTimerScreen
#if DEBUG
import os.signpost

View File

@ -1161,10 +1161,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
ignoreForward = true
effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [])
displayAuthorInfo = !mergedTop.merged && incoming
} else if let _ = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author {
} else if let adAttribute = item.content.firstMessage.adAttribute, let author = item.content.firstMessage.author {
ignoreForward = true
effectiveAuthor = author
displayAuthorInfo = !mergedTop.merged && incoming
hasAvatar = adAttribute.displayAvatar
} else {
effectiveAuthor = firstMessage.author

View File

@ -356,7 +356,19 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
} else if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == message.id {
isBroadcastChannel = true
}
var hasAvatar = false
if !hasActionMedia && !isBroadcastChannel {
hasAvatar = true
}
if let adAttribute = message.adAttribute {
if adAttribute.displayAvatar {
hasAvatar = adAttribute.displayAvatar
}
}
if hasAvatar {
if let effectiveAuthor = effectiveAuthor {
avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), message: message, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction)
}

View File

@ -7,9 +7,11 @@ import Postbox
import SwiftSignalKit
import TelegramStringFormatting
import ChatPresentationInterfaceState
import TelegramPresentationData
final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
private let textNode: ImmediateTextNode
private var iconView: UIImageView?
private var presentationInterfaceState: ChatPresentationInterfaceState?
@ -41,9 +43,12 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
bannedPermission = nil
}
var iconImage: UIImage?
if let threadData = interfaceState.threadData, threadData.isClosed {
//TODO:localize
self.textNode.attributedText = NSAttributedString(string: "The topic is closed by admin", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
iconImage = PresentationResourcesChat.chatPanelLockIcon(interfaceState.theme)
self.textNode.attributedText = NSAttributedString(string: "The topic is closed by admin", font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
} else if let (untilDate, personal) = bannedPermission {
if personal && untilDate != 0 && untilDate != Int32.max {
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RestrictedTextTimed(stringForFullDate(timestamp: untilDate, strings: interfaceState.strings, dateTimeFormat: interfaceState.dateTimeFormat)).string, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
@ -57,7 +62,24 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
let panelHeight = defaultHeight(metrics: metrics)
let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset - 8.0 * 2.0, height: panelHeight))
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0), y: floor((panelHeight - textSize.height) / 2.0)), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - textSize.width) / 2.0), y: floor((panelHeight - textSize.height) / 2.0)), size: textSize)
self.textNode.frame = textFrame
if let iconImage = iconImage {
let iconView: UIImageView
if let current = self.iconView {
iconView = current
} else {
iconView = UIImageView()
self.iconView = iconView
self.view.addSubview(iconView)
}
iconView.image = iconImage
iconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 4.0 - iconImage.size.width, y: textFrame.minY + UIScreenPixel + floorToScreenPixels((textFrame.height - iconImage.size.height) / 2.0)), size: iconImage.size)
} else if let iconView = self.iconView {
self.iconView = nil
iconView.removeFromSuperview()
}
return panelHeight
}

View File

@ -89,7 +89,7 @@ private enum ChatListSearchEntry: Comparable, Identifiable {
messages: [EngineMessage(message)],
peer: EngineRenderedPeer(peer),
threadInfo: nil,
combinedReadState: readState.flatMap(EnginePeerReadCounters.init),
combinedReadState: readState.flatMap { EnginePeerReadCounters(state: $0, isMuted: false) },
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,

View File

@ -105,7 +105,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen
}
let action = ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), textIcon: { theme in
return !item.isPremium && peer.isPremiumRequired ? generateTintedImage(image: UIImage(bundleImageName: "Notification/SecretLock"), color: theme.contextMenu.badgeInactiveFillColor) : nil
return !item.isPremium && peer.isPremiumRequired ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.badgeInactiveFillColor) : nil
}, action: { _, f in
f(.default)

View File

@ -1,748 +1 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import SolidRoundedButtonNode
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
enum ChatTimerScreenStyle {
case `default`
case media
}
enum ChatTimerScreenMode {
case sendTimer
case autoremove
case mute
}
final class ChatTimerScreen: ViewController {
private var controllerNode: ChatTimerScreenNode {
return self.displayNode as! ChatTimerScreenNode
}
private var animatedIn = false
private let context: AccountContext
private let peerId: PeerId
private let style: ChatTimerScreenStyle
private let mode: ChatTimerScreenMode
private let currentTime: Int32?
private let dismissByTapOutside: Bool
private let completion: (Int32) -> Void
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
self.context = context
self.peerId = peerId
self.style = style
self.mode = mode
self.currentTime = currentTime
self.dismissByTapOutside = dismissByTapOutside
self.completion = completion
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, mode: self.mode, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside)
self.controllerNode.completion = { [weak self] time in
guard let strongSelf = self else {
return
}
strongSelf.completion(time)
strongSelf.dismiss()
}
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.cancel = { [weak self] in
self?.dismiss()
}
}
override public func loadView() {
super.loadView()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: completion)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private protocol TimerPickerView: UIView {
}
private class TimerCustomPickerView: UIPickerView, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
}
private class TimerDatePickerView: UIDatePicker, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
}
private class TimerPickerItemView: UIView {
let valueLabel = UILabel()
let unitLabel = UILabel()
var textColor: UIColor? = nil {
didSet {
self.valueLabel.textColor = self.textColor
self.unitLabel.textColor = self.textColor
}
}
var value: (Int32, String)? {
didSet {
if let (_, string) = self.value {
let components = string.components(separatedBy: " ")
if components.count > 1 {
self.valueLabel.text = components[0]
self.unitLabel.text = components[1]
}
}
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
self.valueLabel.backgroundColor = nil
self.valueLabel.isOpaque = false
self.valueLabel.font = Font.regular(24.0)
self.unitLabel.backgroundColor = nil
self.unitLabel.isOpaque = false
self.unitLabel.font = Font.medium(16.0)
super.init(frame: frame)
self.addSubview(self.valueLabel)
self.addSubview(self.unitLabel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.valueLabel.sizeToFit()
self.unitLabel.sizeToFit()
self.valueLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 20.0 - self.valueLabel.frame.size.width, y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size)
self.unitLabel.frame = CGRect(origin: CGPoint(x: self.frame.width / 2.0 - 12.0, y: floor((self.frame.height - self.unitLabel.frame.height) / 2.0) + 2.0), size: self.unitLabel.frame.size)
}
}
private var timerValues: [Int32] = {
var values: [Int32] = []
for i in 1 ..< 20 {
values.append(Int32(i))
}
for i in 0 ..< 9 {
values.append(Int32(20 + i * 5))
}
return values
}()
class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
private let context: AccountContext
private let controllerStyle: ChatTimerScreenStyle
private var presentationData: PresentationData
private let dismissByTapOutside: Bool
private let mode: ChatTimerScreenMode
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
private let effectNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let titleNode: ASTextNode
private let textNode: ImmediateTextNode
private let cancelButton: HighlightableButtonNode
private let doneButton: SolidRoundedButtonNode
private let disableButton: HighlightableButtonNode
private let disableButtonTitle: ImmediateTextNode
private var initialTime: Int32?
private var pickerView: TimerPickerView?
private let autoremoveTimerValues: [Int32]
private var containerLayout: (ContainerViewLayout, CGFloat)?
var completion: ((Int32) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode, currentTime: Int32?, dismissByTapOutside: Bool) {
self.context = context
self.controllerStyle = style
self.presentationData = presentationData
self.dismissByTapOutside = dismissByTapOutside
self.mode = mode
self.initialTime = currentTime
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 16.0
let backgroundColor: UIColor
let textColor: UIColor
let accentColor: UIColor
let blurStyle: UIBlurEffect.Style
switch style {
case .default:
backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
textColor = self.presentationData.theme.actionSheet.primaryTextColor
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
case .media:
backgroundColor = UIColor(rgb: 0x1c1c1e)
textColor = .white
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
blurStyle = .dark
}
self.effectNode = ASDisplayNode(viewBlock: {
return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
})
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
let title: String
switch self.mode {
case .sendTimer:
title = self.presentationData.strings.Conversation_Timer_Title
case .autoremove:
title = self.presentationData.strings.Conversation_DeleteTimer_SetupTitle
case .mute:
title = self.presentationData.strings.Conversation_Mute_SetupTitle
}
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
self.textNode = ImmediateTextNode()
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal)
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
self.disableButton = HighlightableButtonNode()
self.disableButtonTitle = ImmediateTextNode()
self.disableButton.addSubnode(self.disableButtonTitle)
self.disableButtonTitle.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_DeleteTimer_Disable, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
self.disableButton.isHidden = true
switch self.mode {
case .autoremove:
if self.initialTime != nil {
self.disableButton.isHidden = false
}
default:
break
}
self.autoremoveTimerValues = [
1 * 24 * 60 * 60 as Int32,
2 * 24 * 60 * 60 as Int32,
3 * 24 * 60 * 60 as Int32,
4 * 24 * 60 * 60 as Int32,
5 * 24 * 60 * 60 as Int32,
6 * 24 * 60 * 60 as Int32,
1 * 7 * 24 * 60 * 60 as Int32,
2 * 7 * 24 * 60 * 60 as Int32,
3 * 7 * 24 * 60 * 60 as Int32,
1 * 31 * 24 * 60 * 60 as Int32,
2 * 30 * 24 * 60 * 60 as Int32,
3 * 31 * 24 * 60 * 60 as Int32,
4 * 30 * 24 * 60 * 60 as Int32,
5 * 31 * 24 * 60 * 60 as Int32,
6 * 30 * 24 * 60 * 60 as Int32,
365 * 24 * 60 * 60 as Int32
]
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.backgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.backgroundNode.addSubnode(self.effectNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.textNode)
self.contentContainerNode.addSubnode(self.cancelButton)
self.contentContainerNode.addSubnode(self.doneButton)
self.contentContainerNode.addSubnode(self.disableButton)
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.doneButton.pressed = { [weak self] in
if let strongSelf = self, let pickerView = strongSelf.pickerView {
strongSelf.doneButton.isUserInteractionEnabled = false
if let pickerView = pickerView as? TimerCustomPickerView {
switch strongSelf.mode {
case .sendTimer:
strongSelf.completion?(timerValues[pickerView.selectedRow(inComponent: 0)])
case .autoremove:
let timeInterval = strongSelf.autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]
strongSelf.completion?(Int32(timeInterval))
case .mute:
break
}
} else if let pickerView = pickerView as? TimerDatePickerView {
switch strongSelf.mode {
case .mute:
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
strongSelf.completion?(timeInterval)
default:
break
}
}
}
}
self.disableButton.addTarget(self, action: #selector(self.disableButtonPressed), forControlEvents: .touchUpInside)
self.setupPickerView(currentTime: currentTime)
}
@objc private func disableButtonPressed() {
self.completion?(0)
}
func setupPickerView(currentTime: Int32? = nil) {
if let pickerView = self.pickerView {
pickerView.removeFromSuperview()
}
switch self.mode {
case .sendTimer:
let pickerView = TimerCustomPickerView()
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.dataSource = self
pickerView.delegate = self
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
case .autoremove:
let pickerView = TimerCustomPickerView()
pickerView.dataSource = self
pickerView.delegate = self
pickerView.selectorColor = self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.18)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
if let value = self.initialTime {
var selectedRowIndex = 0
for i in 0 ..< self.autoremoveTimerValues.count {
if self.autoremoveTimerValues[i] <= value {
selectedRowIndex = i
}
}
pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false)
}
case .mute:
let pickerView = TimerDatePickerView()
pickerView.locale = localeWithStrings(self.presentationData.strings)
pickerView.datePickerMode = .dateAndTime
pickerView.minimumDate = Date()
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(self.presentationData.theme.list.itemPrimaryTextColor, forKey: "textColor")
pickerView.setValue(false, forKey: "highlightsToday")
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged)
self.contentContainerNode.view.addSubview(pickerView)
self.pickerView = pickerView
}
}
@objc private func dataPickerChanged() {
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
switch self.mode {
case .sendTimer:
return 1
case .autoremove:
return 1
case .mute:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch self.mode {
case .sendTimer:
return timerValues.count
case .autoremove:
return self.autoremoveTimerValues.count
case .mute:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
switch self.mode {
case .sendTimer:
let value = timerValues[row]
let string = timeIntervalString(strings: self.presentationData.strings, value: value)
if let view = view as? TimerPickerItemView {
view.value = (value, string)
return view
}
let view = TimerPickerItemView()
view.value = (value, string)
view.textColor = .white
return view
case .autoremove:
let itemView: TimerPickerItemView
if let current = view as? TimerPickerItemView {
itemView = current
} else {
itemView = TimerPickerItemView()
itemView.textColor = self.presentationData.theme.list.itemPrimaryTextColor
}
let value = self.autoremoveTimerValues[row]
let string: String
string = timeIntervalString(strings: self.presentationData.strings, value: value)
itemView.value = (value, string)
return itemView
case .mute:
preconditionFailure()
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.dataPickerChanged()
}
func updatePresentationData(_ presentationData: PresentationData) {
let previousTheme = self.presentationData.theme
self.presentationData = presentationData
guard case .default = self.controllerStyle else {
return
}
if let effectView = self.effectNode.view as? UIVisualEffectView {
effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
}
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
self.setupPickerView()
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if self.dismissByTapOutside, case .ended = recognizer.state {
self.cancelButtonPressed()
}
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetBounds = self.bounds
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset)
self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset)
transition.animateView({
self.bounds = targetBounds
self.dimNode.position = dimPosition
})
}
func animateOut(completion: (() -> Void)? = nil) {
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
return self.dimNode.view
}
}
return super.hitTest(point, with: event)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
var buttonOffset: CGFloat = 0.0
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
let titleHeight: CGFloat = 54.0
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight)
if !self.disableButton.isHidden {
buttonOffset += 52.0
}
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset = floor((layout.size.width - width) / 2.0)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
let contentFrame = contentContainerFrame
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
switch self.mode {
case .sendTimer:
break
case .autoremove:
self.doneButton.title = self.presentationData.strings.Conversation_DeleteTimer_Apply
case .mute:
if let pickerView = self.pickerView as? TimerDatePickerView {
let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970))
if timeInterval > 0 {
let timeString = stringForPreciseRelativeTimestamp(strings: self.presentationData.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
self.doneButton.title = self.presentationData.strings.Conversation_Mute_ApplyMuteUntil(timeString).string
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
} else {
self.doneButton.title = self.presentationData.strings.Common_Close
}
}
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight)
transition.updateFrame(node: self.doneButton, frame: doneButtonFrame)
let disableButtonTitleSize = self.disableButtonTitle.updateLayout(CGSize(width: contentFrame.width, height: doneButtonHeight))
let disableButtonFrame = CGRect(origin: CGPoint(x: doneButtonFrame.minX, y: doneButtonFrame.maxY), size: CGSize(width: contentFrame.width - buttonInset * 2.0, height: doneButtonHeight))
transition.updateFrame(node: self.disableButton, frame: disableButtonFrame)
transition.updateFrame(node: self.disableButtonTitle, frame: CGRect(origin: CGPoint(x: floor((disableButtonFrame.width - disableButtonTitleSize.width) / 2.0), y: floor((disableButtonFrame.height - disableButtonTitleSize.height) / 2.0)), size: disableButtonTitleSize))
self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
}
}

View File

@ -454,7 +454,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
} else {
avatarCornerRadius = avatarSize / 2.0
}
self.avatarNode.layer.cornerRadius = avatarCornerRadius
if self.avatarNode.layer.cornerRadius != 0.0 {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius)
} else {
self.avatarNode.layer.cornerRadius = avatarCornerRadius
}
self.avatarNode.layer.masksToBounds = true
self.isFirstAvatarLoading = false
@ -766,7 +770,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
} else {
avatarCornerRadius = avatarSize / 2.0
}
self.avatarNode.layer.cornerRadius = avatarCornerRadius
if self.avatarNode.layer.cornerRadius != 0.0 {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius)
} else {
self.avatarNode.layer.cornerRadius = avatarCornerRadius
}
self.avatarNode.layer.masksToBounds = true
if let item = item {

View File

@ -79,6 +79,8 @@ import EmojiStatusComponent
import ChatTitleView
import ForumCreateTopicScreen
import NotificationExceptionsScreen
import ChatTimerScreen
import NotificationPeerExceptionController
protocol PeerInfoScreenItem: AnyObject {
var id: AnyHashable { get }
@ -3940,7 +3942,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
case .voiceChat:
self.requestCall(isVideo: false, gesture: gesture)
case .mute:
var displayCustomNotificationSettings = false
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
} else {
displayCustomNotificationSettings = true
}
if self.data?.threadData == nil, let channel = self.data?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
displayCustomNotificationSettings = true
}
if !displayCustomNotificationSettings {
let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: self.peerId, threadId: self.chatLocation.threadId, muteInterval: nil).start()
let iconColor: UIColor = .white
@ -4019,7 +4030,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
if !isSoundEnabled {
if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.PeerInfo_ButtonUnmute, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: self.peerId, threadId: self.chatLocation.threadId, muteInterval: nil).start()
let iconColor: UIColor = .white
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: self.presentationData.strings.PeerInfo_TooltipUnmuted, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
})))
} else if !isSoundEnabled {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.PeerInfo_EnableSound, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SoundOn"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@ -4156,25 +4188,40 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
})))
var tip: ContextController.Tip?
tip = nil
if !self.forumTopicNotificationExceptions.isEmpty {
items.append(.separator)
//TODO:localize
let text: String
if self.forumTopicNotificationExceptions.count == 1 {
text = "There is 1 topic that is listed as exception."
text = "There is [1 topic]() that is listed as exception."
} else {
text = "There are \(self.forumTopicNotificationExceptions.count) topics that are listed as exceptions."
text = "There are [\(self.forumTopicNotificationExceptions.count) topics]() that are listed as exceptions."
}
tip = .notificationTopicExceptions(text: text, action: { [weak self] in
guard let self else {
return
}
self.controller?.push(threadNotificationExceptionsScreen(context: self.context, peerId: self.peerId, notificationExceptions: self.forumTopicNotificationExceptions, updated: { [weak self] value in
items.append(.action(ContextMenuActionItem(
text: text,
textLayout: .multiline,
textFont: .small,
parseMarkdown: true,
badge: nil,
icon: { _ in
return nil
},
action: { [weak self] _, f in
guard let self else {
return
}
self.forumTopicNotificationExceptions = value
}))
})
f(.default)
self.controller?.push(threadNotificationExceptionsScreen(context: self.context, peerId: self.peerId, notificationExceptions: self.forumTopicNotificationExceptions, updated: { [weak self] value in
guard let self else {
return
}
self.forumTopicNotificationExceptions = value
}))
}
)))
}
self.view.endEditing(true)

View File

@ -342,8 +342,8 @@ func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TG
bridgeChat.deliveryError = hasFailed
bridgeChat.media = makeBridgeMedia(message: message, strings: strings, filterUnsupportedActions: false)
}
bridgeChat.unread = readState?.isUnread ?? false
bridgeChat.unreadCount = readState?.count ?? 0
bridgeChat.unread = readState?.state.isUnread ?? false
bridgeChat.unreadCount = readState?.state.count ?? 0
var bridgeUsers: [Int64 : TGBridgeUser] = participants
if let bridgeUser = makeBridgeUser(message?.author, presence: nil) {