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
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
|||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
|
||||
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
arguments.openAddress(value)
|
||||
}
|
||||
}, longTapAction: {
|
||||
}, longTapAction: { _, _ in
|
||||
if selected == nil {
|
||||
arguments.displayCopyContextMenu(.info(index), string)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public enum AdditionalMessageHistoryViewData {
|
||||
public enum AdditionalMessageHistoryViewData: Equatable {
|
||||
case cachedPeerData(PeerId)
|
||||
case cachedPeerDataMessages(PeerId)
|
||||
case peerChatState(PeerId)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [:]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
BIN
submodules/PremiumUI/Resources/badge
Normal file
BIN
submodules/PremiumUI/Resources/boost
Normal file
BIN
submodules/PremiumUI/Resources/business.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
submodules/PremiumUI/Resources/business.scn
Normal file
BIN
submodules/PremiumUI/Resources/coin
Normal file
BIN
submodules/PremiumUI/Resources/coin_anim.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
submodules/PremiumUI/Resources/coin_edge.png
Normal file
|
After Width: | Height: | Size: 332 B |
BIN
submodules/PremiumUI/Resources/darkerTexture.jpg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
submodules/PremiumUI/Resources/diagonal_shine.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
submodules/PremiumUI/Resources/emoji
Normal file
BIN
submodules/PremiumUI/Resources/gift
Normal file
BIN
submodules/PremiumUI/Resources/lighterTexture.jpg
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
submodules/PremiumUI/Resources/lightspeed
Normal file
BIN
submodules/PremiumUI/Resources/swirl
Normal file
BIN
submodules/PremiumUI/Resources/tag
Normal file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 9 KiB |
61
submodules/PremiumUI/Sources/BadgeBusinessView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
652
submodules/PremiumUI/Sources/BusinessPageComponent.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,6 @@ import AnimationCache
|
|||
import MultiAnimationRenderer
|
||||
import EmojiStatusComponent
|
||||
|
||||
private let sceneVersion: Int = 3
|
||||
|
||||
class EmojiHeaderComponent: Component {
|
||||
let context: AccountContext
|
||||
let animationCache: AnimationCache
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||