Merge branch 'master' into beta

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
#	submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift
#	submodules/TelegramUI/Sources/ChatController.swift
#	versions.json
This commit is contained in:
Isaac 2024-03-01 00:20:22 +04:00
commit d0f8ce7a3a
391 changed files with 29191 additions and 4677 deletions

View file

@ -53,7 +53,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
}
if !isMuted && hasUnread {
signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: .combinedLocation)
signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: index.messageIndex.id.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: .combinedLocation)
|> take(1)
|> map { view -> [INMessage] in
var messages: [INMessage] = []

View file

@ -11218,6 +11218,7 @@ Sorry for the inconvenience.";
"GroupBoost.AdditionalFeatures" = "Additional Features";
"GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features.";
"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features.";
"Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group";
"Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members.";
@ -11315,3 +11316,272 @@ Sorry for the inconvenience.";
"ChannelBoost.Header.Giveaway" = "giveaway";
"ChannelBoost.Header.Features" = "features";
"GroupBoost.Table.Group.VoiceToText" = "Voice-to-Text Conversion";
"GroupBoost.Table.Group.EmojiPack" = "Custom Emojipack";
"ChannelBoost.Header.Boost" = "boost";
"ChannelBoost.Header.Giveaway" = "giveaway";
"ChannelBoost.Header.Features" = "features";
"Premium.FolderTags" = "Folder Tags";
"Premium.FolderTagsInfo" = "Add colorful labels to chats for faster access with Telegram Premium.";
"Premium.FolderTagsStandaloneInfo" = "Add colorful labels to chats for faster access with Telegram Premium.";
"Premium.FolderTags.Proceed" = "About Telegram Premium";
"Premium.Business" = "Telegram Business";
"Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies.";
"Premium.Business.Location.Title" = "Location";
"Premium.Business.Location.Text" = "Display the location of your business on your account.";
"Premium.Business.Hours.Title" = "Opening Hours";
"Premium.Business.Hours.Text" = "Show to your customers when you are
open for business.";
"Premium.Business.Replies.Title" = "Quick Replies";
"Premium.Business.Replies.Text" = "Set up shortcuts with rich text and media
to respond to messages faster.";
"Premium.Business.Greetings.Title" = "Greeting Messages";
"Premium.Business.Greetings.Text" = "Create greetings that will be automatically sent to new customers.";
"Premium.Business.Away.Title" = "Away Messages";
"Premium.Business.Away.Text" = "Define messages that are automatically sent when you are off.";
"Premium.Business.Chatbots.Title" = "Chatbots";
"Premium.Business.Chatbots.Text" = "Add any third party chatbots that will process customer interactions.";
"Business.Title" = "Telegram Business";
"Business.Description" = "Turn your account into a **business page** with these additional features.";
"Business.SubscribedDescription" = "You have now unlocked these additional business features.";
"Business.Location" = "Location";
"Business.OpeningHours" = "Opening Hours";
"Business.QuickReplies" = "Quick Replies";
"Business.GreetingMessages" = "Greeting Messages";
"Business.AwayMessages" = "Away Messages";
"Business.Chatbots" = "Chatbots";
"Business.LocationInfo" = "Display the location of your business on your account.";
"Business.OpeningHoursInfo" = "Show to your customers when you are open for business.";
"Business.QuickRepliesInfo" = "Set up shortcuts with rich text and media to respond to messages faster.";
"Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers.";
"Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off.";
"Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions.";
"Business.MoreFeaturesTitle" = "MORE BUSINESS FEATURES";
"Business.MoreFeaturesInfo" = "Check this section later for new business features.";
"Business.SetEmojiStatus" = "Set Emoji Status";
"Business.SetEmojiStatusInfo" = "Display the current status of your business next to your name.";
"Business.TagYourChats" = "Tag Your Chats";
"Business.TagYourChatsInfo" = "Add colorful labels to chats for faster access in chat list.";
"Business.AddPost" = "Add a Post";
"Business.AddPostInfo" = "Publish photos and videos of your goods or services on your page.";
"StoryList.SavedEmptyPosts.Title" = "No posts yet...";
"StoryList.SavedEmptyPosts.Text" = "Publish photos and videos to display on your profile page.";
"StoryList.SavedAddAction" = "Add a Post";
"PeerInfo.Channel.Boost" = "Boost Channel";
"ChannelBoost.Title" = "Boost Channel";
"ChannelBoost.Info" = "Subscribers of your channel can **boost** it so that it **levels up** and gets **exclusive features**.";
"Conversation.BoostToUnrestrictText" = "Boost this group to send messages";
"Settings.Business" = "Telegram Business";
"Story.Editor.DiscardText" = "If you go back now, you will lose any changes you made.";
"Premium.Business.Description" = "Turn your account to a business page with these additional features.";
"Attachment.Reply" = "Reply";
"ChatList.ItemMoreMessagesFormat_1" = "+1 MORE";
"ChatList.ItemMoreMessagesFormat_any" = "+%d MORE";
"ChatList.ItemMenuEdit" = "Edit";
"ChatList.ItemMenuDelete" = "Delete";
"ChatList.PeerTypeNonContact" = "non-contact";
"ChatListFilter.TagLabelNoTag" = "NO TAG";
"ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED";
"ChatListFilter.TagSectionTitle" = "FOLDER COLOR";
"ChatListFilter.TagSectionFooter" = "This color will be used for the folder's tag in the chat list";
"ChatListFilter.TagPremiumRequiredTooltipText" = "Subscribe to **Telegram Premium** to select folder color.";
"ChatListFilterList.ShowTags" = "Show Folder Tags";
"ChatListFilterList.ShowTagsFooter" = "Display folder names for each chat in the chat list.";
"PeerInfo.BusinessHours.DayOpen24h" = "open 24 hours";
"PeerInfo.BusinessHours.DayClosed" = "closed";
"PeerInfo.BusinessHours.StatusOpen" = "Open";
"PeerInfo.BusinessHours.StatusClosed" = "Closed";
"PeerInfo.BusinessHours.StatusOpensInMinutes_1" = "Opens in one minute";
"PeerInfo.BusinessHours.StatusOpensInMinutes_any" = "Opens in %d minutes";
"PeerInfo.BusinessHours.StatusOpensInHours_1" = "Opens in one hour";
"PeerInfo.BusinessHours.StatusOpensInHours_any" = "Opens in %d hours";
"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@";
"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time";
"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time";
"PeerInfo.BusinessHours.Label" = "business hours";
"PeerInfo.Location.Label" = "location";
"QuickReplies.EmptyState.Title" = "No Quick Replies";
"QuickReplies.EmptyState.Text" = "Set up shortcuts with rich text and media to respond to messages faster.";
"QuickReplies.EmptyState.AddButton" = "Add Quick Reply";
"Chat.CommandList.EditQuickReplies" = "Edit Quick Replies";
"Conversation.EditingQuickReplyPanelTitle" = "Edit Quick Reply";
"Chat.Placeholder.QuickReply" = "Add quick reply..";
"Chat.Placeholder.GreetingMessage" = "Add greeting message...";
"Chat.Placeholder.AwayMessage" = "Add away message...";
"Chat.QuickReplyMessageLimitReachedText_1" = "Limit of %d message reached";
"Chat.QuickReplyMessageLimitReachedText_any" = "Limit of %d messages reached";
"Chat.EmptyState.QuickReply.Title" = "New Quick Reply";
"Chat.EmptyState.QuickReply.Text1" = "· Enter a message below that will be sent in chats when you type \"**/%@\"**.";
"Chat.EmptyState.QuickReply.Text2" = "· You can access Quick Replies in any chat by typing \"/\" or using the Attachment menu.";
"EmptyState.GreetingMessage.Title" = "New Greeting Message";
"EmptyState.GreetingMessage.Text" = "Create greetings that will be automatically sent to new customers";
"EmptyState.AwayMessage.Title" = "New Away Message";
"EmptyState.AwayMessage.Text" = "Add messages that are automatically sent when you are off.";
"QuickReply.Title" = "Quick Replies";
"QuickReply.SelectedTitle_1" = "%d Selected";
"QuickReply.SelectedTitle_any" = "%d Selected";
"QuickReply.ShortcutPlaceholder" = "Shortcut";
"QuickReply.CreateShortcutTitle" = "New Quick Reply";
"QuickReply.CreateShortcutText" = "Add a shortcut for your quick reply.";
"QuickReply.EditShortcutTitle" = "Edit Shortcut";
"QuickReply.EditShortcutText" = "Add a new name for your shortcut.";
"QuickReply.ShortcutExistsInlineError" = "Shortcut with that name already exists";
"QuickReply.ChatRemoveGeneric.Title" = "Remove Shortcut";
"QuickReply.ChatRemoveGeneric.Text" = "You didn't create a quick reply message. Exiting will remove the shortcut.";
"QuickReply.ChatRemoveGreetingMessage.Title" = "Remove Greeting Message";
"QuickReply.ChatRemoveGreetingMessage.Text" = "You didn't create a greeting message. Exiting will remove it.";
"QuickReply.ChatRemoveAwayMessage.Title" = "Remove Away Message";
"QuickReply.ChatRemoveAwayMessage.Text" = "You didn't create an away message. Exiting will remove it.";
"QuickReply.ChatRemoveGeneric.DeleteAction" = "Delete";
"QuickReply.TitleGreetingMessage" = "Greeting Message";
"QuickReply.TitleAwayMessage" = "Away Message";
"QuickReply.InlineCreateAction" = "New Quick Reply";
"QuickReply.DeleteConfirmationSingle" = "Delete Quick Reply";
"QuickReply.DeleteConfirmationMultiple" = "Delete Quick Replies";
"QuickReply.DeleteAction_1" = "Delete 1 Quick Reply";
"QuickReply.DeleteAction_any" = "Delete %d Quick Replies";
"TimeZoneSelection.Title" = "Time Zone";
"BusinessMessageSetup.TitleGreetingMessage" = "Greeting Message";
"BusinessMessageSetup.TextGreetingMessage" = "Greet customers when they message you the first time or after a period of no activity.";
"BusinessMessageSetup.TitleAwayMessage" = "Away Message";
"BusinessMessageSetup.TextAwayMessage" = "Automatically reply with a message when you are away.";
"BusinessMessageSetup.ToggleGreetingMessage" = "Send Greeting Message";
"BusinessMessageSetup.ToggleAwayMessage" = "Send Away Message";
"BusinessMessageSetup.CreateGreetingMessage" = "Create a Greeting Message";
"BusinessMessageSetup.CreateAwayMessage" = "Create an Away Message";
"BusinessMessageSetup.GreetingMessageSectionHeader" = "GREETING MESSAGE";
"BusinessMessageSetup.AwayMessageSectionHeader" = "AWAY MESSAGE";
"BusinessMessageSetup.ScheduleSectionHeader" = "SCHEDULE";
"BusinessMessageSetup.ScheduleAlways" = "Always Send";
"BusinessMessageSetup.ScheduleOutsideBusinessHours" = "Outside of Business Hours";
"BusinessMessageSetup.ScheduleCustom" = "Custom Schedule";
"BusinessMessageSetup.ScheduleStartTime" = "Start Time";
"BusinessMessageSetup.ScheduleEndTime" = "End Time";
"BusinessMessageSetup.ScheduleTimePlaceholder" = "Set";
"BusinessMessageSetup.SendWhenOffline" = "Only if Offline";
"BusinessMessageSetup.SendWhenOfflineFooter" = "Don't send the away message if you've recently been online.";
"BusinessMessageSetup.ErrorNoRecipients.Text" = "No recipients selected. Reset?";
"BusinessMessageSetup.ErrorNoRecipients.ResetAction" = "Reset";
"BusinessMessageSetup.ErrorScheduleEndTimeBeforeStartTime.Text" = "Custom schedule end time must be larger than start time.";
"BusinessMessageSetup.ErrorScheduleTimeMissing.Text" = "Custom schedule time is missing.";
"BusinessMessageSetup.ErrorScheduleStartTimeMissing.Text" = "Custom schedule start time is missing.";
"BusinessMessageSetup.ErrorScheduleEndTimeMissing.Text" = "Custom schedule end time is missing.";
"BusinessMessageSetup.ErrorScheduleTime.ResetAction" = "Reset";
"BusinessMessageSetup.RecipientsSectionHeader" = "RECIPIENTS";
"BusinessMessageSetup.RecipientsOptionAllExcept" = "All 1-to-1 Chats Except...";
"BusinessMessageSetup.RecipientsOptionOnly" = "Only Selected Chats";
"BusinessMessageSetup.Recipients.AddExclude" = "Exclude Chats...";
"BusinessMessageSetup.Recipients.AddInclude" = "Include Chats...";
"BusinessMessageSetup.Recipients.CategoryExistingChats" = "Existing Chats";
"BusinessMessageSetup.Recipients.CategoryNewChats" = "New Chats";
"BusinessMessageSetup.Recipients.CategoryContacts" = "Contacts";
"BusinessMessageSetup.Recipients.CategoryNonContacts" = "Non-Contacts";
"BusinessMessageSetup.Recipients.IncludeSearchTitle" = "Include Chats";
"BusinessMessageSetup.Recipients.ExcludeSearchTitle" = "Exclude Chats";
"BusinessMessageSetup.Recipients.IncludedSectionHeader" = "INCLUDED CHATS";
"BusinessMessageSetup.Recipients.ExcludedSectionHeader" = "EXCLUDED CHATS";
"BusinessMessageSetup.Recipients.GreetingMessageFooter" = "Choose chats or entire chat categories for sending a greeting message.";
"BusinessMessageSetup.Recipients.AwayMessageFooter" = "Choose chats or entire chat categories for sending an away message.";
"BusinessMessageSetup.InactivitySectionHeader" = "PERIOD OF NO ACTIVITY";
"BusinessMessageSetup.InactivitySectionFooter" = "Choose how many days should pass after your last interaction with a recipient to send them the greeting in response to their message.";
"BusinessHoursSetup.Title" = "Business Hours";
"BusinessHoursSetup.Text" = "Turn this on to show your opening hours schedule to your customers.";
"BusinessHoursSetup.MainToggle" = "Show Business Hours";
"BusinessHoursSetup.DaySwitch" = "Open On This Day";
"BusinessHoursSetup.DayIntervalStart" = "Opening time";
"BusinessHoursSetup.DayIntervalEnd" = "Closing time";
"BusinessHoursSetup.DayIntervalRemove" = "Remove";
"BusinessHoursSetup.AddSectionFooter" = "Specify your working hours during the day.";
"BusinessHoursSetup.AddAction" = "Add a Set of Hours";
"BusinessHoursSetup.ErrorIntersectingHours.Text" = "Business hours are intersecting. Reset?";
"BusinessHoursSetup.ErrorIntersectingHours.ResetAction" = "Reset";
"BusinessHoursSetup.ErrorIntersectingDays.Text" = "Business hours are intersecting. Reset?";
"BusinessHoursSetup.ErrorIntersectingDays.ResetAction" = "Reset";
"BusinessHoursSetup.DayOpen24h" = "Open 24 Hours";
"BusinessHoursSetup.DayClosed" = "Closed";
"BusinessHoursSetup.DaysSectionTitle" = "BUSINESS HOURS";
"BusinessHoursSetup.TimeZone" = "Time Zone";
"BusinessLocationSetup.Title" = "Location";
"BusinessLocationSetup.Text" = "Display the location of your business on your account.";
"BusinessLocationSetup.AddressPlaceholder" = "Enter Address";
"BusinessLocationSetup.SetLocationOnMap" = "Set Location on Map";
"BusinessLocationSetup.DeleteLocation" = "Delete Location";
"BusinessLocationSetup.ErrorAddressEmpty.Text" = "Address can't be empty.";
"BusinessLocationSetup.ErrorAddressEmpty.ResetAction" = "Delete";
"ChatbotSetup.Title" = "Chatbots";
"ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()";
"ChatbotSetup.TextLink" = "https://telegram.org";
"ChatbotSetup.BotSearchPlaceholder" = "Bot Username";
"ChatbotSetup.BotSectionFooter" = "Enter the username or URL of the Telegram bot that you want to automatically process your chats.";
"ChatbotSetup.RecipientsSectionHeader" = "CHATS ACCESSIBLE FOR THE BOT";
"ChatbotSetup.Recipients.ExcludedSectionFooter" = "Select chats or entire chat categories which the bot **WILL NOT** have access to.";
"ChatbotSetup.Recipients.IncludedSectionFooter" = "Select chats or entire chat categories which the bot **WILL** have access to.";
"ChatbotSetup.PermissionsSectionHeader" = "BOT PERMISSIONS";
"ChatbotSetup.PermissionsSectionFooter" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.";
"ChatbotSetup.Permission.ReplyToMessages" = "Reply to Messages";
"ChatbotSetup.BotAddAction" = "ADD";
"ChatbotSetup.BotNotFoundStatus" = "Chatbot not found";

View file

@ -395,13 +395,13 @@ public enum ChatSearchDomain: Equatable {
public enum ChatLocation: Equatable {
case peer(id: PeerId)
case replyThread(message: ChatReplyThreadMessage)
case feed(id: Int32)
case customChatContents
}
public extension ChatLocation {
var normalized: ChatLocation {
switch self {
case .peer, .feed:
case .peer, .customChatContents:
return self
case let .replyThread(message):
return .replyThread(message: message.normalized)
@ -847,6 +847,15 @@ public protocol TelegramRootControllerInterface: NavigationController {
func openSettings()
}
public protocol QuickReplySetupScreenInitialData: AnyObject {
}
public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject {
}
public protocol ChatbotSetupScreenInitialData: AnyObject {
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }
@ -932,8 +941,16 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController
func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal<ChatbotSetupScreenInitialData, NoError>
func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController
func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController
func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError>
func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController
func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError>
func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>

View file

@ -744,6 +744,7 @@ public enum ChatControllerSubject: Equatable {
case scheduledMessages
case pinnedMessages(id: EngineMessage.Id?)
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo)
case customChatContents(contents: ChatCustomContentsProtocol)
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
switch lhs {
@ -771,6 +772,12 @@ public enum ChatControllerSubject: Equatable {
} else {
return false
}
case let .customChatContents(lhsValue):
if case let .customChatContents(rhsValue) = rhs, lhsValue === rhsValue {
return true
} else {
return false
}
}
}
@ -796,11 +803,30 @@ public enum ChatControllerPresentationMode: Equatable {
case inline(NavigationController?)
}
public enum ChatInputTextCommand: Equatable {
case command(PeerCommand)
case shortcut(ShortcutMessageList.Item)
}
public struct ChatInputQueryCommandsResult: Equatable {
public var commands: [ChatInputTextCommand]
public var accountPeer: EnginePeer?
public var hasShortcuts: Bool
public var query: String
public init(commands: [ChatInputTextCommand], accountPeer: EnginePeer?, hasShortcuts: Bool, query: String) {
self.commands = commands
self.accountPeer = accountPeer
self.hasShortcuts = hasShortcuts
self.query = query
}
}
public enum ChatPresentationInputQueryResult: Equatable {
case stickers([FoundStickerItem])
case hashtags([String])
case mentions([EnginePeer])
case commands([PeerCommand])
case commands(ChatInputQueryCommandsResult)
case emojis([(String, TelegramMediaFile?, String)], NSRange)
case contextRequestResult(EnginePeer?, ChatContextResultCollection?)
@ -1050,7 +1076,30 @@ public enum ChatHistoryListSource {
}
case `default`
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: Quote?, loadMore: (() -> Void)?)
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?)
case customView(historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError>)
}
public enum ChatQuickReplyShortcutType {
case generic
case greeting
case away
}
public enum ChatCustomContentsKind: Equatable {
case quickReplyMessageInput(shortcut: String, shortcutType: ChatQuickReplyShortcutType)
}
public protocol ChatCustomContentsProtocol: AnyObject {
var kind: ChatCustomContentsKind { get }
var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { get }
var messageLimit: Int? { get }
func enqueueMessages(messages: [EnqueueMessage])
func deleteMessages(ids: [EngineMessage.Id])
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool)
func quickReplyUpdateShortcut(value: String)
}
public enum ChatHistoryListDisplayHeaders {
@ -1069,7 +1118,7 @@ public protocol ChatControllerInteractionProtocol: AnyObject {
public enum ChatHistoryNodeHistoryState: Equatable {
case loading
case loaded(isEmpty: Bool)
case loaded(isEmpty: Bool, hasReachedLimits: Bool)
}
public protocol ChatHistoryListNode: ListView {

View file

@ -45,6 +45,7 @@ public enum ContactMultiselectionControllerMode {
public var chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool
public var displayPresence: Bool
public var onlyUsers: Bool
public init(
title: String,
@ -53,7 +54,8 @@ public enum ContactMultiselectionControllerMode {
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false,
displayPresence: Bool = false
displayPresence: Bool = false,
onlyUsers: Bool = false
) {
self.title = title
self.searchPlaceholder = searchPlaceholder
@ -62,6 +64,7 @@ public enum ContactMultiselectionControllerMode {
self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout
self.displayPresence = displayPresence
self.onlyUsers = onlyUsers
}
}

View file

@ -36,8 +36,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
return .peer(peerId)
case let .replyThread(replyThreaMessage):
return .peer(replyThreaMessage.peerId)
case let .feed(id):
return .feed(id)
case .customChatContents:
return .custom
}
case let .singleMessage(id):
return .peer(id.peerId)

View file

@ -80,6 +80,7 @@ public class PeerNameColors: Equatable {
colors: defaultSingleColors,
darkColors: [:],
displayOrder: [5, 3, 1, 0, 2, 4, 6],
chatFolderTagDisplayOrder: [5, 3, 1, 0, 2, 4, 6],
profileColors: [:],
profileDarkColors: [:],
profilePaletteColors: [:],
@ -97,6 +98,8 @@ public class PeerNameColors: Equatable {
public let darkColors: [Int32: Colors]
public let displayOrder: [Int32]
public let chatFolderTagDisplayOrder: [Int32]
public let profileColors: [Int32: Colors]
public let profileDarkColors: [Int32: Colors]
public let profilePaletteColors: [Int32: Colors]
@ -119,6 +122,16 @@ public class PeerNameColors: Equatable {
}
}
public func getChatFolderTag(_ color: PeerNameColor, dark: Bool = false) -> Colors {
if dark, let colors = self.darkColors[color.rawValue] {
return colors
} else if let colors = self.colors[color.rawValue] {
return colors
} else {
return PeerNameColors.defaultSingleColors[5]!
}
}
public func getProfile(_ color: PeerNameColor, dark: Bool = false, subject: Subject = .background) -> Colors {
switch subject {
case .background:
@ -152,6 +165,7 @@ public class PeerNameColors: Equatable {
colors: [Int32: Colors],
darkColors: [Int32: Colors],
displayOrder: [Int32],
chatFolderTagDisplayOrder: [Int32],
profileColors: [Int32: Colors],
profileDarkColors: [Int32: Colors],
profilePaletteColors: [Int32: Colors],
@ -166,6 +180,7 @@ public class PeerNameColors: Equatable {
self.colors = colors
self.darkColors = darkColors
self.displayOrder = displayOrder
self.chatFolderTagDisplayOrder = chatFolderTagDisplayOrder
self.profileColors = profileColors
self.profileDarkColors = profileDarkColors
self.profilePaletteColors = profilePaletteColors
@ -257,6 +272,7 @@ public class PeerNameColors: Equatable {
colors: colors,
darkColors: darkColors,
displayOrder: displayOrder,
chatFolderTagDisplayOrder: PeerNameColors.defaultValue.chatFolderTagDisplayOrder,
profileColors: profileColors,
profileDarkColors: profileDarkColors,
profilePaletteColors: profilePaletteColors,
@ -280,6 +296,9 @@ public class PeerNameColors: Equatable {
if lhs.displayOrder != rhs.displayOrder {
return false
}
if lhs.chatFolderTagDisplayOrder != rhs.chatFolderTagDisplayOrder {
return false
}
if lhs.profileColors != rhs.profileColors {
return false
}

View file

@ -38,6 +38,7 @@ public enum PremiumIntroSource {
case presence
case readTime
case messageTags
case folderTags
}
public enum PremiumGiftSource: Equatable {
@ -70,6 +71,14 @@ public enum PremiumDemoSubject {
case messageTags
case lastSeen
case messagePrivacy
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum PremiumLimitSubject {

View file

@ -19,6 +19,7 @@ public enum AttachmentButtonType: Equatable {
case gallery
case file
case location
case quickReply
case contact
case poll
case app(AttachMenuBot)
@ -27,54 +28,60 @@ public enum AttachmentButtonType: Equatable {
public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool {
switch lhs {
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .quickReply:
if case .quickReply = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
}
}
}

View file

@ -217,6 +217,9 @@ private final class AttachButtonComponent: CombinedComponent {
name = ""
imageName = ""
imageFile = nil
case .quickReply:
name = strings.Attachment_Reply
imageName = "Chat/Attach Menu/Reply"
}
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
@ -821,6 +824,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}, sendContextResult: { _, _, _, _ in
return false
}, sendBotCommand: { _, _ in
}, sendShortcut: { _ in
}, openEditShortcuts: {
}, sendBotStart: { _ in
}, botSwitchChatWithPayload: { _, _ in
}, beginMediaRecording: { _ in
@ -1181,6 +1186,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
accessibilityTitle = bot.shortName
case .standalone:
accessibilityTitle = ""
case .quickReply:
accessibilityTitle = self.presentationData.strings.Attachment_Reply
}
buttonView.isAccessibilityElement = true
buttonView.accessibilityLabel = accessibilityTitle

View file

@ -1296,18 +1296,9 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
}
public static func defaultCountryCode() -> Int32 {
var countryId: String? = nil
let networkInfo = CTTelephonyNetworkInfo()
if let carrier = networkInfo.serviceSubscriberCellularProviders?.values.first {
countryId = carrier.isoCountryCode
}
if countryId == nil {
countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
}
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCode: Int32 = 1
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {

View file

@ -618,6 +618,7 @@ public class BrowserScreen: ViewController {
bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom,
right: layout.safeInsets.right
),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,

View file

@ -101,6 +101,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen",
"//submodules/TelegramUI/Components/Settings/NewSessionInfoScreen",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

View file

@ -5617,9 +5617,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(chatListFilterPresetListController(context: self.context, mode: .modal, dismissed: { [weak self] in
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in
self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
}))
})
navigationController.pushViewController(controller)
}
}

View file

@ -1327,6 +1327,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
statusBarHeight: layout.statusBarHeight ?? 0.0,
sideInset: layout.safeInsets.left,
isSearchActive: self.isSearchDisplayControllerActive,
isSearchEnabled: true,
primaryContent: headerContent?.primaryContent,
secondaryContent: headerContent?.secondaryContent,
secondaryTransition: self.inlineStackContainerTransitionFraction,

View file

@ -44,6 +44,7 @@ private final class ChatListFilterPresetControllerArguments {
let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void
let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
let updateTagColor: (PeerNameColor?) -> Void
let openTagColorPremium: () -> Void
init(
context: AccountContext,
@ -63,7 +64,8 @@ private final class ChatListFilterPresetControllerArguments {
removeLink: @escaping (ExportedChatFolderLink) -> Void,
linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void,
peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void,
updateTagColor: @escaping (PeerNameColor?) -> Void
updateTagColor: @escaping (PeerNameColor?) -> Void,
openTagColorPremium: @escaping () -> Void
) {
self.context = context
self.updateState = updateState
@ -83,6 +85,7 @@ private final class ChatListFilterPresetControllerArguments {
self.linkContextAction = linkContextAction
self.peerContextAction = peerContextAction
self.updateTagColor = updateTagColor
self.openTagColorPremium = openTagColorPremium
}
}
@ -231,8 +234,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case inviteLinkCreate(hasLinks: Bool)
case inviteLink(Int, ExportedChatFolderLink)
case inviteLinkInfo(text: String)
case tagColorHeader(name: String, color: PeerNameColors.Colors)
case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?)
case tagColorHeader(name: String, color: PeerNameColors.Colors?, isPremium: Bool)
case tagColor(colors: PeerNameColors, currentColor: PeerNameColor?, isPremium: Bool)
case tagColorFooter
var section: ItemListSectionId {
@ -458,26 +461,43 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSection(.exclude)
})
case let .tagColorHeader(name, color):
//TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: "FOLDER COLOR", badge: name.uppercased(), badgeStyle: ItemListSectionHeaderItem.BadgeStyle(
background: color.main.withMultipliedAlpha(0.1),
foreground: color.main
), sectionId: self.section)
case let .tagColor(colors, color):
case let .tagColorHeader(name, color, isPremium):
var badge: String?
var badgeStyle: ItemListSectionHeaderItem.BadgeStyle?
var accessoryText: ItemListSectionHeaderAccessoryText?
if isPremium {
if let color {
badge = name.uppercased()
badgeStyle = ItemListSectionHeaderItem.BadgeStyle(
background: color.main.withMultipliedAlpha(0.1),
foreground: color.main
)
} else {
accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelNoTag, color: .generic)
}
} else if color != nil {
accessoryText = ItemListSectionHeaderAccessoryText(value: presentationData.strings.ChatListFilter_TagLabelPremiumExpired, color: .generic)
}
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_TagSectionTitle, badge: badge, badgeStyle: badgeStyle, accessoryText: accessoryText, sectionId: self.section)
case let .tagColor(colors, color, isPremium):
return PeerNameColorItem(
theme: presentationData.theme,
colors: colors,
isProfile: true,
currentColor: color,
mode: .folderTag,
displayEmptyColor: true,
currentColor: isPremium ? color : nil,
isLocked: !isPremium,
updated: { color in
arguments.updateTagColor(color)
if isPremium {
arguments.updateTagColor(color)
} else {
arguments.openTagColorPremium()
}
},
sectionId: self.section
)
case .tagColorFooter:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .plain("This color will be used for the folder's tag in the chat list"), sectionId: self.section)
return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.ChatListFilter_TagSectionFooter), sectionId: self.section)
case .inviteLinkHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_SectionShare, badge: nil, sectionId: self.section)
case let .inviteLinkCreate(hasLinks):
@ -502,6 +522,7 @@ private struct ChatListFilterPresetControllerState: Equatable {
var name: String
var changedName: Bool
var color: PeerNameColor?
var colorUpdated: Bool = false
var includeCategories: ChatListFilterPeerCategories
var excludeMuted: Bool
var excludeRead: Bool
@ -616,12 +637,20 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
}
/*let tagColor = state.color ?? .blue
let resolvedColor = context.peerNameColors.getProfile(tagColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette)
let tagColor: PeerNameColor?
if state.colorUpdated {
tagColor = state.color
} else {
tagColor = state.color ?? .blue
}
var resolvedColor: PeerNameColors.Colors?
if let tagColor {
resolvedColor = context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance)
}
entries.append(.tagColorHeader(name: state.name, color: resolvedColor))
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor))
entries.append(.tagColorFooter)*/
entries.append(.tagColorHeader(name: state.name, color: resolvedColor, isPremium: isPremium))
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor, isPremium: isPremium))
entries.append(.tagColorFooter)
var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty {
@ -1018,7 +1047,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
} else {
initialName = ""
}
let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: [])
var initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, color: initialPreset?.data?.color, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: [])
initialState.colorUpdated = true
let updatedCurrentPreset: Signal<ChatListFilter?, NoError>
if let initialPreset {
@ -1543,8 +1573,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
updateState { state in
var state = state
state.color = color
state.colorUpdated = true
return state
}
},
openTagColorPremium: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
}
)

View file

@ -22,8 +22,9 @@ private final class ChatListFilterPresetListControllerArguments {
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
let removePreset: (Int32) -> Void
let updateDisplayTags: (Bool) -> Void
let updateDisplayTagsLocked: () -> Void
init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void) {
init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) {
self.context = context
self.addSuggestedPressed = addSuggestedPressed
self.openPreset = openPreset
@ -31,6 +32,7 @@ private final class ChatListFilterPresetListControllerArguments {
self.setItemWithRevealedOptions = setItemWithRevealedOptions
self.removePreset = removePreset
self.updateDisplayTags = updateDisplayTags
self.updateDisplayTagsLocked = updateDisplayTagsLocked
}
}
@ -41,6 +43,19 @@ private enum ChatListFilterPresetListSection: Int32 {
case tags
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChatListFilterPresetListEntryTag, self == other {
return true
} else {
return false
}
}
}
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
@ -80,10 +95,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData)
case suggestedAddCustom(String)
case listHeader(String)
case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool)
case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool)
case addItem(text: String, isEditing: Bool)
case listFooter(String)
case displayTags(Bool)
case displayTags(Bool?)
case displayTagsFooter
var section: ItemListSectionId {
@ -107,7 +122,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
return 100
case .addItem:
return 101
case let .preset(index, _, _, _, _, _, _, _, _):
case let .preset(index, _, _, _, _, _, _, _, _, _):
return 102 + index.value
case .listFooter:
return 1001
@ -136,7 +151,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
return .suggestedAddCustom
case .listHeader:
return .listHeader
case let .preset(_, _, _, preset, _, _, _, _, _):
case let .preset(_, _, _, preset, _, _, _, _, _, _):
return .preset(preset.id)
case .addItem:
return .addItem
@ -170,8 +185,16 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
})
case let .listHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled, displayTags):
var resolvedColor: UIColor?
if displayTags, case let .filter(_, _, _, data) = preset {
let tagColor = data.color
if let tagColor {
resolvedColor = arguments.context.peerNameColors.getChatFolderTag(tagColor, dark: presentationData.theme.overallDarkAppearance).main
}
}
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
if isDisabled {
arguments.addNew()
} else {
@ -189,13 +212,17 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case let .listFooter(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .displayTags(value):
//TODO:localize
return ItemListSwitchItem(presentationData: presentationData, title: "Show Folder Tags", value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateDisplayTags(value)
})
return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ChatListFilterList_ShowTags, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in
if value != nil {
arguments.updateDisplayTags(updatedValue)
} else {
arguments.updateDisplayTagsLocked()
}
}, activatedWhileDisabled: {
arguments.updateDisplayTagsLocked()
}, tag: ChatListFilterPresetListEntryTag.displayTags)
case .displayTagsFooter:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section)
return ItemListTextItem(presentationData: presentationData, text: .plain(presentationData.strings.ChatListFilterList_ShowTagsFooter), sectionId: self.section)
}
}
}
@ -249,15 +276,20 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
entries.append(.addItem(text: presentationData.strings.ChatListFilterList_CreateFolder, isEditing: state.isEditing))
var effectiveDisplayTags: Bool?
if isPremium {
effectiveDisplayTags = displayTags
}
if !filters.isEmpty || suggestedFilters.isEmpty {
var folderCount = 0
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) {
if case .allChats = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false))
entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true))
}
if case let .filter(_, title, _, _) = filter {
folderCount += 1
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount))
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
}
}
@ -274,8 +306,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
}
}
/*entries.append(.displayTags(displayTags))
entries.append(.displayTagsFooter)*/
entries.append(.displayTags(effectiveDisplayTags))
entries.append(.displayTagsFooter)
return entries
}
@ -285,7 +317,7 @@ public enum ChatListFilterPresetListControllerMode {
case modal
}
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController {
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -523,6 +555,16 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
})
}, updateDisplayTags: { value in
context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value)
}, updateDisplayTagsLocked: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
})
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
@ -627,7 +669,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments))
}
@ -659,7 +702,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal<Bool, NoError> in
let fromEntry = entries[fromIndex]
guard case let .preset(_, _, _, fromPreset, _, _, _, _, _) = fromEntry else {
guard case let .preset(_, _, _, fromPreset, _, _, _, _, _, _) = fromEntry else {
return .single(false)
}
var referenceFilter: ChatListFilter?
@ -667,7 +710,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
var afterAll = false
if toIndex < entries.count {
switch entries[toIndex] {
case let .preset(_, _, _, preset, _, _, _, _, _):
case let .preset(_, _, _, preset, _, _, _, _, _, _):
referenceFilter = preset
default:
if entries[toIndex] < fromEntry {

View file

@ -19,6 +19,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let preset: ChatListFilter
let title: String
let label: String
let tagColor: UIColor?
let editing: ChatListFilterPresetListItemEditing
let canBeReordered: Bool
let canBeDeleted: Bool
@ -34,6 +35,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
preset: ChatListFilter,
title: String,
label: String,
tagColor: UIColor?,
editing: ChatListFilterPresetListItemEditing,
canBeReordered: Bool,
canBeDeleted: Bool,
@ -48,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
self.preset = preset
self.title = title
self.label = label
self.tagColor = tagColor
self.editing = editing
self.canBeReordered = canBeReordered
self.canBeDeleted = canBeDeleted
@ -125,6 +128,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
private let labelNode: TextNode
private let arrowNode: ASImageNode
private let sharedIconNode: ASImageNode
private var tagIconView: UIImageView?
private let activateArea: AccessibilityAreaNode
@ -173,7 +177,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
self.sharedIconNode.displayWithoutProcessing = true
self.sharedIconNode.displaysAsynchronously = false
self.sharedIconNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
self.highlightedBackgroundNode = ASDisplayNode()
@ -214,6 +218,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
} else {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor)
}
@ -406,7 +411,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
strongSelf.arrowNode.isHidden = item.isAllChats
if let sharedIconImage = strongSelf.sharedIconNode.image {
strongSelf.sharedIconNode.frame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size)
var sharedIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size)
if item.tagColor != nil {
sharedIconFrame.origin.x -= 34.0
}
strongSelf.sharedIconNode.frame = sharedIconFrame
}
var isShared = false
if case let .filter(_, _, _, data) = item.preset, data.isShared {
@ -414,6 +423,28 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
strongSelf.sharedIconNode.isHidden = !isShared
if let tagColor = item.tagColor {
let tagIconView: UIImageView
if let current = strongSelf.tagIconView {
tagIconView = current
} else {
tagIconView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 24.0, color: .white)?.withRenderingMode(.alwaysTemplate))
strongSelf.tagIconView = tagIconView
strongSelf.containerNode.view.addSubview(tagIconView)
}
tagIconView.tintColor = tagColor
let tagIconFrame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX - 2.0 - 24.0, y: floorToScreenPixels((layout.contentSize.height - 24.0) / 2.0)), size: CGSize(width: 24.0, height: 24.0))
tagIconView.frame = tagIconFrame
transition.updateAlpha(layer: tagIconView.layer, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
} else {
if let tagIconView = strongSelf.tagIconView {
strongSelf.tagIconView = nil
tagIconView.removeFromSuperview()
}
}
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
@ -507,7 +538,16 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
var sharedIconFrame = self.sharedIconNode.frame
sharedIconFrame.origin.x = arrowFrame.minX + 2.0 - sharedIconFrame.width
if self.item?.tagColor != nil {
sharedIconFrame.origin.x -= 34.0
}
transition.updateFrame(node: self.sharedIconNode, frame: sharedIconFrame)
if let tagIconView = self.tagIconView {
var tagIconFrame = tagIconView.frame
tagIconFrame.origin.x = arrowFrame.minX - 2.0 - tagIconFrame.width
transition.updateFrame(view: tagIconView, frame: tagIconFrame)
}
}
override func revealOptionsInteractivelyOpened() {

View file

@ -2324,6 +2324,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.interaction.openStories?(id, sourceNode.avatarNode)
}
}, dismissNotice: { _ in
}, editPeer: { _ in
})
chatListInteraction.isSearchMode = true
@ -3689,6 +3690,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
var isInlineMode = false
if case .topics = key {

View file

@ -157,6 +157,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in
}, editPeer: { _ in
})
interaction.isInlineMode = isInlineMode

View file

@ -26,6 +26,8 @@ import TextNodeWithEntities
import ComponentFlow
import EmojiStatusComponent
import AvatarVideoNode
import AppBundle
import MultilineTextComponent
public enum ChatListItemContent {
public struct ThreadInfo: Equatable {
@ -89,6 +91,20 @@ public enum ChatListItemContent {
}
}
public struct CustomMessageListData: Equatable {
public var commandPrefix: String?
public var searchQuery: String?
public var messageCount: Int?
public var hideSeparator: Bool
public init(commandPrefix: String?, searchQuery: String?, messageCount: Int?, hideSeparator: Bool) {
self.commandPrefix = commandPrefix
self.searchQuery = searchQuery
self.messageCount = messageCount
self.hideSeparator = hideSeparator
}
}
public struct PeerData {
public var messages: [EngineMessage]
public var peer: EngineRenderedPeer
@ -112,6 +128,7 @@ public enum ChatListItemContent {
public var requiresPremiumForMessaging: Bool
public var displayAsTopicList: Bool
public var tags: [Tag]
public var customMessageListData: CustomMessageListData?
public init(
messages: [EngineMessage],
@ -135,7 +152,8 @@ public enum ChatListItemContent {
storyState: StoryState?,
requiresPremiumForMessaging: Bool,
displayAsTopicList: Bool,
tags: [Tag]
tags: [Tag],
customMessageListData: CustomMessageListData? = nil
) {
self.messages = messages
self.peer = peer
@ -159,6 +177,7 @@ public enum ChatListItemContent {
self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.displayAsTopicList = displayAsTopicList
self.tags = tags
self.customMessageListData = customMessageListData
}
}
@ -251,7 +270,9 @@ private final class ChatListItemTagListComponent: Component {
func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor) -> CGSize {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: title.isEmpty ? " " : title, font: Font.semibold(11.0), color: foregroundColor)),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(11.0), textColor: foregroundColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
@ -310,7 +331,7 @@ private final class ChatListItemTagListComponent: Component {
itemId = tag.id
let tagColor = PeerNameColor(rawValue: tag.colorId)
let resolvedColor = component.context.peerNameColors.getProfile(tagColor, dark: component.theme.overallDarkAppearance, subject: .palette)
let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance)
itemTitle = tag.title.uppercased()
itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1)
@ -541,6 +562,7 @@ private enum RevealOptionKey: Int32 {
case hidePsa
case open
case close
case edit
}
private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) -> Bool {
@ -781,8 +803,8 @@ private let playIconImage = UIImage(bundleImageName: "Chat List/MiniThumbnailPla
private final class ChatListMediaPreviewNode: ASDisplayNode {
private let context: AccountContext
private let message: EngineMessage
private let media: EngineMedia
let message: EngineMessage
let media: EngineMedia
private let imageNode: TransformImageNode
private let playIcon: ASImageNode
@ -843,10 +865,14 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
if file.isInstantVideo {
isRound = true
}
if file.isAnimated {
if file.isSticker || file.isAnimatedSticker {
self.playIcon.isHidden = true
} else {
self.playIcon.isHidden = false
if file.isAnimated {
self.playIcon.isHidden = true
} else {
self.playIcon.isHidden = false
}
}
if let mediaDimensions = file.dimensions {
dimensions = mediaDimensions.cgSize
@ -858,9 +884,18 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
}
}
let radius: CGFloat
if isRound {
radius = size.width / 2.0
} else if size.width >= 30.0 {
radius = 8.0
} else {
radius = 2.0
}
let makeLayout = self.imageNode.asyncLayout()
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: isRound ? size.width / 2.0 : 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
}
}
@ -1123,6 +1158,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
}
private struct ContentImageSpec {
var message: EngineMessage
var media: EngineMedia
var size: CGSize
init(message: EngineMessage, media: EngineMedia, size: CGSize) {
self.message = message
self.media = media
self.size = size
}
}
var item: ChatListItem?
private let backgroundNode: ASDisplayNode
@ -1137,6 +1184,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode?
var avatarTapRecognizer: UITapGestureRecognizer?
private var avatarMediaNode: ChatListMediaPreviewNode?
private var inlineNavigationMarkLayer: SimpleLayer?
@ -1149,10 +1197,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
private var currentItemHeight: CGFloat?
let forwardedIconNode: ASImageNode
let textNode: TextNodeWithEntities
var trailingTextBadgeNode: TextNode?
var trailingTextBadgeBackground: UIImageView?
var dustNode: InvisibleInkDustNode?
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
var dateStatusIconNode: ASImageNode?
var dateDisclosureIconView: UIImageView?
let separatorNode: ASDisplayNode
let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode
@ -1174,7 +1225,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
private var cachedDataDisposable = MetaDisposable()
private var currentTextLeftCutout: CGFloat = 0.0
private var currentMediaPreviewSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
private var currentMediaPreviewSpecs: [ContentImageSpec] = []
private var mediaPreviewNodes: [EngineMedia.Id: ChatListMediaPreviewNode] = [:]
var selectableControlNode: ItemListSelectableControlNode?
@ -1419,6 +1470,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.displaysAsynchronously = true
self.textNode.textNode.anchorPoint = CGPoint()
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false
@ -1587,7 +1639,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id.isReplies {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
} else if peer.id.isReplies {
overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages {
overrideImage = .anonymousSavedMessagesIcon
@ -1609,7 +1662,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
isForumAvatar = true
}
}
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
}
if avatarDiameter != 60.0 {
let avatarFontSize = floor(avatarDiameter * 26.0 / 60.0)
if self.avatarNode.font.pointSize != avatarFontSize {
self.avatarNode.font = avatarPlaceholderFont(size: avatarFontSize)
}
}
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
if peer.isPremium && peer.id != item.context.account.peerId {
@ -1792,6 +1857,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let authorLayout = self.authorNode.asyncLayout()
let makeMeasureLayout = TextNode.asyncLayout(self.measureNode)
@ -1995,41 +2061,62 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, true)
let selectionControlStyle: ItemListSelectableControlNode.Style
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
selectionControlStyle = .small
} else {
selectionControlStyle = .compact
}
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, selectionControlStyle)
if promoInfo == nil && !isPeerGroup {
selectableControlSizeAndApply = sizeAndApply
}
editingOffset = sizeAndApply.0
var canReorder = false
if case let .chatList(index) = item.index, index.pinningIndex != nil, promoInfo == nil, !isPeerGroup {
let sizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = sizeAndApply
reorderInset = sizeAndApply.0
canReorder = true
} else if case let .forum(pinnedIndex, _, _, _, _) = item.index, case .index = pinnedIndex {
if case let .chat(itemPeer) = contentPeer, case let .channel(channel) = itemPeer.peer {
let canPin = channel.flags.contains(.isCreator) || channel.hasPermission(.pinMessages)
if canPin {
let sizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = sizeAndApply
reorderInset = sizeAndApply.0
canReorder = true
}
}
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
canReorder = true
}
if canReorder {
let sizeAndApply = reorderControlLayout(item.presentationData.theme)
reorderControlSizeAndApply = sizeAndApply
reorderInset = sizeAndApply.0
}
} else {
editingOffset = 0.0
}
let enableChatListPhotos = true
let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
// if changed, adjust setupItem accordingly
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
let avatarLeftInset: CGFloat
if item.interaction.isInlineMode {
avatarLeftInset = 12.0
} else if !useChatListLayout {
avatarLeftInset = 50.0
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
avatarLeftInset = 17.0 + avatarDiameter
} else {
avatarLeftInset = 18.0 + avatarDiameter
if item.interaction.isInlineMode {
avatarLeftInset = 12.0
} else if !useChatListLayout {
avatarLeftInset = 50.0
} else {
avatarLeftInset = 18.0 + avatarDiameter
}
}
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
@ -2083,7 +2170,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
hideAuthor = true
}
let attributedText: NSAttributedString
var attributedText: NSAttributedString
var hasDraft = false
var inlineAuthorPrefix: String?
@ -2092,13 +2179,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
useInlineAuthorPrefix = true
}
if !itemTags.isEmpty {
if case let .chat(peer) = contentPeer, peer.peerId == item.context.account.peerId {
} else {
useInlineAuthorPrefix = true
}
forumTopicData = nil
topForumTopicItems = []
if case let .chat(itemPeer, _, _, _, _, _, _) = contentData {
if let messagePeer = itemPeer.chatMainPeer {
switch messagePeer {
case let .channel(channel):
if case .group = channel.info {
useInlineAuthorPrefix = true
}
case .legacyGroup:
useInlineAuthorPrefix = true
default:
break
}
}
}
}
if useInlineAuthorPrefix {
@ -2121,7 +2218,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let contentImageSpacing: CGFloat = 2.0
let forwardedIconSpacing: CGFloat = 6.0
let contentImageTrailingSpace: CGFloat = 5.0
var contentImageSpecs: [(message: EngineMessage, media: EngineMedia, size: CGSize)] = []
var contentImageSpecs: [ContentImageSpec] = []
var avatarContentImageSpec: ContentImageSpec?
var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)?
var displayForwardedIcon = false
@ -2208,20 +2307,27 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
hasDraft = true
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
//TODO:localize
switch mediaDraftContentType {
case .audio:
attributedText = NSAttributedString(string: "Voice Message", font: textFont, textColor: theme.messageTextColor)
attributedText = NSAttributedString(string: item.presentationData.strings.Message_Audio, font: textFont, textColor: theme.messageTextColor)
case .video:
attributedText = NSAttributedString(string: "Video Message", font: textFont, textColor: theme.messageTextColor)
attributedText = NSAttributedString(string: item.presentationData.strings.Message_VideoMessage, font: textFont, textColor: theme.messageTextColor)
}
} else if inlineAuthorPrefix == nil, let draftState = draftState {
hasDraft = true
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
let draftText = stringWithAppliedEntities(draftState.text, entities: draftState.entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil)
attributedText = foldLineBreaks(draftText)
if !itemTags.isEmpty {
let tempAttributedText = foldLineBreaks(draftText)
let attributedTextWithDraft = NSMutableAttributedString()
attributedTextWithDraft.append(NSAttributedString(string: item.presentationData.strings.DialogList_Draft + ": ", font: textFont, textColor: theme.messageDraftTextColor))
attributedTextWithDraft.append(tempAttributedText)
attributedText = attributedTextWithDraft
} else {
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
attributedText = foldLineBreaks(draftText)
}
} else if let message = messages.first {
var composedString: NSMutableAttributedString
@ -2401,6 +2507,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
attributedText = composedString
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let commandPrefix = customMessageListData.commandPrefix {
let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText)
let boldTextFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
mutableAttributedText.insert(NSAttributedString(string: commandPrefix + " ", font: boldTextFont, textColor: theme.titleColor), at: 0)
if let searchQuery = customMessageListData.searchQuery {
let range = (mutableAttributedText.string as NSString).range(of: searchQuery)
if range.location == 0 {
mutableAttributedText.addAttribute(.foregroundColor, value: item.presentationData.theme.list.itemAccentColor, range: range)
}
}
attributedText = mutableAttributedText
}
if !ignoreForwardedIcon {
if case .savedMessagesChats = item.chatListLocation {
displayForwardedIcon = false
@ -2420,21 +2539,31 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if displayMediaPreviews {
let contentImageFillSize = CGSize(width: 8.0, height: contentImageSize.height)
_ = contentImageFillSize
var contentImageIsDisplayedAsAvatar = false
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
contentImageIsDisplayedAsAvatar = true
}
for message in messages {
if contentImageSpecs.count >= 3 {
break
}
inner: for media in message.media {
if let image = media as? TelegramMediaImage {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = media as? TelegramMediaFile {
if file.isVideo, !file.isVideoSticker, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
} else if contentImageIsDisplayedAsAvatar && (file.isSticker || file.isVideoSticker) {
let fitSize = contentImageSize
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
@ -2442,36 +2571,41 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let image = content.image, let type = content.type, imageTypes.contains(type) {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = content.file {
if file.isVideo, !file.isInstantVideo, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
}
} else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image {
let fitSize = contentImageSize
contentImageSpecs.append((message, .action(action), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .action(action), size: fitSize))
} else if let storyMedia = media as? TelegramMediaStory, let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self) {
if let image = storyItem.media as? TelegramMediaImage {
if let _ = largestImageRepresentation(image.representations) {
let fitSize = contentImageSize
contentImageSpecs.append((message, .image(image), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize))
}
break inner
} else if let file = storyItem.media as? TelegramMediaFile {
if file.isVideo, !file.isInstantVideo, let _ = file.dimensions {
let fitSize = contentImageSize
contentImageSpecs.append((message, .file(file), fitSize))
contentImageSpecs.append(ContentImageSpec(message: message, media: .file(file), size: fitSize))
}
break inner
}
}
}
}
if contentImageIsDisplayedAsAvatar {
avatarContentImageSpec = contentImageSpecs.first
contentImageSpecs.removeAll()
}
}
} else {
attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
@ -2548,7 +2682,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch contentData {
case let .chat(itemPeer, threadInfo, _, _, _, _, _):
if let threadInfo = threadInfo {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.commandPrefix != nil {
titleAttributedString = nil
} else {
if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
let textColor: UIColor
if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat {
textColor = theme.secretTitleColor
} else {
textColor = theme.titleColor
}
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
}
}
} else if let threadInfo = threadInfo {
titleAttributedString = NSAttributedString(string: threadInfo.info.title, font: titleFont, textColor: theme.titleColor)
} else if let message = messages.last, case let .user(author) = message.author, displayAsMessage {
titleAttributedString = NSAttributedString(string: author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer.user(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: titleFont, textColor: theme.titleColor)
@ -2587,7 +2735,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case let .peer(peerData):
topIndex = peerData.messages.first?.index
}
if let topIndex {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if let messageCount = customMessageListData.messageCount, customMessageListData.commandPrefix == nil {
dateText = "\(messageCount)"
} else {
dateText = " "
}
} else if let topIndex {
var t = Int(topIndex.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
@ -2754,7 +2908,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case let .peer(peerData):
if let peer = peerData.messages.last?.author {
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
currentCredibilityIconContent = nil
} else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
@ -2776,7 +2932,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
break
}
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
currentCredibilityIconContent = nil
} else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
@ -2902,9 +3060,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let (authorLayout, authorApply) = authorLayout(item.context, rawContentWidth - badgeSize, item.presentationData.theme, effectiveAuthorTitle, forumThreads)
var textBottomRightCutout: CGFloat = 0.0
let trailingTextBadgeInsets = UIEdgeInsets(top: 2.0 - UIScreenPixel, left: 5.0, bottom: 2.0 - UIScreenPixel, right: 5.0)
var trailingTextBadgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil, let messageCount = customMessageListData.messageCount, messageCount > 1 {
let trailingText: String
trailingText = item.presentationData.strings.ChatList_ItemMoreMessagesFormat(Int32(messageCount - 1))
let trailingAttributedText = NSAttributedString(string: trailingText, font: Font.regular(12.0), textColor: theme.messageTextColor)
let (layout, apply) = makeTrailingTextBadgeLayout(TextNodeLayoutArguments(attributedString: trailingAttributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
trailingTextBadgeLayoutAndApply = (layout, apply)
textBottomRightCutout += layout.size.width + 4.0 + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right
}
var textCutout: TextNodeCutout?
if !textLeftCutout.isZero {
textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil)
if !textLeftCutout.isZero || !textBottomRightCutout.isZero {
textCutout = TextNodeCutout(topLeft: textLeftCutout.isZero ? nil : CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: textBottomRightCutout.isZero ? nil : CGSize(width: textBottomRightCutout, height: 10.0))
}
var textMaxWidth = rawContentWidth - badgeSize
@ -3035,6 +3206,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor)
]
peerLeftRevealOptions = []
} else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
peerLeftRevealOptions = []
if customMessageListData.commandPrefix != nil {
peerRevealOptions = [
ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor),
ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)
]
} else {
peerRevealOptions = []
}
} else if promoInfo == nil {
peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: !isAccountPeer ? (currentMutedIconImage != nil) : nil, location: item.chatListLocation, peerId: renderedPeer.peerId, accountPeerId: item.context.account.peerId, canDelete: true, isEditing: item.editing, filterData: item.filterData)
if case let .chat(itemPeer) = contentPeer {
@ -3066,16 +3247,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
animateContent = true
}
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: " ", font: titleFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = -1.0
let authorSpacing: CGFloat = -3.0
var itemHeight: CGFloat = 8.0 * 2.0 + 1.0
itemHeight -= 21.0
itemHeight += titleLayout.size.height
itemHeight += measureLayout.size.height * 3.0
itemHeight += titleSpacing
itemHeight += authorSpacing
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
itemHeight += measureLayout.size.height * 2.0
itemHeight += 20.0
} else {
itemHeight += titleLayout.size.height
itemHeight += measureLayout.size.height * 3.0
itemHeight += titleSpacing
itemHeight += authorSpacing
}
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
@ -3448,6 +3634,19 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = measureApply()
let _ = dateApply()
var currentTextSnapshotView: UIView?
if transition.isAnimated, let currentItem, currentItem.editing != item.editing, strongSelf.textNode.textNode.cachedLayout?.linesRects() != textLayout.linesRects() {
if let textSnapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() {
textSnapshotView.layer.anchorPoint = CGPoint()
currentTextSnapshotView = textSnapshotView
strongSelf.textNode.textNode.view.superview?.insertSubview(textSnapshotView, aboveSubview: strongSelf.textNode.textNode.view)
textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in
textSnapshotView?.removeFromSuperview()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.interaction.animationCache,
@ -3466,7 +3665,32 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = mentionBadgeApply(animateBadges, true)
let _ = onlineApply(animateContent && animateOnline)
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 dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil {
dateFrame.origin.x -= 10.0
let dateDisclosureIconView: UIImageView
if let current = strongSelf.dateDisclosureIconView {
dateDisclosureIconView = current
} else {
dateDisclosureIconView = UIImageView(image: UIImage(bundleImageName: "Item List/DisclosureArrow")?.withRenderingMode(.alwaysTemplate))
strongSelf.dateDisclosureIconView = dateDisclosureIconView
strongSelf.mainContentContainerNode.view.addSubview(dateDisclosureIconView)
}
dateDisclosureIconView.tintColor = item.presentationData.theme.list.disclosureArrowColor
let iconScale: CGFloat = 0.7
if let image = dateDisclosureIconView.image {
let imageSize = CGSize(width: floor(image.size.width * iconScale), height: floor(image.size.height * iconScale))
let iconFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - imageSize.width + 4.0, y: floorToScreenPixels(dateFrame.midY - imageSize.height * 0.5)), size: imageSize)
dateDisclosureIconView.frame = iconFrame
}
} else if let dateDisclosureIconView = strongSelf.dateDisclosureIconView {
strongSelf.dateDisclosureIconView = nil
dateDisclosureIconView.removeFromSuperview()
}
transition.updateFrame(node: strongSelf.dateNode, frame: dateFrame)
var statusOffset: CGFloat = 0.0
if let dateIconImage {
@ -3689,13 +3913,69 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.authorNode.assignParentNode(parentNode: nil)
}
if let currentTextSnapshotView {
transition.updatePosition(layer: currentTextSnapshotView.layer, position: textNodeFrame.origin)
}
if let trailingTextBadgeLayoutAndApply {
let badgeSize = CGSize(width: trailingTextBadgeLayoutAndApply.0.size.width + trailingTextBadgeInsets.left + trailingTextBadgeInsets.right, height: trailingTextBadgeLayoutAndApply.0.size.height + trailingTextBadgeInsets.top + trailingTextBadgeInsets.bottom - UIScreenPixel)
var badgeFrame: CGRect
if textLayout.numberOfLines > 1 {
badgeFrame = CGRect(origin: CGPoint(x: textLayout.trailingLineWidth + 4.0, y: textNodeFrame.height - 3.0 - badgeSize.height), size: badgeSize)
} else {
let firstLineFrame = textLayout.linesRects().first ?? CGRect(origin: CGPoint(), size: textNodeFrame.size)
badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: firstLineFrame.height + 5.0), size: badgeSize)
}
if badgeFrame.origin.x + badgeFrame.width >= textNodeFrame.width - 2.0 - 10.0 {
badgeFrame.origin.x = textNodeFrame.width - 2.0 - badgeFrame.width
}
let trailingTextBadgeBackground: UIImageView
if let current = strongSelf.trailingTextBadgeBackground {
trailingTextBadgeBackground = current
} else {
trailingTextBadgeBackground = UIImageView(image: tagBackgroundImage)
strongSelf.trailingTextBadgeBackground = trailingTextBadgeBackground
strongSelf.textNode.textNode.view.addSubview(trailingTextBadgeBackground)
}
trailingTextBadgeBackground.tintColor = theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1)
trailingTextBadgeBackground.frame = badgeFrame
let trailingTextBadgeFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + trailingTextBadgeInsets.left, y: badgeFrame.minY + trailingTextBadgeInsets.top), size: trailingTextBadgeLayoutAndApply.0.size)
let trailingTextBadgeNode = trailingTextBadgeLayoutAndApply.1()
if strongSelf.trailingTextBadgeNode !== trailingTextBadgeNode {
strongSelf.trailingTextBadgeNode?.removeFromSupernode()
strongSelf.trailingTextBadgeNode = trailingTextBadgeNode
strongSelf.textNode.textNode.addSubnode(trailingTextBadgeNode)
trailingTextBadgeNode.layer.anchorPoint = CGPoint()
}
trailingTextBadgeNode.frame = trailingTextBadgeFrame
} else {
if let trailingTextBadgeNode = strongSelf.trailingTextBadgeNode {
strongSelf.trailingTextBadgeNode = nil
trailingTextBadgeNode.removeFromSupernode()
}
if let trailingTextBadgeBackground = strongSelf.trailingTextBadgeBackground {
strongSelf.trailingTextBadgeBackground = nil
trailingTextBadgeBackground.removeFromSuperview()
}
}
if !itemTags.isEmpty {
let itemTagListFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.maxY - 12.0), size: CGSize(width: contentRect.width, height: 20.0))
var itemTagListTransition = transition
let itemTagList: ComponentView<Empty>
if let current = strongSelf.itemTagList {
itemTagList = current
} else {
itemTagListTransition = .immediate
itemTagList = ComponentView()
strongSelf.itemTagList = itemTagList
}
@ -3714,7 +3994,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
itemTagListView.isUserInteractionEnabled = false
strongSelf.mainContentContainerNode.view.addSubview(itemTagListView)
}
itemTagListView.frame = itemTagListFrame
itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame)
}
} else {
if let itemTagList = strongSelf.itemTagList {
@ -3833,7 +4114,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
var validMediaIds: [EngineMedia.Id] = []
for (message, media, mediaSize) in contentImageSpecs {
for spec in contentImageSpecs {
let message = spec.message
let media = spec.media
let mediaSize = spec.size
var mediaId = media.id
if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action {
mediaId = image?.id
@ -3872,6 +4157,36 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.currentMediaPreviewSpecs = contentImageSpecs
strongSelf.currentTextLeftCutout = textLeftCutout
if let avatarContentImageSpec {
strongSelf.avatarNode.isHidden = true
if let previous = strongSelf.avatarMediaNode, previous.media != avatarContentImageSpec.media {
strongSelf.avatarMediaNode = nil
previous.removeFromSupernode()
}
var avatarMediaNodeTransition = transition
let avatarMediaNode: ChatListMediaPreviewNode
if let current = strongSelf.avatarMediaNode {
avatarMediaNode = current
} else {
avatarMediaNodeTransition = .immediate
avatarMediaNode = ChatListMediaPreviewNode(context: item.context, message: avatarContentImageSpec.message, media: avatarContentImageSpec.media)
strongSelf.avatarMediaNode = avatarMediaNode
strongSelf.contextContainer.addSubnode(avatarMediaNode)
}
avatarMediaNodeTransition.updateFrame(node: avatarMediaNode, frame: avatarFrame)
avatarMediaNode.updateLayout(size: avatarFrame.size, synchronousLoads: synchronousLoads)
} else {
strongSelf.avatarNode.isHidden = false
if let avatarMediaNode = strongSelf.avatarMediaNode {
strongSelf.avatarMediaNode = nil
avatarMediaNode.removeFromSupernode()
}
}
if !contentDelta.x.isZero || !contentDelta.y.isZero {
let titlePosition = strongSelf.titleNode.position
transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDelta.x, y: titlePosition.y - contentDelta.y))
@ -3997,6 +4312,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0)
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.hideSeparator {
strongSelf.separatorNode.isHidden = true
}
}
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: itemHeight)))
let backgroundColor: UIColor
let highlightedBackgroundColor: UIColor
@ -4012,7 +4333,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor
}
} else {
backgroundColor = theme.itemBackgroundColor
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
backgroundColor = .clear
} else {
backgroundColor = theme.itemBackgroundColor
}
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
}
@ -4228,6 +4553,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.revealOptionsInteractivelyClosed()
self.customAnimationInProgress = false
}
case RevealOptionKey.edit.rawValue:
item.interaction.editPeer(item)
close = true
default:
break
}

View file

@ -108,6 +108,7 @@ public final class ChatListNodeInteraction {
let hideChatFolderUpdates: () -> Void
let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void
let dismissNotice: (ChatListNotice) -> Void
let editPeer: (ChatListItem) -> Void
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
@ -159,7 +160,8 @@ public final class ChatListNodeInteraction {
openChatFolderUpdates: @escaping () -> Void,
hideChatFolderUpdates: @escaping () -> Void,
openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void,
dismissNotice: @escaping (ChatListNotice) -> Void
dismissNotice: @escaping (ChatListNotice) -> Void,
editPeer: @escaping (ChatListItem) -> Void
) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
@ -199,6 +201,7 @@ public final class ChatListNodeInteraction {
self.hideChatFolderUpdates = hideChatFolderUpdates
self.openStories = openStories
self.dismissNotice = dismissNotice
self.editPeer = editPeer
}
}
@ -347,7 +350,7 @@ public struct ChatListNodeState: Equatable {
}
}
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case .HeaderEntry:
@ -430,7 +433,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
},
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
@ -748,7 +751,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .PeerEntry(peerEntry):
@ -808,7 +811,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
},
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(accountPeerId: context.account.peerId, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
@ -1103,8 +1106,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
}
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
}
private final class ChatListOpaqueTransactionState {
@ -1785,6 +1788,7 @@ public final class ChatListNode: ListView {
default:
break
}
}, editPeer: { _ in
})
nodeInteraction.isInlineMode = isInlineMode
@ -2110,8 +2114,17 @@ public final class ChatListNode: ListView {
}
let previousChatListFilters = Atomic<[ChatListFilter]?>(value: nil)
let chatListNodeViewTransition = combineLatest(
queue: viewProcessingQueue,
let previousAccountIsPremium = Atomic<Bool?>(value: nil)
let accountIsPremium = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> map { peer -> Bool in
return peer?.isPremium ?? false
}
|> distinctUntilChanged
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue,
hideArchivedFolderByDefault,
displayArchiveIntro,
storageInfo,
@ -2120,9 +2133,10 @@ public final class ChatListNode: ListView {
chatListViewUpdate,
self.statePromise.get(),
contacts,
chatListFilters
chatListFilters,
accountIsPremium
)
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts, chatListFilters) -> Signal<ChatListNodeListViewTransition, NoError> in
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts, chatListFilters, accountIsPremium) -> Signal<ChatListNodeListViewTransition, NoError> in
let (update, filter) = updateAndFilter
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
@ -2583,9 +2597,12 @@ public final class ChatListNode: ListView {
if chatListFilters != previousChatListFiltersValue {
forceAllUpdated = true
}
if accountIsPremium != previousAccountIsPremium.swap(accountIsPremium) {
forceAllUpdated = true
}
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated)
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) })
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
}
@ -3185,7 +3202,7 @@ public final class ChatListNode: ListView {
}
private func resetFilter() {
if let chatListFilter = self.chatListFilter {
if let chatListFilter = self.chatListFilter, chatListFilter.id != Int32.max {
self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters()
|> map { filters -> ChatListFilter? in
for filter in filters {
@ -4200,7 +4217,14 @@ func hideChatListContacts(context: AccountContext) {
let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).startStandalone()
}
func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] {
func chatListItemTags(location: ChatListControllerLocation, accountPeerId: EnginePeer.Id, isPremium: Bool, peer: EnginePeer?, isUnread: Bool, isMuted: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> [ChatListItemContent.Tag] {
if !isPremium {
return []
}
if case .chatList = location {
} else {
return []
}
guard let chatListFilters, !chatListFilters.isEmpty else {
return []
}
@ -4210,13 +4234,15 @@ func chatListItemTags(accountPeerId: EnginePeer.Id, peer: EnginePeer?, isUnread:
var result: [ChatListItemContent.Tag] = []
for case let .filter(id, title, _, data) in chatListFilters {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
result.append(ChatListItemContent.Tag(
id: id,
title: title,
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
))
if data.color != nil {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
result.append(ChatListItemContent.Tag(
id: id,
title: title,
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
))
}
}
}
return result

View file

@ -67,7 +67,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
}
if !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
if user.botInfo == nil && !user.flags.contains(.isSupport) {
return false
}
} else if let _ = peer as? TelegramSecretChat {
@ -88,7 +88,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
}
if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return false
}
}

View file

@ -104,6 +104,8 @@ public final class ChatPanelInterfaceInteraction {
public let togglePeerNotifications: () -> Void
public let sendContextResult: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool
public let sendBotCommand: (Peer, String) -> Void
public let sendShortcut: (Int32) -> Void
public let openEditShortcuts: () -> Void
public let sendBotStart: (String?) -> Void
public let botSwitchChatWithPayload: (PeerId, String) -> Void
public let beginMediaRecording: (Bool) -> Void
@ -217,6 +219,8 @@ public final class ChatPanelInterfaceInteraction {
togglePeerNotifications: @escaping () -> Void,
sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool,
sendBotCommand: @escaping (Peer, String) -> Void,
sendShortcut: @escaping (Int32) -> Void,
openEditShortcuts: @escaping () -> Void,
sendBotStart: @escaping (String?) -> Void,
botSwitchChatWithPayload: @escaping (PeerId, String) -> Void,
beginMediaRecording: @escaping (Bool) -> Void,
@ -329,6 +333,8 @@ public final class ChatPanelInterfaceInteraction {
self.togglePeerNotifications = togglePeerNotifications
self.sendContextResult = sendContextResult
self.sendBotCommand = sendBotCommand
self.sendShortcut = sendShortcut
self.openEditShortcuts = openEditShortcuts
self.sendBotStart = sendBotStart
self.botSwitchChatWithPayload = botSwitchChatWithPayload
self.beginMediaRecording = beginMediaRecording
@ -448,6 +454,8 @@ public final class ChatPanelInterfaceInteraction {
}, sendContextResult: { _, _, _, _ in
return false
}, sendBotCommand: { _, _ in
}, sendShortcut: { _ in
}, openEditShortcuts: {
}, sendBotStart: { _ in
}, botSwitchChatWithPayload: { _, _ in
}, beginMediaRecording: { _ in

View file

@ -17,7 +17,7 @@ public extension ChatLocation {
return peerId
case let .replyThread(replyThreadMessage):
return replyThreadMessage.peerId
case .feed:
case .customChatContents:
return nil
}
}
@ -28,7 +28,7 @@ public extension ChatLocation {
return nil
case let .replyThread(replyThreadMessage):
return replyThreadMessage.threadId
case .feed:
case .customChatContents:
return nil
}
}
@ -1202,6 +1202,8 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo
} else {
return false
}
} else if case .customChatContents = state.chatLocation {
return true
} else {
return false
}

View file

@ -722,11 +722,11 @@ public extension CombinedComponent {
updatedChild.view.layer.shadowRadius = 0.0
updatedChild.view.layer.shadowOpacity = 0.0
}
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition, isLocal in
guard let viewContext = viewContext else {
return
}
viewContext.state.updated(transition: transition)
viewContext.state.updated(transition: transition, isLocal: isLocal)
}
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {

View file

@ -89,15 +89,15 @@ extension UIView {
}
open class ComponentState {
open var _updated: ((Transition) -> Void)?
open var _updated: ((Transition, Bool) -> Void)?
var isUpdated: Bool = false
public init() {
}
public final func updated(transition: Transition = .immediate) {
public final func updated(transition: Transition = .immediate, isLocal: Bool = false) {
self.isUpdated = true
self._updated?(transition)
self._updated?(transition, isLocal)
}
}

View file

@ -8,16 +8,16 @@ public final class RoundedRectangle: Component {
}
public let colors: [UIColor]
public let cornerRadius: CGFloat
public let cornerRadius: CGFloat?
public let gradientDirection: GradientDirection
public let stroke: CGFloat?
public let strokeColor: UIColor?
public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor)
}
public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
self.colors = colors
self.cornerRadius = cornerRadius
self.gradientDirection = gradientDirection
@ -49,8 +49,10 @@ public final class RoundedRectangle: Component {
func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component != component {
let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5
if component.colors.count == 1, let color = component.colors.first {
let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0)
let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
if let strokeColor = component.strokeColor {
@ -69,13 +71,13 @@ public final class RoundedRectangle: Component {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
} else if component.colors.count > 1 {
let imageSize = availableSize
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: component.cornerRadius).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath)
context.clip()
let colors = component.colors
@ -93,12 +95,12 @@ public final class RoundedRectangle: Component {
if let stroke = component.stroke, stroke > 0.0 {
context.resetClip()
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: component.cornerRadius).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: cornerRadius).cgPath)
context.setBlendMode(.clear)
context.fill(CGRect(origin: .zero, size: imageSize))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
}
}

View file

@ -82,7 +82,7 @@ public final class ComponentHostView<EnvironmentType>: UIView {
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition in
componentState._updated = { [weak self] transition, _ in
guard let strongSelf = self else {
return
}
@ -208,11 +208,11 @@ public final class ComponentView<EnvironmentType> {
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition in
componentState._updated = { [weak self] transition, isLocal in
guard let strongSelf = self else {
return
}
if let parentState = strongSelf.parentState {
if !isLocal, let parentState = strongSelf.parentState {
parentState.updated(transition: transition)
} else {
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {

View file

@ -46,6 +46,7 @@ open class ViewControllerComponentContainer: ViewController {
public let statusBarHeight: CGFloat
public let navigationHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let additionalInsets: UIEdgeInsets
public let inputHeight: CGFloat
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
@ -60,6 +61,7 @@ open class ViewControllerComponentContainer: ViewController {
statusBarHeight: CGFloat,
navigationHeight: CGFloat,
safeInsets: UIEdgeInsets,
additionalInsets: UIEdgeInsets,
inputHeight: CGFloat,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
@ -73,6 +75,7 @@ open class ViewControllerComponentContainer: ViewController {
self.statusBarHeight = statusBarHeight
self.navigationHeight = navigationHeight
self.safeInsets = safeInsets
self.additionalInsets = additionalInsets
self.inputHeight = inputHeight
self.metrics = metrics
self.deviceMetrics = deviceMetrics
@ -98,6 +101,9 @@ open class ViewControllerComponentContainer: ViewController {
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.additionalInsets != rhs.additionalInsets {
return false
}
if lhs.inputHeight != rhs.inputHeight {
return false
}
@ -167,6 +173,7 @@ open class ViewControllerComponentContainer: ViewController {
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: navigationHeight,
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,

View file

@ -702,9 +702,29 @@ private struct ContactsListNodeTransition {
}
public enum ContactListPresentation {
public struct Search {
public var signal: Signal<String, NoError>
public var searchChatList: Bool
public var searchDeviceContacts: Bool
public var searchGroups: Bool
public var searchChannels: Bool
public var globalSearch: Bool
public var displaySavedMessages: Bool
public init(signal: Signal<String, NoError>, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool, displaySavedMessages: Bool) {
self.signal = signal
self.searchChatList = searchChatList
self.searchDeviceContacts = searchDeviceContacts
self.searchGroups = searchGroups
self.searchChannels = searchChannels
self.globalSearch = globalSearch
self.displaySavedMessages = displaySavedMessages
}
}
case orderedByPresence(options: [ContactListAdditionalOption])
case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool)
case search(signal: Signal<String, NoError>, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool)
case search(Search)
public var sortOrder: ContactsSortOrder? {
switch self {
@ -1097,7 +1117,15 @@ public final class ContactListNode: ASDisplayNode {
displayTopPeers = displayTopPeersValue
}
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {
if case let .search(search) = presentation {
let query = search.signal
let searchChatList = search.searchChatList
let searchDeviceContacts = search.searchDeviceContacts
let searchGroups = search.searchGroups
let searchChannels = search.searchChannels
let globalSearch = search.globalSearch
let displaySavedMessages = search.displaySavedMessages
return query
|> mapToSignal { query in
let foundLocalContacts: Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError>
@ -1108,6 +1136,12 @@ public final class ContactListNode: ASDisplayNode {
var resultPeers: [FoundPeer] = []
for peer in peers {
if !displaySavedMessages {
if peer.peerId == context.account.peerId {
continue
}
}
if searchGroups || searchChannels {
let mainPeer = peer.chatMainPeer
if let _ = mainPeer as? TelegramUser {

View file

@ -362,6 +362,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
statusBarHeight: layout.statusBarHeight ?? 0.0,
sideInset: layout.safeInsets.left,
isSearchActive: self.isSearchDisplayControllerActive,
isSearchEnabled: true,
primaryContent: primaryContent,
secondaryContent: nil,
secondaryTransition: 0.0,

View file

@ -83,7 +83,7 @@ open class AlertController: ViewController, StandalonePresentableController, Key
}
}
}
private let contentNode: AlertContentNode
public let contentNode: AlertContentNode
private let allowInputInset: Bool
private weak var existingAlertController: AlertController?

View file

@ -409,6 +409,8 @@ public final class TextNodeLayout: NSObject {
switch self.resolvedAlignment {
case .center:
lineFrame = CGRect(origin: CGPoint(x: floor((size.width - line.frame.size.width) / 2.0), y: line.frame.minY), size: line.frame.size)
case .right:
lineFrame = CGRect(origin: CGPoint(x: size.width - line.frame.size.width, y: line.frame.minY), size: line.frame.size)
default:
lineFrame = displayLineFrame(frame: line.frame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: size), cutout: cutout)
}
@ -521,6 +523,8 @@ public final class TextNodeLayout: NSObject {
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
case .right:
lineFrame.origin.x = self.size.width - lineFrame.size.width
default:
break
}
@ -589,6 +593,8 @@ public final class TextNodeLayout: NSObject {
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
case .right:
lineFrame.origin.x = self.size.width - lineFrame.size.width
default:
break
}
@ -666,6 +672,8 @@ public final class TextNodeLayout: NSObject {
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
case .right:
lineFrame.origin.x = self.size.width - lineFrame.size.width
default:
break
}
@ -846,6 +854,8 @@ public final class TextNodeLayout: NSObject {
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
case .right:
lineFrame.origin.x = self.size.width - lineFrame.size.width
default:
break
}
@ -2009,7 +2019,7 @@ open class TextNode: ASDisplayNode {
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1)
addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1)
}
}
}
@ -2124,7 +2134,7 @@ open class TextNode: ASDisplayNode {
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length - 1, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1)
addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1)
}
}
@ -2407,6 +2417,8 @@ open class TextNode: ASDisplayNode {
} else {
lineFrame.origin.x += offset.x
}
} else if alignment == .right {
lineFrame.origin.x = offset.x + (bounds.size.width - lineFrame.width)
}
//context.setStrokeColor(UIColor.red.cgColor)

View file

@ -60,6 +60,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
private let hasBin: Bool
private let isStickerEditor: Bool
weak var drawingView: DrawingView?
public weak var selectionContainerView: DrawingSelectionContainerView?
@ -95,16 +96,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let yAxisView = UIView()
private let angleLayer = SimpleShapeLayer()
private let bin = ComponentView<Empty>()
private let stickerOverlayLayer = SimpleShapeLayer()
private let stickerFrameLayer = SimpleShapeLayer()
public var onInteractionUpdated: (Bool) -> Void = { _ in }
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, size: CGSize, hasBin: Bool = false) {
public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) {
self.context = context
self.size = size
self.hasBin = hasBin
self.isStickerEditor = isStickerEditor
super.init(frame: CGRect(origin: .zero, size: size))
@ -140,6 +145,13 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.opacity = 0.0
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor
self.stickerFrameLayer.fillColor = UIColor.clear.cgColor
self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber]
self.stickerFrameLayer.lineCap = .round
self.addSubview(self.topEdgeView)
self.addSubview(self.leftEdgeView)
self.addSubview(self.rightEdgeView)
@ -148,12 +160,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.addSubview(self.xAxisView)
self.addSubview(self.yAxisView)
self.layer.addSublayer(self.angleLayer)
if isStickerEditor {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func addSubview(_ view: UIView) {
super.addSubview(view)
if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
public override func layoutSubviews() {
super.layoutSubviews()
@ -189,6 +214,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.path = anglePath
self.angleLayer.lineWidth = width
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
let frameWidth = floor(self.bounds.width * 0.97)
let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth))
self.stickerOverlayLayer.frame = self.bounds
let overlayOuterRect = UIBezierPath(rect: self.bounds)
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil))
let overlayLineWidth: CGFloat = 2.0 * 2.2
overlayOuterRect.append(overlayInnerRect)
overlayOuterRect.usesEvenOddFillRule = true
self.stickerOverlayLayer.path = overlayOuterRect.cgPath
self.stickerOverlayLayer.fillRule = .evenOdd
self.stickerFrameLayer.frame = self.bounds
self.stickerFrameLayer.lineWidth = overlayLineWidth
self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil)
}
public var entities: [DrawingEntity] {
@ -841,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
self.selectEntity(viewToSelect.entity, animate: false)
self.onInteractionUpdated(true)
} else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
} else if gestureRecognizer.numberOfTouches == 2 || (self.isStickerEditor && self.autoSelectEntities), let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePan(gestureRecognizer)
}
}

View file

@ -2634,6 +2634,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom,
right: layout.safeInsets.right
),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,

View file

@ -1,84 +0,0 @@
import Foundation
import UIKit
import Vision
import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftSignalKit
import VideoToolbox
private let queue = Queue()
public func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
if #available(iOS 17.0, *) {
guard let cgImage = image.cgImage else {
return .single(nil)
}
return Signal { subscriber in
let ciContext = CIContext(options: nil)
let inputImage = CIImage(cgImage: cgImage)
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in
guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else {
subscriber.putNext(nil)
subscriber.putCompletion()
return
}
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
let filter = CIFilter.blendWithMask()
filter.inputImage = inputImage
filter.backgroundImage = CIImage(color: .clear)
filter.maskImage = CIImage(cvPixelBuffer: mask)
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
let image = UIImage(cgImage: cgImage)
subscriber.putNext(image)
subscriber.putCompletion()
return
}
}
subscriber.putNext(nil)
subscriber.putCompletion()
}
try? handler.perform([request])
return ActionDisposable {
request.cancel()
}
}
|> runOn(queue)
} else {
return .single(nil)
}
}
@available(iOS 17.0, *)
private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet {
guard let point = maybePoint else {
return observation.allInstances
}
let instanceMap = observation.instanceMask
let coords = VNImagePointForNormalizedPoint(point, CVPixelBufferGetWidth(instanceMap) - 1, CVPixelBufferGetHeight(instanceMap) - 1)
CVPixelBufferLockBaseAddress(instanceMap, .readOnly)
guard let pixels = CVPixelBufferGetBaseAddress(instanceMap) else {
fatalError()
}
let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMap)
let instanceLabel = pixels.load(fromByteOffset: Int(coords.y) * bytesPerRow + Int(coords.x), as: UInt8.self)
CVPixelBufferUnlockBaseAddress(instanceMap, .readOnly)
return instanceLabel == 0 ? observation.allInstances : [Int(instanceLabel)]
}
private extension UIImage {
convenience init?(pixelBuffer: CVPixelBuffer) {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
guard let cgImage = cgImage else {
return nil
}
self.init(cgImage: cgImage)
}
}

View file

@ -192,6 +192,7 @@ private final class StickerSelectionComponent: Component {
insertText: { _ in
},
backwardsDeleteText: {},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self, let controller = self.component?.getController() {
controller.present(c, in: .window(.root), with: a)

View file

@ -236,6 +236,11 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
}*/
}
var source = source
if standalone {
source = .standaloneMessage(message)
}
if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") {
let gallery = GalleryController(context: context, source: source ?? .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(id: message.id.peerId), customTag: chatFilterTag, chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
navigationController?.replaceTopController(controller, animated: false, ready: ready)

View file

@ -816,8 +816,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) {
self.currentMessage = message
var displayInfo = displayInfo
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
displayInfo = false
}
var canDelete: Bool
var canShare = !message.containsSecretMedia
var canShare = !message.containsSecretMedia && !Namespaces.Message.allNonRegular.contains(message.id.namespace)
var canFullscreen = false

View file

@ -248,13 +248,18 @@ public func galleryItemForEntry(
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: file.duration.flatMap(Double.init)) {
entities = result
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
let caption = galleryCaptionStringWithAppliedEntities(context: context, text: text, entities: entities, message: message)
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp),
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: caption,
@ -348,11 +353,17 @@ public func galleryItemForEntry(
}
description = galleryCaptionStringWithAppliedEntities(context: context, text: descriptionText, entities: entities, message: message)
}
var originData = GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp)
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
originData = GalleryItemOriginData(title: nil, timestamp: nil)
}
return UniversalVideoGalleryItem(
context: context,
presentationData: presentationData,
content: content,
originData: GalleryItemOriginData(title: message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp),
originData: originData,
indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) },
contentInfo: .message(message),
caption: NSAttributedString(string: ""),
@ -605,7 +616,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
case let .replyThread(message):
peerIdValue = message.peerId
threadIdValue = message.threadId
case .feed:
case .customChatContents:
break
}
if peerIdValue == context.account.peerId, let customTag {
@ -656,8 +667,10 @@ public class GalleryController: ViewController, StandalonePresentableController,
let namespaces: MessageIdNamespaces
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
namespaces = .just(Namespaces.Message.allScheduled)
} else if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
namespaces = .just(Namespaces.Message.allQuickReply)
} else {
namespaces = .not(Namespaces.Message.allScheduled)
namespaces = .not(Namespaces.Message.allNonRegular)
}
let inputTag: HistoryViewInputTag
if let customTag {
@ -1391,8 +1404,10 @@ public class GalleryController: ViewController, StandalonePresentableController,
let namespaces: MessageIdNamespaces
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
namespaces = .just(Namespaces.Message.allScheduled)
} else if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
namespaces = .just(Namespaces.Message.allQuickReply)
} else {
namespaces = .not(Namespaces.Message.allScheduled)
namespaces = .not(Namespaces.Message.allNonRegular)
}
let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), ignoreMessagesInTimestampRange: nil, count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: tag, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation])
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in

View file

@ -1184,7 +1184,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var hintSeekable = false
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
if Namespaces.Message.allNonRegular.contains(message.id.namespace) {
disablePictureInPicture = true
} else {
let throttledSignal = videoNode.status

View file

@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)

View file

@ -20,12 +20,12 @@ public final class ItemListAddressItem: ListViewItem, ItemListItem {
let style: ItemListStyle
let displayDecorations: Bool
let action: (() -> Void)?
let longTapAction: (() -> Void)?
let longTapAction: ((ASDisplayNode, String) -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
public let tag: Any?
public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true, action: (() -> Void)?, longTapAction: ((ASDisplayNode, String) -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) {
self.theme = theme
self.label = label
self.text = text
@ -188,7 +188,7 @@ public class ItemListAddressItemNode: ListViewItemNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if let selected = item.selected {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, false)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth - 8.0
}
@ -202,11 +202,11 @@ public class ItemListAddressItemNode: ListViewItemNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset - 98.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let padding: CGFloat = !item.label.isEmpty ? 39.0 : 20.0
let imageSide = min(90.0, max(46.0, textLayout.size.height + padding - 18.0))
let imageSide = min(90.0, max(66.0, textLayout.size.height + padding - 18.0))
let imageSize = CGSize(width: imageSide, height: imageSide)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 18.0))
let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 14.0))
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (nodeLayout, { [weak self] animation in
@ -268,7 +268,7 @@ public class ItemListAddressItemNode: ListViewItemNode {
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: item.label.isEmpty ? 11.0 : 31.0), size: textLayout.size)
let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: floorToScreenPixels((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: 7.0), size: imageSize)
strongSelf.imageNode.frame = imageFrame
if let icon = strongSelf.iconNode.image {
@ -396,7 +396,10 @@ public class ItemListAddressItemNode: ListViewItemNode {
}
override public func longTapped() {
self.item?.longTapAction?()
guard let item = self.item else {
return
}
item.longTapAction?(self, item.text)
}
public var tag: Any? {

View file

@ -55,6 +55,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
if self.alwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false)
}
if self.alwaysPlain {
neighbors.bottom = .sameSection(alwaysPlain: false)
}
let (layout, apply) = node.asyncLayout()(self, params, neighbors)
node.contentSize = layout.contentSize
@ -83,6 +86,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
if self.alwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false)
}
if self.alwaysPlain {
neighbors.bottom = .sameSection(alwaysPlain: false)
}
let (layout, apply) = makeLayout(self, params, neighbors)
Queue.mainQueue().async {
completion(layout, { _ in

View file

@ -440,7 +440,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
if case let .check(checked) = item.control {
selected = checked
}
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, true)
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .compact)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
} else {

View file

@ -5,6 +5,12 @@ import Display
import CheckNode
public final class ItemListSelectableControlNode: ASDisplayNode {
public enum Style {
case regular
case compact
case small
}
private let checkNode: CheckNode
public init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor) {
@ -16,8 +22,8 @@ public final class ItemListSelectableControlNode: ASDisplayNode {
self.addSubnode(self.checkNode)
}
public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ compact: Bool) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) {
return { strokeColor, fillColor, foregroundColor, selected, compact in
public static func asyncLayout(_ node: ItemListSelectableControlNode?) -> (_ strokeColor: UIColor, _ fillColor: UIColor, _ foregroundColor: UIColor, _ selected: Bool, _ style: Style) -> (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode) {
return { strokeColor, fillColor, foregroundColor, selected, style in
let resultNode: ItemListSelectableControlNode
if let node = node {
resultNode = node
@ -25,9 +31,28 @@ public final class ItemListSelectableControlNode: ASDisplayNode {
resultNode = ItemListSelectableControlNode(strokeColor: strokeColor, fillColor: fillColor, foregroundColor: foregroundColor)
}
return (compact ? 38.0 : 45.0, { size, animated in
let checkSize = CGSize(width: 26.0, height: 26.0)
resultNode.checkNode.frame = CGRect(origin: CGPoint(x: compact ? 11.0 : 13.0, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize)
let offsetSize: CGFloat
switch style {
case .regular:
offsetSize = 45.0
case .compact:
offsetSize = 38.0
case .small:
offsetSize = 44.0
}
return (offsetSize, { size, animated in
let checkSize: CGSize
let checkOffset: CGFloat
switch style {
case .regular, .compact:
checkSize = CGSize(width: 26.0, height: 26.0)
checkOffset = style == .compact ? 11.0 : 13.0
case .small:
checkSize = CGSize(width: 22.0, height: 22.0)
checkOffset = 16.0
}
resultNode.checkNode.frame = CGRect(origin: CGPoint(x: checkOffset, y: floorToScreenPixels((size.height - checkSize.height) / 2.0)), size: checkSize)
resultNode.checkNode.setSelected(selected, animated: animated)
return resultNode
})

View file

@ -189,7 +189,7 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if let selected = item.selected {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, false)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth - 8.0
}

View file

@ -567,6 +567,9 @@
updateGroupingButtonVisibility();
} file:__FILE_NAME__ line:__LINE__]];
if (_adjustmentsChangedDisposable) {
[_adjustmentsChangedDisposable dispose];
}
_adjustmentsChangedDisposable = [[SMetaDisposable alloc] init];
[_adjustmentsChangedDisposable setDisposable:[_editingContext.adjustmentsUpdatedSignal startStrictWithNext:^(__unused NSNumber *next)
{
@ -583,6 +586,7 @@
self.delegate = nil;
[_selectionChangedDisposable dispose];
[_tooltipDismissDisposable dispose];
[_adjustmentsChangedDisposable dispose];
}
- (void)loadView

View file

@ -222,7 +222,7 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle:
})
}
public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController {
public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -230,18 +230,20 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl
let recipientName: String
if let threadTitle {
recipientName = threadTitle
} else {
} else if let peer {
if peer.id == context.account.peerId {
recipientName = presentationData.strings.DialogList_SavedMessages
} else {
recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else {
recipientName = ""
}
let actionSheetTheme = ActionSheetControllerTheme(presentationData: presentationData)
let fontSize = floor(actionSheetTheme.baseFontSize * 20.0 / 17.0)
let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat
let isSecretChat = peer?.id.namespace == Namespaces.Peer.SecretChat
let controller = TGMenuSheetController(context: parentController.context, dark: false)!
controller.dismissesByOutsideTap = true
@ -314,14 +316,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl
carouselItem.selectionLimitExceeded = {
presentSelectionLimitExceeded()
}
if peer.id != context.account.peerId {
if let peer, peer.id != context.account.peerId {
if peer is TelegramUser {
carouselItem.hasTimer = hasSchedule
}
carouselItem.hasSilentPosting = true
}
carouselItem.hasSchedule = hasSchedule
carouselItem.reminder = peer.id == context.account.peerId
carouselItem.reminder = peer?.id == context.account.peerId
carouselItem.presentScheduleController = { media, done in
presentSchedulePicker(media, { time in
done?(time)
@ -449,7 +451,12 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
let recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let recipientName: String
if let peer {
recipientName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} else {
recipientName = ""
}
legacyController.enableSizeClassSignal = true
@ -489,12 +496,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl
itemViews.append(locationItem)
var peerSupportsPolls = false
if peer is TelegramGroup || peer is TelegramChannel {
peerSupportsPolls = true
} else if let user = peer as? TelegramUser, let _ = user.botInfo {
peerSupportsPolls = true
if let peer {
if peer is TelegramGroup || peer is TelegramChannel {
peerSupportsPolls = true
} else if let user = peer as? TelegramUser, let _ = user.botInfo {
peerSupportsPolls = true
}
}
if peerSupportsPolls && canSendMessagesToPeer(peer) && canSendPolls {
if let peer, peerSupportsPolls, canSendMessagesToPeer(peer) && canSendPolls {
let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openPoll()

View file

@ -595,7 +595,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if case let .selectable(selected) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth
}

View file

@ -259,7 +259,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if case let .selectable(selected) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false)
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth
}

View file

@ -187,7 +187,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab
if ["home", "work"].contains(venueType) {
completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil)
} else {
completion(venue, queryId, resultId, nil, nil)
completion(venue, queryId, resultId, venue.venue?.address, nil)
}
strongSelf.dismiss()
}, toggleMapModeSelection: { [weak self] in

View file

@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
})
if case let .share(_, selfPeer, _) = self.mode {
switch self.mode {
case let .share(_, selfPeer, _):
if let selfPeer {
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer)
}
self.headerNode.mapNode.hasPickerAnnotation = true
case .pick:
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true)
self.headerNode.mapNode.hasPickerAnnotation = true
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in

View file

@ -224,7 +224,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
})
}
}
if !isScheduledMessages {
if !isScheduledMessages && peer != nil {
model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in
if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext {
var effectiveHasSchedule = hasSchedule
@ -269,8 +269,8 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
}
let _ = (sendWhenOnlineAvailable
|> take(1)
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
|> take(1)
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer)
let dismissImpl = { [weak model] in

View file

@ -155,6 +155,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
case wallpaper
case story
case addImage
case createSticker
}
case assets(PHAssetCollection?, AssetsMode)
@ -273,7 +274,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.presentationData = controller.presentationData
var assetType: PHAssetMediaType?
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) {
assetType = .image
}
let mediaAssetsContext = MediaAssetsContext(assetType: assetType)
@ -432,7 +433,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) {
} else {
let selectionGesture = MediaPickerGridSelectionGesture<TGMediaSelectableItem>()
@ -1566,7 +1567,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
} else {
switch mode {
case .default:
case .default, .createSticker:
self.titleView.title = presentationData.strings.MediaPicker_Recents
self.titleView.isEnabled = true
case .story:
@ -2258,15 +2259,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return self.controllerNode.defaultTransitionView()
}
fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource)
}
fileprivate func transitionImage(for identifier: String) -> UIImage? {
public func transitionImage(for identifier: String) -> UIImage? {
return self.controllerNode.transitionImage(for: identifier)
}
func updateHiddenMediaId(_ id: String?) {
public func updateHiddenMediaId(_ id: String?) {
self.controllerNode.hiddenMediaId.set(.single(id))
}

View file

@ -5,7 +5,6 @@ import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import CoreTelephony
import TelegramPresentationData
import AccountContext
import AlertUI
@ -382,17 +381,9 @@ extension SecureIdPlaintextFormInnerState {
init(type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?) {
switch type {
case .phone:
var countryId: String? = nil
if let carrier = CTTelephonyNetworkInfo().serviceSubscriberCellularProviders?.values.first {
countryId = carrier.isoCountryCode
}
if countryId == nil {
countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
}
let countryId = (Locale.current as NSLocale).object(forKey: .countryCode) as? String
var countryCodeAndId: (Int32, String) = (1, "US")
if let countryId = countryId {
let normalizedId = countryId.uppercased()
for (code, idAndName) in countryCodeToIdAndName {

View file

@ -510,7 +510,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
} else {
arguments.openAddress(value)
}
}, longTapAction: {
}, longTapAction: { _, _ in
if selected == nil {
arguments.displayCopyContextMenu(.info(index), string)
}

View file

@ -1,6 +1,6 @@
import Foundation
public enum AdditionalMessageHistoryViewData {
public enum AdditionalMessageHistoryViewData: Equatable {
case cachedPeerData(PeerId)
case cachedPeerDataMessages(PeerId)
case peerChatState(PeerId)

View file

@ -4,7 +4,7 @@ import SwiftSignalKit
public enum ChatLocationInput {
case peer(peerId: PeerId, threadId: Int64?)
case thread(peerId: PeerId, threadId: Int64, data: Signal<MessageHistoryViewExternalInput, NoError>)
case feed(id: Int32, data: Signal<MessageHistoryViewExternalInput, NoError>)
case customChatContents
}
public extension ChatLocationInput {
@ -14,7 +14,7 @@ public extension ChatLocationInput {
return peerId
case let .thread(peerId, _, _):
return peerId
case .feed:
case .customChatContents:
return nil
}
}
@ -25,7 +25,7 @@ public extension ChatLocationInput {
return threadId
case let .thread(_, threadId, _):
return threadId
case .feed:
case .customChatContents:
return nil
}
}

View file

@ -639,6 +639,10 @@ public extension MessageAttribute {
public struct MessageGroupInfo: Equatable {
public let stableId: UInt32
public init(stableId: UInt32) {
self.stableId = stableId
}
}
public final class Message {
@ -732,6 +736,14 @@ public final class Message {
self.associatedStories = associatedStories
}
public func withUpdatedStableId(stableId: UInt32) -> Message {
return Message(stableId: stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories)
}
public func withUpdatedId(id: MessageId) -> Message {
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories)
}
public func withUpdatedStableVersion(stableVersion: UInt32) -> Message {
return Message(stableId: self.stableId, stableVersion: stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: self.threadId, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, customTags: self.customTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds, associatedMedia: self.associatedMedia, associatedThreadInfo: self.associatedThreadInfo, associatedStories: self.associatedStories)
}
@ -1017,7 +1029,7 @@ final class InternalStoreMessage {
}
}
public enum MessageIdNamespaces {
public enum MessageIdNamespaces: Equatable {
case all
case just(Set<MessageId.Namespace>)
case not(Set<MessageId.Namespace>)
@ -1033,3 +1045,23 @@ public enum MessageIdNamespaces {
}
}
}
public struct PeerAndThreadId: Hashable {
public var peerId: PeerId
public var threadId: Int64?
public init(peerId: PeerId, threadId: Int64?) {
self.peerId = peerId
self.threadId = threadId
}
}
public struct MessageAndThreadId: Hashable {
public var messageId: MessageId
public var threadId: Int64?
public init(messageId: MessageId, threadId: Int64?) {
self.messageId = messageId
self.threadId = threadId
}
}

View file

@ -237,7 +237,7 @@ public enum MessageHistoryViewRelativeHoleDirection: Equatable, Hashable, Custom
}
}
public struct MessageHistoryViewOrderStatistics: OptionSet {
public struct MessageHistoryViewOrderStatistics: OptionSet, Equatable {
public var rawValue: Int32
public init(rawValue: Int32) {
@ -290,7 +290,7 @@ public enum MessageHistoryViewInput: Equatable {
case external(MessageHistoryViewExternalInput)
}
public enum MessageHistoryViewReadState {
public enum MessageHistoryViewReadState: Equatable {
case peer([PeerId: CombinedPeerReadState])
}
@ -302,7 +302,7 @@ public enum HistoryViewInputAnchor: Equatable {
case unread
}
final class MutableMessageHistoryView {
final class MutableMessageHistoryView: MutablePostboxView {
private(set) var peerIds: MessageHistoryViewInput
private let ignoreMessagesInTimestampRange: ClosedRange<Int32>?
let tag: HistoryViewInputTag?
@ -310,6 +310,7 @@ final class MutableMessageHistoryView {
let namespaces: MessageIdNamespaces
private let orderStatistics: MessageHistoryViewOrderStatistics
private let clipHoles: Bool
private let trackHoles: Bool
private let anchor: HistoryViewInputAnchor
fileprivate var combinedReadStates: MessageHistoryViewReadState?
@ -333,6 +334,7 @@ final class MutableMessageHistoryView {
postbox: PostboxImpl,
orderStatistics: MessageHistoryViewOrderStatistics,
clipHoles: Bool,
trackHoles: Bool,
peerIds: MessageHistoryViewInput,
ignoreMessagesInTimestampRange: ClosedRange<Int32>?,
anchor inputAnchor: HistoryViewInputAnchor,
@ -343,13 +345,13 @@ final class MutableMessageHistoryView {
namespaces: MessageIdNamespaces,
count: Int,
topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?],
additionalDatas: [AdditionalMessageHistoryViewDataEntry],
getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32
additionalDatas: [AdditionalMessageHistoryViewDataEntry]
) {
self.anchor = inputAnchor
self.orderStatistics = orderStatistics
self.clipHoles = clipHoles
self.trackHoles = trackHoles
self.peerIds = peerIds
self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange
self.combinedReadStates = combinedReadStates
@ -1040,6 +1042,10 @@ final class MutableMessageHistoryView {
}
func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection, Int, Int64?)? {
if !self.trackHoles {
return nil
}
switch self.sampledState {
case let .loading(loadingSample):
switch loadingSample {
@ -1065,9 +1071,13 @@ final class MutableMessageHistoryView {
}
}
}
func immutableView() -> PostboxView {
return MessageHistoryView(self)
}
}
public final class MessageHistoryView {
public final class MessageHistoryView: PostboxView {
public let tag: HistoryViewInputTag?
public let namespaces: MessageIdNamespaces
public let anchorIndex: MessageHistoryAnchorIndex
@ -1101,7 +1111,7 @@ public final class MessageHistoryView {
self.topTaggedMessages = []
self.additionalData = []
self.isLoading = isLoading
self.isLoadingEarlier = true
self.isLoadingEarlier = false
self.isAddedToChatList = false
self.peerStoryStats = [:]
}

View file

@ -60,7 +60,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView {
}
}
self.anchor = anchor
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0})
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [])
let _ = self.updateFromView()
}
@ -134,7 +134,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView {
case let .peer(id, threadId):
peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false)
}
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0})
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [])
return self.updateFromView()
} else if self.wrappedView.replay(postbox: postbox, transaction: transaction) {
var reloadView = false
@ -167,7 +167,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView {
case let .peer(id, threadId):
peerIds = postbox.peerIdsForLocation(.peer(peerId: id, threadId: threadId), ignoreRelatedChats: false)
}
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0})
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, trackHoles: false, peerIds: peerIds, ignoreMessagesInTimestampRange: nil, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [])
}
return self.updateFromView()

View file

@ -2997,6 +2997,8 @@ final class PostboxImpl {
private func internalTransaction<T>(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) {
let _ = self.isInTransaction.swap(true)
let startTime = CFAbsoluteTimeGetCurrent()
self.valueBox.begin()
let transaction = Transaction(queue: self.queue, postbox: self)
self.afterBegin(transaction: transaction)
@ -3005,6 +3007,12 @@ final class PostboxImpl {
transaction.disposed = true
self.valueBox.commit()
let endTime = CFAbsoluteTimeGetCurrent()
let transactionDuration = endTime - startTime
if transactionDuration > 0.1 {
postboxLog("Postbox transaction took \(transactionDuration * 1000.0) ms")
}
let _ = self.isInTransaction.swap(false)
if let currentUpdatedState = self.currentUpdatedState {
@ -3079,7 +3087,7 @@ final class PostboxImpl {
switch chatLocation {
case let .peer(peerId, threadId):
return .single((.peer(peerId: peerId, threadId: threadId), false))
case .thread(_, _, let data), .feed(_, let data):
case .thread(_, _, let data):
return Signal { subscriber in
var isHoleFill = false
return (data
@ -3089,6 +3097,9 @@ final class PostboxImpl {
return (.external(value), wasHoleFill)
}).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
case .customChatContents:
assert(false)
return .never()
}
}
@ -3333,13 +3344,7 @@ final class PostboxImpl {
readStates = transientReadStates
}
let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in
if case let .tag(tagMask) = tag {
return Int32(self.messageHistoryTable.getMessageCountInRange(peerId: lowerBound.id.peerId, namespace: lowerBound.id.namespace, tag: tagMask, lowerBound: lowerBound, upperBound: upperBound))
} else {
return 0
}
})
let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, trackHoles: true, peerIds: peerIds, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries)
let initialUpdateType: ViewUpdateType = .Initial

View file

@ -1,9 +1,9 @@
import Foundation
public protocol PostboxView {
public protocol PostboxView: AnyObject {
}
protocol MutablePostboxView {
protocol MutablePostboxView: AnyObject {
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool
func immutableView() -> PostboxView
@ -16,14 +16,71 @@ final class CombinedMutableView {
self.views = views
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> (updated: Bool, updateTrackedHoles: Bool) {
var anyUpdated = false
var updateTrackedHoles = false
for (_, view) in self.views {
if view.replay(postbox: postbox, transaction: transaction) {
updated = true
if let mutableView = view as? MutableMessageHistoryView {
var innerUpdated = false
let previousPeerIds = mutableView.peerIds
if mutableView.replay(postbox: postbox, transaction: transaction) {
innerUpdated = true
}
var updateType: ViewUpdateType = .Generic
switch mutableView.peerIds {
case let .single(peerId, threadId):
for key in transaction.currentPeerHoleOperations.keys {
if key.peerId == peerId && key.threadId == threadId {
updateType = .FillHole
break
}
}
case .associated:
var ids = Set<PeerId>()
switch mutableView.peerIds {
case .single, .external:
assertionFailure()
case let .associated(mainPeerId, associatedId):
ids.insert(mainPeerId)
if let associatedId = associatedId {
ids.insert(associatedId.peerId)
}
}
if !ids.isEmpty {
for key in transaction.currentPeerHoleOperations.keys {
if ids.contains(key.peerId) {
updateType = .FillHole
break
}
}
}
case .external:
break
}
mutableView.updatePeerIds(transaction: transaction)
if mutableView.peerIds != previousPeerIds {
updateType = .UpdateVisible
let _ = mutableView.refreshDueToExternalTransaction(postbox: postbox)
innerUpdated = true
}
if innerUpdated {
anyUpdated = true
updateTrackedHoles = true
let _ = updateType
//pipe.putNext((MessageHistoryView(mutableView), updateType))
}
} else if view.replay(postbox: postbox, transaction: transaction) {
anyUpdated = true
}
}
return updated
return (anyUpdated, updateTrackedHoles)
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {

View file

@ -1,7 +1,7 @@
import Foundation
import SwiftSignalKit
public enum ViewUpdateType : Equatable {
public enum ViewUpdateType: Equatable {
case Initial
case InitialUnread(MessageIndex)
case Generic
@ -351,10 +351,6 @@ final class ViewTracker {
self.updateTrackedChatListHoles()
if updateTrackedHoles {
self.updateTrackedHoles()
}
if self.unsentMessageView.replay(transaction.unsentMessageOperations) {
self.unsentViewUpdated()
}
@ -414,9 +410,14 @@ final class ViewTracker {
}
for (mutableView, pipe) in self.combinedViews.copyItems() {
if mutableView.replay(postbox: postbox, transaction: transaction) {
let result = mutableView.replay(postbox: postbox, transaction: transaction)
if result.updated {
pipe.putNext(mutableView.immutableView())
}
if result.updateTrackedHoles {
updateTrackedHoles = true
}
}
for (view, pipe) in self.failedMessageIdsViews.copyItems() {
@ -425,6 +426,10 @@ final class ViewTracker {
}
}
if updateTrackedHoles {
self.updateTrackedHoles()
}
self.updateTrackedForumTopicListHoles()
}
@ -486,6 +491,26 @@ final class ViewTracker {
firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId))
}
}
for (view, _) in self.combinedViews.copyItems() {
for (_, subview) in view.views {
if let subview = subview as? MutableMessageHistoryView {
if let (hole, direction, count, userId) = subview.firstHole() {
let space: MessageHistoryHoleOperationSpace
if let tag = subview.tag {
switch tag {
case let .tag(value):
space = .tag(value)
case let .customTag(value, regularTag):
space = .customTag(value, regularTag)
}
} else {
space = .everywhere
}
firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId))
}
}
}
}
if self.messageHistoryHolesView.update(firstHolesAndTags) {
for subscriber in self.messageHistoryHolesViewSubscribers.copyItems() {

View file

@ -1,6 +1,52 @@
import Foundation
public enum PostboxViewKey: Hashable {
public struct HistoryView: Equatable {
public var peerId: PeerId
public var threadId: Int64?
public var clipHoles: Bool
public var trackHoles: Bool
public var orderStatistics: MessageHistoryViewOrderStatistics
public var ignoreMessagesInTimestampRange: ClosedRange<Int32>?
public var anchor: HistoryViewInputAnchor
public var combinedReadStates: MessageHistoryViewReadState?
public var transientReadStates: MessageHistoryViewReadState?
public var tag: HistoryViewInputTag?
public var appendMessagesFromTheSameGroup: Bool
public var namespaces: MessageIdNamespaces
public var count: Int
public init(
peerId: PeerId,
threadId: Int64?,
clipHoles: Bool,
trackHoles: Bool,
orderStatistics: MessageHistoryViewOrderStatistics = [],
ignoreMessagesInTimestampRange: ClosedRange<Int32>? = nil,
anchor: HistoryViewInputAnchor,
combinedReadStates: MessageHistoryViewReadState? = nil,
transientReadStates: MessageHistoryViewReadState? = nil,
tag: HistoryViewInputTag? = nil,
appendMessagesFromTheSameGroup: Bool,
namespaces: MessageIdNamespaces,
count: Int
) {
self.peerId = peerId
self.threadId = threadId
self.clipHoles = clipHoles
self.trackHoles = trackHoles
self.orderStatistics = orderStatistics
self.ignoreMessagesInTimestampRange = ignoreMessagesInTimestampRange
self.anchor = anchor
self.combinedReadStates = combinedReadStates
self.transientReadStates = transientReadStates
self.tag = tag
self.appendMessagesFromTheSameGroup = appendMessagesFromTheSameGroup
self.namespaces = namespaces
self.count = count
}
}
case itemCollectionInfos(namespaces: [ItemCollectionId.Namespace])
case itemCollectionIds(namespaces: [ItemCollectionId.Namespace])
case itemCollectionInfo(id: ItemCollectionId)
@ -50,6 +96,7 @@ public enum PostboxViewKey: Hashable {
case savedMessagesIndex(peerId: PeerId)
case savedMessagesStats(peerId: PeerId)
case chatInterfaceState(peerId: PeerId)
case historyView(HistoryView)
public func hash(into hasher: inout Hasher) {
switch self {
@ -168,6 +215,10 @@ public enum PostboxViewKey: Hashable {
hasher.combine(peerId)
case let .chatInterfaceState(peerId):
hasher.combine(peerId)
case let .historyView(historyView):
hasher.combine(20)
hasher.combine(historyView.peerId)
hasher.combine(historyView.threadId)
}
}
@ -467,6 +518,12 @@ public enum PostboxViewKey: Hashable {
} else {
return false
}
case let .historyView(historyView):
if case .historyView(historyView) = rhs {
return true
} else {
return false
}
}
}
}
@ -571,5 +628,23 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
return MutableMessageHistorySavedMessagesStatsView(postbox: postbox, peerId: peerId)
case let .chatInterfaceState(peerId):
return MutableChatInterfaceStateView(postbox: postbox, peerId: peerId)
case let .historyView(historyView):
return MutableMessageHistoryView(
postbox: postbox,
orderStatistics: historyView.orderStatistics,
clipHoles: historyView.clipHoles,
trackHoles: historyView.trackHoles,
peerIds: .single(peerId: historyView.peerId, threadId: historyView.threadId),
ignoreMessagesInTimestampRange: historyView.ignoreMessagesInTimestampRange,
anchor: historyView.anchor,
combinedReadStates: historyView.combinedReadStates,
transientReadStates: historyView.transientReadStates,
tag: historyView.tag,
appendMessagesFromTheSameGroup: historyView.appendMessagesFromTheSameGroup,
namespaces: historyView.namespaces,
count: historyView.count,
topTaggedMessages: [:],
additionalDatas: []
)
}
}

View file

@ -112,6 +112,10 @@ swift_library(
"//submodules/AlertUI",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EntityKeyboard",
],
visibility = [
"//visibility:public",

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Before After
Before After

View file

@ -0,0 +1,61 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
private let sceneVersion: Int = 1
final class BadgeBusinessView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "business", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View file

@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "badge", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "emoji", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "tag", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View file

@ -12,7 +12,7 @@ import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
public final class BoostHeaderBackgroundComponent: Component {
let isVisible: Bool
@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component {
private func setup() {
guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else {
return
}

View file

@ -0,0 +1,652 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import MultilineTextComponent
import BlurredBackgroundComponent
import Markdown
import TelegramPresentationData
import BundleIconComponent
private final class HeaderComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
self.context = context
self.theme = theme
self.strings = strings
}
static func ==(lhs: HeaderComponent, rhs: HeaderComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
private let coin = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var component: HeaderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: HeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let containerSize = CGSize(width: min(414.0, availableSize.width), height: 220.0)
let coinSize = self.coin.update(
transition: .immediate,
component: AnyComponent(PremiumCoinComponent(isIntro: true, isVisible: true, hasIdleAnimations: true)),
environment: {},
containerSize: containerSize
)
if let view = self.coin.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - coinSize.width) / 2.0), y: -84.0), size: coinSize)
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Premium_Business_Description, font: Font.regular(15.0), textColor: .black)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 1000.0)
)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) / 2.0), y: 139.0), size: textSize)
}
return CGSize(width: availableSize.width, height: 210.0)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let iconName: String
let iconColor: UIColor
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
iconName: String,
iconColor: UIColor
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.iconName = iconName
self.iconColor = iconColor
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 64.0
let rightInset: CGFloat = 32.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0)
}
}
}
private final class BusinessListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
let topInset: CGFloat
let bottomInset: CGFloat
init(context: AccountContext, theme: PresentationTheme, topInset: CGFloat, bottomInset: CGFloat) {
self.context = context
self.theme = theme
self.topInset = topInset
self.bottomInset = bottomInset
}
static func ==(lhs: BusinessListComponent, rhs: BusinessListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var limits: EngineConfiguration.UserLimits = .defaultValue
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
var accountPeer: EnginePeer?
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in
if let strongSelf = self {
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.accountPeer = accountPeer
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let list = Child(List<Empty>.self)
return { context in
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let colors = [
UIColor(rgb: 0x007aff),
UIColor(rgb: 0x798aff),
UIColor(rgb: 0xac64f3),
UIColor(rgb: 0xc456ae),
UIColor(rgb: 0xe95d44),
UIColor(rgb: 0xf2822a),
UIColor(rgb: 0xe79519),
UIColor(rgb: 0xe7ad19)
]
let titleColor = theme.list.itemPrimaryTextColor
let textColor = theme.list.itemSecondaryTextColor
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "header",
component: AnyComponent(HeaderComponent(
context: context.component.context,
theme: theme,
strings: strings
))
)
)
items.append(
AnyComponentWithIdentity(
id: "location",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Location_Title,
titleColor: titleColor,
text: strings.Premium_Business_Location_Text,
textColor: textColor,
iconName: "Premium/Business/Location",
iconColor: colors[0]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "hours",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Hours_Title,
titleColor: titleColor,
text: strings.Premium_Business_Hours_Text,
textColor: textColor,
iconName: "Premium/Business/Hours",
iconColor: colors[1]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "replies",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Replies_Title,
titleColor: titleColor,
text: strings.Premium_Business_Replies_Text,
textColor: textColor,
iconName: "Premium/Business/Replies",
iconColor: colors[2]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "greetings",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Greetings_Title,
titleColor: titleColor,
text: strings.Premium_Business_Greetings_Text,
textColor: textColor,
iconName: "Premium/Business/Greetings",
iconColor: colors[3]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "away",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Away_Title,
titleColor: titleColor,
text: strings.Premium_Business_Away_Text,
textColor: textColor,
iconName: "Premium/Business/Away",
iconColor: colors[4]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "chatbots",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Chatbots_Title,
titleColor: titleColor,
text: strings.Premium_Business_Chatbots_Text,
textColor: textColor,
iconName: "Premium/Business/Chatbots",
iconColor: colors[5]
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
transition: context.transition
)
let contentHeight = context.component.topInset - 56.0 + list.size.height + context.component.bottomInset
context.add(list
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
)
return CGSize(width: context.availableSize.width, height: contentHeight)
}
}
}
final class BusinessPageComponent: CombinedComponent {
typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
let theme: PresentationTheme
let neighbors: PageNeighbors
let bottomInset: CGFloat
let updatedBottomAlpha: (CGFloat) -> Void
let updatedDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
init(context: AccountContext, theme: PresentationTheme, neighbors: PageNeighbors, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
self.context = context
self.theme = theme
self.neighbors = neighbors
self.bottomInset = bottomInset
self.updatedBottomAlpha = updatedBottomAlpha
self.updatedDismissOffset = updatedDismissOffset
self.updatedIsDisplaying = updatedIsDisplaying
}
static func ==(lhs: BusinessPageComponent, rhs: BusinessPageComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.neighbors != rhs.neighbors {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
final class State: ComponentState {
let updateBottomAlpha: (CGFloat) -> Void
let updateDismissOffset: (CGFloat) -> Void
let updatedIsDisplaying: (Bool) -> Void
var resetScroll: ActionSlot<Void>?
var topContentOffset: CGFloat = 0.0
var bottomContentOffset: CGFloat = 100.0 {
didSet {
self.updateAlpha()
}
}
var position: CGFloat? {
didSet {
self.updateAlpha()
}
}
var isDisplaying = false {
didSet {
if oldValue != self.isDisplaying {
self.updatedIsDisplaying(self.isDisplaying)
if !self.isDisplaying {
self.resetScroll?.invoke(Void())
}
}
}
}
var neighbors = PageNeighbors(leftIsList: false, rightIsList: false)
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
self.updateBottomAlpha = updateBottomAlpha
self.updateDismissOffset = updateDismissOffset
self.updatedIsDisplaying = updateIsDisplaying
super.init()
}
func updateAlpha() {
var dismissToLeft = false
if let position = self.position, position > 0.0 {
dismissToLeft = true
}
var dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
var position = min(1.0, abs(self.position ?? 0.0))
if position > 0.001, (dismissToLeft && self.neighbors.leftIsList) || (!dismissToLeft && self.neighbors.rightIsList) {
dismissPosition = 0.0
position = 1.0
}
self.updateDismissOffset(dismissPosition)
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
let backgroundAlpha: CGFloat = max(position, verticalPosition)
self.updateBottomAlpha(backgroundAlpha)
}
}
func makeState() -> State {
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
}
static var body: Body {
let background = Child(Rectangle.self)
let scroll = Child(ScrollComponent<Empty>.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let resetScroll = ActionSlot<Void>()
return { context in
let state = context.state
let environment = context.environment[DemoPageEnvironment.self].value
state.neighbors = context.component.neighbors
state.resetScroll = resetScroll
state.position = environment.position
state.isDisplaying = environment.isDisplaying
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let topInset: CGFloat = 56.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
BusinessListComponent(
context: context.component.context,
theme: theme,
topInset: topInset,
bottomInset: context.component.bottomInset + 110.0
)
),
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { _ in },
resetScroll: resetScroll
),
availableSize: context.availableSize,
transition: context.transition
)
let background = background.update(
component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor),
availableSize: scroll.size,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
)
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: topInset),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Premium_Business, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 1
),
availableSize: context.availableSize,
transition: context.transition
)
let topPanelAlpha: CGFloat
if state.topContentOffset > 78.0 {
topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0
} else {
topPanelAlpha = 0.0
}
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
let titleTopOriginY = topPanel.size.height / 2.0
let titleBottomOriginY: CGFloat = 176.0
let titleOriginDelta = titleTopOriginY - titleBottomOriginY
let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta))
let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta
let titleScale = 1.0 - max(0.0, fraction * 0.2)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY))
.scale(titleScale)
)
return scroll.size
}
}
}

View file

@ -13,8 +13,6 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
private let sceneVersion: Int = 3
class EmojiHeaderComponent: Component {
let context: AccountContext
let animationCache: AnimationCache

View file

@ -5,6 +5,8 @@ import Display
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View file

@ -14,7 +14,7 @@ import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
final class GiftAvatarComponent: Component {
let context: AccountContext
@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component {
}
private func setup() {
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else {
return
}

View file

@ -296,7 +296,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
var editingOffset: CGFloat = 0.0
if let isSelected = item.isSelected {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, false)
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, isSelected, .regular)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
}
@ -309,6 +309,10 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
textConstrainedWidth -= 54.0
subtitleConstrainedWidth -= 30.0
}
if let _ = item.titleBadge {
textConstrainedWidth -= 32.0
subtitleConstrainedWidth -= 32.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude)))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: subtitleConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))

Some files were not shown because too many files have changed in this diff Show more