mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Topics
This commit is contained in:
parent
02b28ee6fc
commit
6847dbb4c3
@ -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 {
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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? {
|
||||
|
@ -106,6 +106,7 @@ swift_library(
|
||||
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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] = []
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import SearchUI
|
||||
import NotificationPeerExceptionController
|
||||
|
||||
public class NotificationExceptionsController: ViewController {
|
||||
private let context: AccountContext
|
||||
|
@ -15,6 +15,7 @@ import PresentationDataUtils
|
||||
import TelegramNotices
|
||||
import NotificationSoundSelectionUI
|
||||
import TelegramStringFormatting
|
||||
import NotificationPeerExceptionController
|
||||
|
||||
private struct CounterTagSettings: OptionSet {
|
||||
var rawValue: Int32
|
||||
|
@ -18,6 +18,7 @@ import NotificationSoundSelectionUI
|
||||
import TelegramStringFormatting
|
||||
import ItemListPeerItem
|
||||
import ItemListPeerActionItem
|
||||
import NotificationPeerExceptionController
|
||||
|
||||
private extension EnginePeer.NotificationSettings.MuteState {
|
||||
var timeInterval: Int32? {
|
||||
|
@ -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):
|
||||
|
@ -17,6 +17,7 @@ import PresentationDataUtils
|
||||
import PhoneNumberFormat
|
||||
import AccountUtils
|
||||
import InstantPageCache
|
||||
import NotificationPeerExceptionController
|
||||
|
||||
enum SettingsSearchableItemIcon {
|
||||
case profile
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: [],
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
27
submodules/TelegramUI/Components/ChatTimerScreen/BUILD
Normal file
27
submodules/TelegramUI/Components/ChatTimerScreen/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import TelegramStringFormatting
|
||||
import ItemListPeerItem
|
||||
import ItemListPeerActionItem
|
||||
import SettingsUI
|
||||
import NotificationPeerExceptionController
|
||||
|
||||
private extension EnginePeer.NotificationSettings.MuteState {
|
||||
var timeInterval: Int32? {
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Chat List/StatusLockIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/StatusLockIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "lockedtopic.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
91
submodules/TelegramUI/Images.xcassets/Chat List/StatusLockIcon.imageset/lockedtopic.pdf
vendored
Normal file
91
submodules/TelegramUI/Images.xcassets/Chat List/StatusLockIcon.imageset/lockedtopic.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Pause.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Pause.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "closetopic_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
93
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Pause.imageset/closetopic_24.pdf
vendored
Normal file
93
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Pause.imageset/closetopic_24.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Play.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Play.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "restarttopic_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
93
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Play.imageset/restarttopic_24.pdf
vendored
Normal file
93
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Play.imageset/restarttopic_24.pdf
vendored
Normal 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
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "locksettings (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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
|
@ -83,6 +83,7 @@ import TextNodeWithEntities
|
||||
import EntityKeyboard
|
||||
import ChatTitleView
|
||||
import EmojiStatusComponent
|
||||
import ChatTimerScreen
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user