Various improvements

This commit is contained in:
Ilya Laktyushin 2023-12-13 01:00:55 +04:00
parent 44b0464161
commit ef28f034d8
47 changed files with 1703 additions and 411 deletions

View file

@ -10637,3 +10637,50 @@ Sorry for the inconvenience.";
"BoostGift.WinnersInfo" = "Choose whether to make the list of winners public when the giveaway ends.";
"Story.ViewList.TitleReactions" = "Reactions";
"Wallpaper.ApplyForChannel" = "Apply For This Channel";
"Notification.ChannelChangedWallpaper" = "Channel set a new wallpaper";
"Chat.Giveaway.Message.WinnersSelectedTitle.One" = "Winner Selected!";
"Chat.Giveaway.Message.WinnersSelectedTitle.Many" = "Winners Selected!";
"Chat.Giveaway.Message.WinnersSelectedText_1" = "**%@** winner of the [Giveaway]() was randomly selected by Telegram.";
"Chat.Giveaway.Message.WinnersSelectedText_any" = "**%@** winners of the [Giveaway]() were randomly selected by Telegram.";
"Chat.Giveaway.Message.WinnersTitle.One" = "Winner";
"Chat.Giveaway.Message.WinnersTitle.Many" = "Winners";
"Chat.Giveaway.Message.WinnersMore_1" = "and %@ more!";
"Chat.Giveaway.Message.WinnersMore_any" = "and %@ more!";
"Chat.Giveaway.Message.WinnersInfo.One" = "The winner received their gift link in a private message.";
"Chat.Giveaway.Message.WinnersInfo.Many" = "All winners received gift links in private messages.";
"ChannelBoost.Wallpaper" = "Set Wallpaper";
"ChannelBoost.WallpaperText" = "Your channel needs **Level %1$@** to set custom wallpaper.\n\nAsk your **Premium** subscribers to boost your channel with this link:";
"Settings.PremiumGift" = "Premium Gifting";
"Story.ViewList.ContextSortChannelInfo" = "Choose the order for the list of reactions.";
"Premium.Gift.MultipleDescription" = "Get **%1$@**%2$@ access to exclusive features with **Telegram Premium**.";
"Premium.Gift.NamesAndMore_1" = " and %@ more";
"Premium.Gift.NamesAndMore_any" = " and %@ more";
"Premium.Gift.YouWillReceiveBoosts_1" = "You will receive []() **%@ Boost**!";
"Premium.Gift.YouWillReceiveBoosts_any" = "You will receive []() **%@ Boosts**!";
"Premium.Gift.ContactSelection.Title" = "Gift Premium";
"Premium.Gift.ContactSelection.Placeholder" = "Search people to gift Premium to...";
"Premium.Gift.ContactSelection.Proceed" = "Proceed";
"Premium.Gift.ContactSelection.FrequentContacts" = "FREQUENT CONTACTS";
"Premium.Gift.ContactSelection.MaximumReached" = "You can select up to %@ users.";
"Premium.Gift.GiftMultipleSubscriptionsFormat" = "%1$@ for %2$@";
"Premium.Gift.GiftMultipleSubscriptions_1" = "Gift %@ Subscription";
"Premium.Gift.GiftMultipleSubscriptions_any" = "Gift %@ Subscriptions";
"Message.GiveawayEndedWinners_1" = "%@ winner of the giveaway was randomly selected by Telegram";
"Message.GiveawayEndedWinners_any" = "%@ winners of the giveaway was randomly selected by Telegram";
"Message.GiveawayEndedNoWinners" = "Due to the giveaway terms, no winners could be selected by Telegram";

View file

@ -1101,7 +1101,7 @@ public protocol AccountContext: AnyObject {
public struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2)
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, minChannelWallpaperLevel: 5)
}
public let isPremiumDisabled: Bool
@ -1112,8 +1112,9 @@ public struct PremiumConfiguration {
public let minChannelNameColorLevel: Int32
public let audioTransciptionTrialMaxDuration: Int32
public let audioTransciptionTrialCount: Int32
public let minChannelWallpaperLevel: Int32
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32) {
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, minChannelWallpaperLevel: Int32) {
self.isPremiumDisabled = isPremiumDisabled
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
self.showPremiumGiftInTextField = showPremiumGiftInTextField
@ -1122,6 +1123,7 @@ public struct PremiumConfiguration {
self.minChannelNameColorLevel = minChannelNameColorLevel
self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration
self.audioTransciptionTrialCount = audioTransciptionTrialCount
self.minChannelWallpaperLevel = minChannelWallpaperLevel
}
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
@ -1134,7 +1136,8 @@ public struct PremiumConfiguration {
boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3),
minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5),
audioTransciptionTrialMaxDuration: Int32(data["transcribe_audio_trial_duration_max"] as? Double ?? 300),
audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2)
audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2),
minChannelWallpaperLevel: Int32(data["channel_wallpaper_level_min"] as? Double ?? 5)
)
} else {
return .defaultValue

View file

@ -69,6 +69,7 @@ public enum ContactMultiselectionControllerMode {
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection(ChatSelection)
case premiumGifting
}
public enum ContactListFilter {

View file

@ -311,6 +311,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
} else {
messageText = strings.Message_GiveawayStarted
}
case let results as TelegramMediaGiveawayResults:
if results.winnersCount == 0 {
messageText = strings.Message_GiveawayEndedNoWinners
} else {
messageText = strings.Message_GiveawayEndedWinners(results.winnersCount)
}
case let webpage as TelegramMediaWebpage:
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
messageText = content.displayUrl

View file

@ -11,16 +11,18 @@ public final class RoundedRectangle: Component {
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) {
self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke)
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) {
public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
self.colors = colors
self.cornerRadius = cornerRadius
self.gradientDirection = gradientDirection
self.stroke = stroke
self.strokeColor = strokeColor
}
public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool {
@ -36,6 +38,9 @@ public final class RoundedRectangle: Component {
if lhs.stroke != rhs.stroke {
return false
}
if lhs.strokeColor != rhs.strokeColor {
return false
}
return true
}
@ -48,11 +53,19 @@ public final class RoundedRectangle: Component {
let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(color.cgColor)
if let strokeColor = component.strokeColor {
context.setFillColor(strokeColor.cgColor)
} else {
context.setFillColor(color.cgColor)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize))
if let stroke = component.stroke, stroke > 0.0 {
context.setBlendMode(.clear)
if let _ = component.strokeColor {
context.setFillColor(color.cgColor)
} else {
context.setBlendMode(.clear)
}
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke))
}
}

View file

@ -358,7 +358,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
}
}
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?) -> [ContactListNodeEntry] {
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer]) -> [ContactListNodeEntry] {
var entries: [ContactListNodeEntry] = []
var commonHeader: ListViewItemHeader?
@ -421,7 +421,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
for i in 0 ..< options.count {
entries.append(.option(i, options[i], commonHeader, theme, strings))
}
case let .natural(options, _):
case let .natural(options, _, _):
let sortedPeers = peers.sorted(by: { lhs, rhs in
let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder)
if result == .orderedSame {
@ -513,6 +513,27 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
}
}
var existingPeerIds = Set<ContactListPeerId>()
if !topPeers.isEmpty {
let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_FrequentContacts, AnyHashable(0)), theme: theme, strings: strings)
var index: Int = 0
for peer in topPeers.prefix(15) {
existingPeerIds.insert(.peer(peer.id))
let selection: ContactsPeerItemSelection
if let selectionState = selectionState {
selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil)
} else {
selection = .none
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil))
index += 1
}
}
if let storySubscriptions {
let _ = storySubscriptions
@ -528,7 +549,6 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
var index: Int = 0
var existingPeerIds = Set<ContactListPeerId>()
if let selectionState = selectionState {
for peer in selectionState.foundPeers {
if existingPeerIds.contains(peer.id) {
@ -657,7 +677,7 @@ private struct ContactsListNodeTransition {
public enum ContactListPresentation {
case orderedByPresence(options: [ContactListAdditionalOption])
case natural(options: [ContactListAdditionalOption], includeChatList: Bool)
case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool)
case search(signal: Signal<String, NoError>, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool)
public var sortOrder: ContactsSortOrder? {
@ -983,9 +1003,11 @@ public final class ContactListNode: ASDisplayNode {
|> mapToSignal { presentation in
var generateSections = false
var includeChatList = false
if case let .natural(_, includeChatListValue) = presentation {
var displayTopPeers = false
if case let .natural(_, includeChatListValue, displayTopPeersValue) = presentation {
generateSections = true
includeChatList = includeChatListValue
displayTopPeers = displayTopPeersValue
}
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {
@ -1214,7 +1236,7 @@ public final class ContactListNode: ASDisplayNode {
peers.append(.deviceContact(stableId, contact.0))
}
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil)
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [])
let previous = previousEntries.swap(entries)
return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch))
}
@ -1274,8 +1296,24 @@ public final class ContactListNode: ASDisplayNode {
chatListSignal = .single([])
}
return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get())
|> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal<ContactsListNodeTransition, NoError> in
let recentPeers: Signal<RecentPeers, NoError>
if displayTopPeers {
recentPeers = context.engine.peers.recentPeers()
} else {
recentPeers = .single(.disabled)
}
return (combineLatest(
self.contactPeersViewPromise.get(),
chatListSignal,
selectionStateSignal,
presentationDataPromise.get(),
contactsAuthorization.get(),
contactsWarningSuppressed.get(),
self.storySubscriptions.get(),
recentPeers
)
|> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions, recentPeers -> Signal<ContactsListNodeTransition, NoError> in
let signal = deferred { () -> Signal<ContactsListNodeTransition, NoError> in
var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) })
for (peer, memberCount) in chatListPeers {
@ -1312,11 +1350,16 @@ public final class ContactListNode: ASDisplayNode {
}
}
var topPeers: [EnginePeer] = []
if case let .peers(peers) = recentPeers {
topPeers = peers.map(EnginePeer.init)
}
var isEmpty = false
if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty {
isEmpty = true
}
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions)
let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers)
let previous = previousEntries.swap(entries)
let previousSelection = previousSelectionState.swap(selectionState)

View file

@ -107,7 +107,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
case .presence:
return .orderedByPresence(options: options)
case .natural:
return .natural(options: options, includeChatList: false)
return .natural(options: options, includeChatList: false, topPeers: false)
}
}

View file

@ -110,6 +110,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/InvisibleInkDustNode",
"//submodules/AlertUI",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
],
visibility = [
"//visibility:public",

View file

@ -10,24 +10,29 @@ import LegacyComponents
import AvatarNode
import AccountContext
import TelegramCore
import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
class GiftAvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer?
let theme: PresentationTheme
let peers: [EnginePeer]
let isVisible: Bool
let hasIdleAnimations: Bool
init(context: AccountContext, peer: EnginePeer?, isVisible: Bool, hasIdleAnimations: Bool) {
init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool) {
self.context = context
self.peer = peer
self.theme = theme
self.peers = peers
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
return lhs.peer == rhs.peer && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -52,6 +57,10 @@ class GiftAvatarComponent: Component {
private let sceneView: SCNView
private let avatarNode: ImageNode
private var mergedAvatarsNode: MergedAvatarsNode?
private let badgeBackground = ComponentView<Empty>()
private let badge = ComponentView<Empty>()
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
@ -242,11 +251,75 @@ class GiftAvatarComponent: Component {
}
self.hasIdleAnimations = component.hasIdleAnimations
let avatarSize = CGSize(width: 100.0, height: 100.0)
if let peer = component.peer {
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
if component.peers.count > 1 {
let avatarSize = CGSize(width: 60.0, height: 60.0)
let mergedAvatarsNode: MergedAvatarsNode
if let current = self.mergedAvatarsNode {
mergedAvatarsNode = current
} else {
mergedAvatarsNode = MergedAvatarsNode()
mergedAvatarsNode.isUserInteractionEnabled = false
self.addSubview(mergedAvatarsNode.view)
self.mergedAvatarsNode = mergedAvatarsNode
}
mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.map { $0._asPeer() }.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0)
let avatarsSize = CGSize(width: avatarSize.width + 60.0, height: avatarSize.height)
mergedAvatarsNode.updateLayout(size: avatarsSize)
mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarsSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarsSize)
self.avatarNode.isHidden = true
} else {
self.mergedAvatarsNode?.view.removeFromSuperview()
self.mergedAvatarsNode = nil
self.avatarNode.isHidden = false
let avatarSize = CGSize(width: 100.0, height: 100.0)
if let peer = component.peers.first {
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
}
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarSize)
}
if component.peers.count > 3 {
let badgeTextSize = self.badge.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "+\(component.peers.count - 3)", font: Font.with(size: 10.0, design: .round, weight: .semibold), textColor: component.theme.list.itemCheckColors.foregroundColor))
)
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let lineWidth = 1.0 + UIScreenPixel
let badgeSize = CGSize(width: max(17.0, badgeTextSize.width + 7.0) + lineWidth * 2.0, height: 17.0 + lineWidth * 2.0)
let _ = self.badgeBackground.update(
transition: .immediate,
component: AnyComponent(
RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: badgeSize.height / 2.0, stroke: lineWidth, strokeColor: component.theme.list.blocksBackgroundColor)
),
environment: {},
containerSize: badgeSize
)
if let badgeTextView = self.badge.view, let badgeBackgroundView = self.badgeBackground.view {
if badgeBackgroundView.superview == nil {
self.addSubview(badgeBackgroundView)
self.addSubview(badgeTextView)
}
let avatarsSize = CGSize(width: 120.0, height: 60.0)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width + avatarsSize.width) / 2.0) - 19.0 - lineWidth, y: 113.0 + avatarsSize.height / 2.0 - 15.0 - lineWidth), size: badgeSize)
badgeBackgroundView.frame = backgroundFrame
badgeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeTextSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - badgeTextSize.height / 2.0) - UIScreenPixel), size: badgeTextSize)
}
} else {
self.badge.view?.removeFromSuperview()
self.badgeBackground.view?.removeFromSuperview()
}
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize)
return availableSize
}

View file

@ -31,19 +31,66 @@ public func presentGiveawayInfoController(
guard let message else {
return
}
guard let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else {
return
let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway
let giveawayResults = message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults
var channelPeerId: EnginePeer.Id?
if let giveaway {
if let peerId = giveaway.channelPeerIds.first {
channelPeerId = peerId
}
} else if let _ = giveawayResults {
channelPeerId = message.author?.id
}
var quantity: Int32 = 0
if let giveaway {
quantity = giveaway.quantity
} else if let giveawayResults {
quantity = giveawayResults.winnersCount + giveawayResults.unclaimedCount
}
var months: Int32 = 0
if let giveaway {
months = giveaway.months
} else if let giveawayResults {
months = giveawayResults.months
}
var prizeDescription: String?
if let giveaway {
prizeDescription = giveaway.prizeDescription
} else if let giveawayResults {
prizeDescription = giveawayResults.prizeDescription
}
var untilDateValue: Int32 = 0
if let giveaway {
untilDateValue = giveaway.untilDate
} else if let _ = giveawayResults {
untilDateValue = message.timestamp
}
var onlyNewSubscribers = false
if let giveaway, giveaway.flags.contains(.onlyNewSubscribers) {
onlyNewSubscribers = true
}
var channelsCount = 1
if let giveaway {
channelsCount = giveaway.channelPeerIds.count
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var peerName = ""
if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] {
if let peerId = channelPeerId, let peer = message.peers[peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
let timeZone = TimeZone.current
let untilDate = stringForDate(timestamp: giveaway.untilDate, timeZone: timeZone, strings: presentationData.strings)
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)
let title: String
let text: String
@ -56,8 +103,8 @@ public func presentGiveawayInfoController(
})]
var additionalPrizes = ""
if let prizeDescription = giveaway.prizeDescription, !prizeDescription.isEmpty {
additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(giveaway.quantity) \(prizeDescription)").string
if let prizeDescription, !prizeDescription.isEmpty {
additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(quantity) \(prizeDescription)").string
}
switch giveawayInfo {
@ -71,23 +118,23 @@ public func presentGiveawayInfoController(
let intro: String
if case .almostOver = status {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
}
let ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), startDate).string
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string
}
@ -96,8 +143,8 @@ public func presentGiveawayInfoController(
var participation: String
switch status {
case .notQualified:
if giveaway.channelPeerIds.count > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), untilDate).string
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), untilDate).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string
}
@ -116,8 +163,8 @@ public func presentGiveawayInfoController(
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry
}
case .participating:
if giveaway.channelPeerIds.count > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string
if channelsCount > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string
}
@ -139,19 +186,19 @@ public func presentGiveawayInfoController(
let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings)
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string
var ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
if onlyNewSubscribers {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity)
if channelsCount > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string

View file

@ -23,6 +23,7 @@ import UniversalMediaPlayer
public enum PremiumGiftSource: Equatable {
case profile
case attachMenu
case settings
var identifier: String? {
switch self {
@ -30,6 +31,8 @@ public enum PremiumGiftSource: Equatable {
return "profile"
case .attachMenu:
return "attach"
case .settings:
return "settings"
}
}
}
@ -39,7 +42,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let context: AccountContext
let source: PremiumGiftSource
let peer: EnginePeer?
let peers: [EnginePeer]
let products: [PremiumGiftProduct]?
let selectedProductId: String?
@ -47,10 +50,10 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let selectProduct: (String) -> Void
let buy: () -> Void
init(context: AccountContext, source: PremiumGiftSource, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) {
init(context: AccountContext, source: PremiumGiftSource, peers: [EnginePeer], products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) {
self.context = context
self.source = source
self.peer = peer
self.peers = peers
self.products = products
self.selectedProductId = selectedProductId
self.present = present
@ -65,7 +68,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
if lhs.source != rhs.source {
return false
}
if lhs.peer != rhs.peer {
if lhs.peers != rhs.peers {
return false
}
if lhs.products != rhs.products {
@ -88,6 +91,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
private var stickersDisposable: Disposable?
private var preloadDisposableSet = DisposableSet()
var cachedBoostIcon: UIImage?
var price: String?
init(context: AccountContext, source: PremiumGiftSource) {
@ -186,7 +191,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
var size = CGSize(width: context.availableSize.width, height: 0.0)
let overscroll = overscroll.update(
component: Rectangle(color: theme.list.plainBackgroundColor),
component: Rectangle(color: theme.list.blocksBackgroundColor),
availableSize: CGSize(width: context.availableSize.width, height: 1000),
transition: context.transition
)
@ -205,15 +210,63 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
var descriptionString: String = ""
if context.component.peers.count > 1 {
if context.component.peers.count < 4 {
var names = ""
for i in 0 ..< context.component.peers.count {
if i == 0 {
} else if i < context.component.peers.count - 1 {
names.append(environment.strings.CreateGroup_PeersTitleDelimeter)
} else {
names.append(environment.strings.CreateGroup_PeersTitleLastDelimeter)
}
names.append(context.component.peers[i].compactDisplayTitle)
}
descriptionString = environment.strings.Premium_Gift_MultipleDescription(names, "").string
} else {
var names = ""
for i in 0 ..< min(3, context.component.peers.count) {
if i == 0 {
} else {
names.append(environment.strings.CreateGroup_PeersTitleDelimeter)
}
names.append(context.component.peers[i].compactDisplayTitle)
}
let more = environment.strings.Premium_Gift_NamesAndMore(Int32(context.component.peers.count - 3))
descriptionString = environment.strings.Premium_Gift_MultipleDescription(names, more).string
}
} else {
descriptionString = strings.Premium_Gift_Description(component.peers.first?.compactDisplayTitle ?? "").string
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with { $0 })
descriptionString += "\n\n"
descriptionString += environment.strings.Premium_Gift_YouWillReceiveBoosts(Int32(context.component.peers.count) * premiumConfiguration.boostsPerGiftCount).replacingOccurrences(of: "[]()", with: " [ ]() ")
let boostIcon: UIImage
if let current = context.state.cachedBoostIcon {
boostIcon = current
} else {
boostIcon = generateImage(CGSize(width: 14.0, height: 20.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = UIImage(bundleImageName: "Premium/BoostChannel")?.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
}
})!
context.state.cachedBoostIcon = boostIcon
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor, additionalAttributes: [NSAttributedString.Key.attachment.rawValue: boostIcon]), linkAttribute: { _ in
return nil
})
let descriptionText = parseMarkdownIntoAttributedString(descriptionString, attributes: markdownAttributes, textAlignment: .center)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_Gift_Description(component.peer?.compactDisplayTitle ?? "").string,
attributes: markdownAttributes
),
text: .plain(descriptionText),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
@ -489,7 +542,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: PeerId
let peerIds: [EnginePeer.Id]
let options: [CachedPremiumGiftOption]
let source: PremiumGiftSource
let buttonStatePromise: Promise<AttachmentMainButtonState?>
@ -502,7 +555,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
init(
context: AccountContext,
peerId: PeerId,
peerIds: [EnginePeer.Id],
options: [CachedPremiumGiftOption],
source: PremiumGiftSource,
buttonStatePromise: Promise<AttachmentMainButtonState?>,
@ -514,7 +567,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
completion: @escaping (Int32) -> Void)
{
self.context = context
self.peerId = peerId
self.peerIds = peerIds
self.options = options
self.source = source
self.buttonStatePromise = buttonStatePromise
@ -530,7 +583,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
if lhs.peerIds != rhs.peerIds {
return false
}
if lhs.options != rhs.options {
@ -544,7 +597,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
final class State: ComponentState {
private let context: AccountContext
private let peerId: PeerId
private let peerIds: [EnginePeer.Id]
private let options: [CachedPremiumGiftOption]
private let source: PremiumGiftSource
private let buttonStatePromise: Promise<AttachmentMainButtonState?>
@ -564,7 +617,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
}
}
var peer: EnginePeer?
var peers: [EnginePeer.Id: EnginePeer] = [:]
var products: [PremiumGiftProduct]?
var selectedProductId: String?
@ -574,7 +627,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
init(
context: AccountContext,
peerId: PeerId,
peerIds: [EnginePeer.Id],
options: [CachedPremiumGiftOption],
source: PremiumGiftSource,
buttonStatePromise: Promise<AttachmentMainButtonState?>,
@ -584,7 +637,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
completion: @escaping (Int32) -> Void)
{
self.context = context
self.peerId = peerId
self.peerIds = peerIds
self.options = options
self.source = source
self.buttonAction = buttonAction
@ -605,8 +658,10 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
self.disposable = combineLatest(
queue: Queue.mainQueue(),
availableProducts,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
).start(next: { [weak self] products, peer in
context.engine.data.get(
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
).start(next: { [weak self] products, peers in
if let strongSelf = self {
var gifts: [PremiumGiftProduct] = []
for option in strongSelf.options {
@ -619,7 +674,15 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
if strongSelf.selectedProductId == nil && strongSelf.source != .attachMenu {
strongSelf.selectedProductId = strongSelf.products?.first?.id
}
strongSelf.peer = peer
var unwrappedPeers: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
unwrappedPeers[peerId] = peer
}
}
strongSelf.peers = unwrappedPeers
strongSelf.updated(transition: .immediate)
}
})
@ -675,7 +738,9 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
self.updateInProgress(true)
self.updated(transition: .immediate)
let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerId, currency: currency, amount: amount)
//TODO:update buy when api arrives
let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerIds.first!, currency: currency, amount: amount)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
@ -741,7 +806,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
func makeState() -> State {
return State(
context: self.context,
peerId: self.peerId,
peerIds: self.peerIds,
options: self.options,
source: self.source,
buttonStatePromise: self.buttonStatePromise,
@ -823,12 +888,19 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
var peers: [EnginePeer] = []
for peerId in context.component.peerIds {
if let peer = state.peers[peerId] {
peers.append(peer)
}
}
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumGiftScreenContentComponent(
context: context.component.context,
source: context.component.source,
peer: state.peer,
peers: peers,
products: state.products,
selectedProductId: state.selectedProductId,
present: context.component.present,
@ -887,7 +959,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
let star = star.update(
component: GiftAvatarComponent(
context: context.component.context,
peer: context.state.peer,
theme: environment.theme,
peers: peers,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations
),
@ -939,9 +1012,18 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
context.component.updateTabBarAlpha(bottomPanelAlpha, .immediate)
} else {
let sideInset: CGFloat = 16.0
let buttonText: String
if context.component.peerIds.count > 1 {
let subscriptions = environment.strings.Premium_Gift_GiftMultipleSubscriptions(Int32(context.component.peerIds.count))
buttonText = environment.strings.Premium_Gift_GiftMultipleSubscriptionsFormat(subscriptions, price ?? "").string
} else {
buttonText = environment.strings.Premium_Gift_GiftSubscription(price ?? "").string
}
let button = button.update(
component: SolidRoundedButtonComponent(
title: environment.strings.Premium_Gift_GiftSubscription(price ?? "").string,
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [
@ -1015,7 +1097,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer {
public let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonActionSlot = ActionSlot<Void>()
public init(context: AccountContext, peerId: PeerId, options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
public init(context: AccountContext, peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
var updateInProgressImpl: ((Bool) -> Void)?
@ -1025,7 +1107,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer {
var updateTabBarAlphaImpl: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
super.init(context: context, component: PremiumGiftScreenComponent(
context: context,
peerId: peerId,
peerIds: peerIds,
options: options,
source: source,
buttonStatePromise: self.mainButtonStatePromise,

View file

@ -1195,6 +1195,9 @@ private final class LimitSheetContent: CombinedComponent {
case let .channelReactions(reactionCount):
titleText = strings.ChannelBoost_CustomReactions
string = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string
case .wallpaper:
titleText = strings.ChannelBoost_Wallpaper
string = strings.ChannelBoost_WallpaperText("\(premiumConfiguration.minChannelWallpaperLevel)").string
}
} else {
let storiesString = strings.ChannelBoost_StoriesPerDay(level)
@ -1780,6 +1783,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case stories
case nameColors
case channelReactions(reactionCount: Int)
case wallpaper
}
case storiesChannelBoost(peer: EnginePeer, boostSubject: BoostSubject, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool)

View file

@ -229,7 +229,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
selectionPosition: .right,
isEnabled: isEnabled,
hasNext: i != occupiedBoosts.count - 1,
action: { [weak state] _ in
action: { [weak state] _, _ in
guard let state, hasSelection else {
return
}

View file

@ -308,7 +308,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
if case .edit(_, _, _, _, _, true, _) = self.mode {
doneButtonType = .proceed
} else if case let .peer(peer) = resultMode {
doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium)
if peer.id.namespace == Namespaces.Peer.CloudUser {
doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium)
} else {
doneButtonType = .setChannel
}
} else {
doneButtonType = .set
}
@ -775,7 +779,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
if case .edit(_, _, _, _, _, true, _) = self.mode {
doneButtonType = .proceed
} else if case let .peer(peer) = self.resultMode {
doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium)
if peer.id.namespace == Namespaces.Peer.CloudUser {
doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium)
} else {
doneButtonType = .setChannel
}
} else {
doneButtonType = .set
}
@ -1182,7 +1190,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
if case .background = self.mode, toolbarBottomInset.isZero {
toolbarBottomInset = 16.0
}
if case .peer = self.resultMode, !self.state.displayPatternPanel {
if case let .peer(peer) = self.resultMode, case .user = peer, !self.state.displayPatternPanel {
toolbarBottomInset += 58.0
}
let toolbarHeight = 49.0 + toolbarBottomInset

View file

@ -498,7 +498,11 @@ public class WallpaperGalleryController: ViewController {
break
}
if case let .peer(peer, _) = self.mode {
doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium)
if case .user = peer {
doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium)
} else {
doneButtonType = .setChannel
}
}
let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType)
@ -969,7 +973,7 @@ public class WallpaperGalleryController: ViewController {
self.overlayNode?.frame = self.galleryNode.bounds
var toolbarHeight: CGFloat = 66.0
if case .peer = self.mode {
if case let .peer(peer, _) = self.mode, case .user = peer {
toolbarHeight += 58.0
}
transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom)))

View file

@ -1330,7 +1330,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
let buttonSpacing: CGFloat = 18.0
var toolbarHeight: CGFloat = 66.0
if let mode = self.mode, case .peer = mode {
if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer {
toolbarHeight += 58.0
}
@ -1482,7 +1482,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
self.nativeNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
var bottomInset: CGFloat = 132.0
if let mode = self.mode, case .peer = mode {
if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer {
bottomInset += 58.0
}

View file

@ -13,6 +13,7 @@ enum WallpaperGalleryToolbarCancelButtonType {
enum WallpaperGalleryToolbarDoneButtonType {
case set
case setPeer(String, Bool)
case setChannel
case proceed
case apply
case none
@ -284,6 +285,8 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar
applyTitle = strings.Wallpaper_ApplyForMe
applyForBothTitle = strings.Wallpaper_ApplyForBoth(name).string
applyForBothLocked = !isPremium
case .setChannel:
applyTitle = strings.Wallpaper_ApplyForChannel
case .proceed:
applyTitle = strings.Theme_Colors_Proceed
case .apply:
@ -423,7 +426,7 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb
}
let doneTitle: String
switch self.doneButtonType {
case .set, .setPeer:
case .set, .setPeer, .setChannel:
doneTitle = strings.Wallpaper_Set
case .proceed:
doneTitle = strings.Theme_Colors_Proceed

View file

@ -2612,3 +2612,148 @@ private func restrictedSendingContentsText(peer: EnginePeer, presentationData: P
return presentationData.strings.Chat_SendAllowedContentPeerText(peer.compactDisplayTitle, itemListString).string
}
public final class MessageStoryRenderer {
private let context: AccountContext
private let presentationData: PresentationData
private let messages: [Message]
let containerNode: ASDisplayNode
private let wallpaperBackgroundNode: WallpaperBackgroundNode
private let messagesContainerNode: ASDisplayNode
private var dateHeaderNode: ListViewItemHeaderNode?
private var messageNodes: [ListViewItemNode]?
private let addressNode: ImmediateTextNode
private let _ready = Promise<Bool>()
var isReady: Signal<Bool, NoError> {
return self._ready.get()
}
public init(context: AccountContext, messages: [Message]) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.messages = messages
self.containerNode = ASDisplayNode()
self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false)
self.wallpaperBackgroundNode.displaysAsynchronously = false
self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
let message = messages.first!
let addressName = message.peers[message.id.peerId]?.addressName ?? ""
self.addressNode = ImmediateTextNode()
self.addressNode.displaysAsynchronously = false
self.addressNode.attributedText = NSAttributedString(string: "t.me/\(addressName)/\(message.id.id)", font: Font.medium(14.0), textColor: UIColor(rgb: 0xffffff))
self.addressNode.textShadowColor = UIColor(rgb: 0x929292, alpha: 0.8)
self.containerNode.addSubnode(self.wallpaperBackgroundNode)
self.containerNode.addSubnode(self.messagesContainerNode)
self.containerNode.addSubnode(self.addressNode)
let wallpaper = context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper
self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false)
self._ready.set(self.wallpaperBackgroundNode.isReady)
}
public func update(layout: ContainerViewLayout, completion: @escaping (UIImage?) -> Void) {
self.updateMessagesLayout(layout: layout)
Queue.mainQueue().after(0.55) {
UIGraphicsBeginImageContextWithOptions(layout.size, false, 3.0)
self.containerNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: layout.size), afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let finalImage = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
if let cgImage = img?.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false)
}
}, opaque: true, scale: 1.0)
if let finalImage {
completion(finalImage)
}
}
}
private func updateMessagesLayout(layout: ContainerViewLayout) {
let size = layout.size
self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.wallpaperBackgroundNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate)
self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
let addressLayout = self.addressNode.updateLayout(size)
let theme = self.presentationData.theme.withUpdated(preview: true)
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)]
let inset: CGFloat = 16.0
let width = layout.size.width - inset * 2.0
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - layout.size.height) / 2.0)), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainerNode.addSubnode(itemNode!)
}
self.messageNodes = messageNodes
}
var bottomOffset: CGFloat = 0.0
if let messageNodes = self.messageNodes {
for itemNode in messageNodes {
itemNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((size.height - itemNode.frame.height) / 2.0)), size: itemNode.frame.size)
bottomOffset += itemNode.frame.maxY
itemNode.updateFrame(itemNode.frame, within: layout.size)
}
}
self.addressNode.frame = CGRect(origin: CGPoint(x: inset + 16.0, y: bottomOffset + 3.0), size: CGSize(width: addressLayout.width, height: addressLayout.height + 3.0))
let dateHeaderNode: ListViewItemHeaderNode
if let currentDateHeaderNode = self.dateHeaderNode {
dateHeaderNode = currentDateHeaderNode
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
} else {
dateHeaderNode = headerItem.node(synchronousLoad: true)
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.addSubnode(dateHeaderNode)
self.dateHeaderNode = dateHeaderNode
}
dateHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))
dateHeaderNode.updateLayout(size: self.containerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right)
}
}

View file

@ -1461,11 +1461,33 @@ public final class SolidRoundedButtonView: UIView {
let spacingOffset: CGFloat = 9.0
let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset
let iconSpacing: CGFloat = self.iconSpacing
let badgeSpacing: CGFloat = 6.0
var contentWidth: CGFloat = titleSize.width
if !iconSize.width.isZero {
contentWidth += iconSize.width + iconSpacing
}
var badgeSize: CGSize = .zero
if let badge = self.badge {
let badgeNode: BadgeNode
if let current = self.badgeNode {
badgeNode = current
} else {
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0
self.badgeNode = badgeNode
self.addSubnode(badgeNode)
}
badgeNode.text = badge
badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
contentWidth += badgeSize.width + badgeSpacing
} else if let badgeNode = self.badgeNode {
self.badgeNode = nil
badgeNode.removeFromSupernode()
}
var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0)
let iconFrame: CGRect
@ -1484,6 +1506,7 @@ public final class SolidRoundedButtonView: UIView {
}
iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)
}
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
transition.updateFrame(view: self.iconNode, frame: iconFrame)
if let animationNode = self.animationNode {
@ -1491,22 +1514,8 @@ public final class SolidRoundedButtonView: UIView {
}
transition.updateFrame(view: self.titleNode, frame: titleFrame)
if let badge = self.badge {
let badgeNode: BadgeNode
if let current = self.badgeNode {
badgeNode = current
} else {
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0
self.badgeNode = badgeNode
self.addSubnode(badgeNode)
}
badgeNode.text = badge
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
} else if let badgeNode = self.badgeNode {
self.badgeNode = nil
badgeNode.removeFromSupernode()
if let badgeNode = self.badgeNode {
transition.updateFrame(node: badgeNode, frame: badgeFrame)
}
if self.subtitle != self.subtitleNode.attributedText?.string {

View file

@ -69,7 +69,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
flags |= (1 << 0)
}
if showWinners {
flags |= (1 << 0)
flags |= (1 << 3)
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {

View file

@ -64,6 +64,33 @@ public struct PresentationResourcesSettings {
drawBorder(context: context, rect: bounds)
})
public static let premiumGift = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0)
context.addPath(path.cgPath)
context.clip()
let colorsArray: [CGColor] = [
UIColor(rgb: 0x3da3f4).cgColor,
UIColor(rgb: 0x3da3f4).cgColor,
UIColor(rgb: 0x39b3b9).cgColor,
UIColor(rgb: 0x35c37c).cgColor,
UIColor(rgb: 0x35c37c).cgColor
]
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size))
}
drawBorder(context: context, rect: bounds)
})
public static let passport = renderIcon(name: "Settings/Menu/Passport")
public static let watch = renderIcon(name: "Settings/Menu/Watch")

View file

@ -894,8 +894,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = NSAttributedString(string: strings.Notification_YouChangedWallpaper, font: titleFont, textColor: primaryTextColor)
}
} else {
let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
if message.id.peerId.isGroupOrChannel {
attributedString = NSAttributedString(string: strings.Notification_ChannelChangedWallpaper, font: titleFont, textColor: primaryTextColor)
} else {
let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
}
case .setSameChatWallpaper:
if message.author?.id == accountPeerId {

View file

@ -150,7 +150,11 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
if case .user = peer {
item.controllerInteraction.openPeer(peer, .info(nil), nil, .default)
} else {
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
}
}
@ -252,9 +256,28 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
}
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 2)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor)
let badgeText: String
if let giveaway {
badgeText = "X\(giveaway.quantity)"
} else if let giveawayResults {
badgeText = "X\(giveawayResults.winnersCount)"
} else {
badgeText = ""
}
let badgeString = NSAttributedString(string: badgeText, font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor)
let prizeTitleString = NSAttributedString(string: giveawayResults != nil ? "Winners Selected!" : item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor)
let prizeTitleText: String
if let giveawayResults {
if giveawayResults.winnersCount > 1 {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_Many
} else {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_One
}
} else {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle
}
let prizeTitleString = NSAttributedString(string: prizeTitleText, font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString?
var additionalPrizeSeparatorString: NSAttributedString?
var additionalPrizeTextString: NSAttributedString?
@ -294,7 +317,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
}
), textAlignment: .center)
} else if let giveawayResults {
prizeTextString = parseMarkdownIntoAttributedString("**\(giveawayResults.winnersCount)** winners of the [Giveaway]() were randomly selected by Telegram.", attributes: MarkdownAttributes(
prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedText(giveawayResults.winnersCount), attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
@ -304,7 +327,18 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
), textAlignment: .center)
}
let participantsTitleString = NSAttributedString(string: giveawayResults != nil ? "Winners" : item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle, font: titleFont, textColor: textColor)
let participantsTitleText: String
if let giveawayResults {
if giveawayResults.winnersCount > 1 {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_Many
} else {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_One
}
} else {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle
}
let participantsTitleString = NSAttributedString(string: participantsTitleText, font: titleFont, textColor: textColor)
let participantsText: String
let countriesText: String
@ -360,7 +394,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
if let giveawayResults {
if giveawayResults.winnersCount > giveawayResults.winnersPeerIds.count {
let moreCount = giveawayResults.winnersCount - Int32(giveawayResults.winnersPeerIds.count)
dateTitleText = "and \(moreCount) more!"
dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersMore(moreCount)
} else {
dateTitleText = ""
}
@ -370,8 +404,8 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
var dateTextString: NSAttributedString?
if let giveaway {
dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor)
} else if let _ = giveawayResults {
dateTextString = NSAttributedString(string: "All winners received gift links in private messages.", font: textFont, textColor: textColor)
} else if let giveawayResults {
dateTextString = NSAttributedString(string: giveawayResults.winnersCount > 1 ? item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_Many : item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_One, font: textFont, textColor: textColor)
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, hidesHeaders: true)

View file

@ -36,6 +36,7 @@ swift_library(
"//submodules/ChatMessageBackground",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
],
visibility = [
"//visibility:public",

View file

@ -27,6 +27,7 @@ import BundleIconComponent
import ChatMessageBackground
import ContextUI
import UndoUI
import MergedAvatarsNode
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
@ -776,7 +777,7 @@ private final class ChannelItemComponent: Component {
self.mergedAvatarsNode = mergedAvatarsNode
}
mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0)
mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0)
let avatarsSize = CGSize(width: avatarSize.width + 20.0, height: avatarSize.height)
mergedAvatarsNode.updateLayout(size: avatarsSize)
mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: avatarFrame.midX - avatarsSize.width / 2.0, y: avatarFrame.minY), size: avatarsSize)

View file

@ -25,6 +25,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
],
visibility = [
"//visibility:public",

View file

@ -15,6 +15,7 @@ import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import PollBubbleTimerNode
import MergedAvatarsNode
private final class ChatMessagePollOptionRadioNodeParameters: NSObject {
let timestamp: Double
@ -1440,10 +1441,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0)
}
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - defaultMergedImageSize) / 2.0)), size: CGSize(width: defaultMergedImageSize + defaultMergedImageSpacing * 2.0, height: defaultMergedImageSize))
let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize))
strongSelf.avatarsNode.frame = avatarsFrame
strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: defaultMergedImageSize, imageSpacing: defaultMergedImageSpacing, borderWidth: defaultBorderWidth)
strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth)
strongSelf.avatarsNode.isHidden = isBotChat
let alphaTransition: ContainedViewLayoutTransition
if animation.isAnimated {
@ -1706,202 +1707,3 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
return nil
}
}
private enum PeerAvatarReference: Equatable {
case letters(PeerId, PeerNameColor?, [String])
case image(PeerReference, TelegramMediaImageRepresentation)
var peerId: PeerId {
switch self {
case let .letters(value, _, _):
return value
case let .image(value, _):
return value.id
}
}
}
private extension PeerAvatarReference {
init(peer: Peer) {
if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) {
self = .image(peerReference, photo)
} else {
self = .letters(peer.id, peer.nameColor, peer.displayLetters)
}
}
}
private final class MergedAvatarsNodeArguments: NSObject {
let peers: [PeerAvatarReference]
let images: [PeerId: UIImage]
let imageSize: CGFloat
let imageSpacing: CGFloat
let borderWidth: CGFloat
init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) {
self.peers = peers
self.images = images
self.imageSize = imageSize
self.imageSpacing = imageSpacing
self.borderWidth = borderWidth
}
}
private let defaultMergedImageSize: CGFloat = 16.0
private let defaultMergedImageSpacing: CGFloat = 15.0
private let defaultBorderWidth: CGFloat = 1.0
private let avatarFont = avatarPlaceholderFont(size: 8.0)
public final class MergedAvatarsNode: ASDisplayNode {
private var peers: [PeerAvatarReference] = []
private var images: [PeerId: UIImage] = [:]
private var disposables: [PeerId: Disposable] = [:]
private let buttonNode: HighlightTrackingButtonNode
private var imageSize: CGFloat = defaultMergedImageSize
private var imageSpacing: CGFloat = defaultMergedImageSpacing
private var borderWidthValue: CGFloat = defaultBorderWidth
public var pressed: (() -> Void)?
override public init() {
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.buttonNode)
}
deinit {
for (_, disposable) in self.disposables {
disposable.dispose()
}
}
@objc private func buttonPressed() {
self.pressed?()
}
public func updateLayout(size: CGSize) {
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
}
public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) {
self.imageSize = imageSize
self.imageSpacing = imageSpacing
self.borderWidthValue = borderWidth
var filteredPeers = peers.map(PeerAvatarReference.init)
if filteredPeers.count > 3 {
filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3)
}
if filteredPeers != self.peers {
self.peers = filteredPeers
var validImageIds: [PeerId] = []
for peer in filteredPeers {
if case .image = peer {
validImageIds.append(peer.peerId)
}
}
var removedImageIds: [PeerId] = []
for (id, _) in self.images {
if !validImageIds.contains(id) {
removedImageIds.append(id)
}
}
var removedDisposableIds: [PeerId] = []
for (id, disposable) in self.disposables {
if !validImageIds.contains(id) {
disposable.dispose()
removedDisposableIds.append(id)
}
}
for id in removedImageIds {
self.images.removeValue(forKey: id)
}
for id in removedDisposableIds {
self.disposables.removeValue(forKey: id)
}
for peer in filteredPeers {
switch peer {
case let .image(peerReference, representation):
if self.disposables[peer.peerId] == nil {
if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) {
let disposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.images[peer.peerId] = image
strongSelf.setNeedsDisplay()
}
})
self.disposables[peer.peerId] = disposable
}
}
case .letters:
break
}
}
self.setNeedsDisplay()
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? MergedAvatarsNodeArguments else {
return
}
let mergedImageSize = parameters.imageSize
let mergedImageSpacing = parameters.imageSpacing
var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize
for i in (0 ..< parameters.peers.count).reversed() {
let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth))
context.setBlendMode(.normal)
context.saveGState()
switch parameters.peers[i] {
case let .letters(peerId, nameColor, letters):
context.translateBy(x: currentX, y: 0.0)
drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId, nameColor: nameColor)
context.translateBy(x: -currentX, y: 0.0)
case .image:
if let image = parameters.images[parameters.peers[i].peerId] {
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(image.cgImage!, in: imageRect)
} else {
context.setFillColor(UIColor.gray.cgColor)
context.fillEllipse(in: imageRect)
}
}
context.restoreGState()
currentX -= mergedImageSpacing
}
}
}

View file

@ -588,6 +588,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.cachedChatMessageText = updatedCachedChatMessageText
}
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
let cachedLayout = strongSelf.textNode.textNode.cachedLayout

View file

@ -265,6 +265,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
}
let fromYou = item.message.author?.id == item.context.account.peerId
let isChannel = item.message.id.peerId.isGroupOrChannel
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? ""
let text: String
@ -281,7 +282,11 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
}
}
} else {
text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string
if item.message.id.peerId.isGroupOrChannel {
text = item.presentationData.strings.Notification_ChannelChangedWallpaper
} else {
text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string
}
}
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor)
@ -311,15 +316,15 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
if displayTrailingAnimatedDots {
textHeight += subtitleLayout.size.height
}
let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou ? 0.0 : 42.0))
let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou || isChannel ? 0.0 : 42.0))
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.buttonNode.isHidden = fromYou
strongSelf.buttonTitleNode.isHidden = fromYou
strongSelf.buttonNode.isHidden = fromYou || isChannel
strongSelf.buttonTitleNode.isHidden = fromYou || isChannel
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize)
if let media, mediaUpdated {

View file

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

View file

@ -0,0 +1,217 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import Postbox
import TelegramCore
import AvatarNode
import AccountContext
private enum PeerAvatarReference: Equatable {
case letters(PeerId, PeerNameColor?, [String])
case image(PeerReference, TelegramMediaImageRepresentation)
var peerId: PeerId {
switch self {
case let .letters(value, _, _):
return value
case let .image(value, _):
return value.id
}
}
}
private extension PeerAvatarReference {
init(peer: Peer) {
if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) {
self = .image(peerReference, photo)
} else {
self = .letters(peer.id, peer.nameColor, peer.displayLetters)
}
}
}
private final class MergedAvatarsNodeArguments: NSObject {
let peers: [PeerAvatarReference]
let images: [PeerId: UIImage]
let imageSize: CGFloat
let imageSpacing: CGFloat
let borderWidth: CGFloat
let avatarFontSize: CGFloat
init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat) {
self.peers = peers
self.images = images
self.imageSize = imageSize
self.imageSpacing = imageSpacing
self.borderWidth = borderWidth
self.avatarFontSize = avatarFontSize
}
}
private let defaultMergedImageSize: CGFloat = 16.0
private let defaultMergedImageSpacing: CGFloat = 15.0
private let defaultBorderWidth: CGFloat = 1.0
public final class MergedAvatarsNode: ASDisplayNode {
public static let defaultMergedImageSize: CGFloat = 16.0
public static let defaultMergedImageSpacing: CGFloat = 15.0
public static let defaultBorderWidth: CGFloat = 1.0
public static let defaultAvatarFontSize: CGFloat = 8.0
private var peers: [PeerAvatarReference] = []
private var images: [PeerId: UIImage] = [:]
private var disposables: [PeerId: Disposable] = [:]
private let buttonNode: HighlightTrackingButtonNode
private var imageSize: CGFloat = defaultMergedImageSize
private var imageSpacing: CGFloat = defaultMergedImageSpacing
private var borderWidthValue: CGFloat = defaultBorderWidth
private var avatarFontSize: CGFloat = defaultAvatarFontSize
public var pressed: (() -> Void)?
override public init() {
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.buttonNode)
}
deinit {
for (_, disposable) in self.disposables {
disposable.dispose()
}
}
@objc private func buttonPressed() {
self.pressed?()
}
public func updateLayout(size: CGSize) {
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
}
public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) {
self.imageSize = imageSize
self.imageSpacing = imageSpacing
self.borderWidthValue = borderWidth
self.avatarFontSize = avatarFontSize
var filteredPeers = peers.map(PeerAvatarReference.init)
if filteredPeers.count > 3 {
filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3)
}
if filteredPeers != self.peers {
self.peers = filteredPeers
var validImageIds: [PeerId] = []
for peer in filteredPeers {
if case .image = peer {
validImageIds.append(peer.peerId)
}
}
var removedImageIds: [PeerId] = []
for (id, _) in self.images {
if !validImageIds.contains(id) {
removedImageIds.append(id)
}
}
var removedDisposableIds: [PeerId] = []
for (id, disposable) in self.disposables {
if !validImageIds.contains(id) {
disposable.dispose()
removedDisposableIds.append(id)
}
}
for id in removedImageIds {
self.images.removeValue(forKey: id)
}
for id in removedDisposableIds {
self.disposables.removeValue(forKey: id)
}
for peer in filteredPeers {
switch peer {
case let .image(peerReference, representation):
if self.disposables[peer.peerId] == nil {
if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) {
let disposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.images[peer.peerId] = image
strongSelf.setNeedsDisplay()
}
})
self.disposables[peer.peerId] = disposable
}
}
case .letters:
break
}
}
self.setNeedsDisplay()
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue, avatarFontSize: self.avatarFontSize)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? MergedAvatarsNodeArguments else {
return
}
let mergedImageSize = parameters.imageSize
let mergedImageSpacing = parameters.imageSpacing
var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize
for i in (0 ..< parameters.peers.count).reversed() {
let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth))
context.setBlendMode(.normal)
context.saveGState()
switch parameters.peers[i] {
case let .letters(peerId, nameColor, letters):
context.translateBy(x: currentX, y: 0.0)
drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarPlaceholderFont(size: parameters.avatarFontSize), letters: letters, peerId: peerId, nameColor: nameColor)
context.translateBy(x: -currentX, y: 0.0)
case .image:
if let image = parameters.images[parameters.peers[i].peerId] {
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(image.cgImage!, in: imageRect)
} else {
context.setFillColor(UIColor.gray.cgColor)
context.fillEllipse(in: imageRect)
}
}
context.restoreGState()
currentX -= mergedImageSpacing
}
}
}

View file

@ -230,7 +230,7 @@ final class ContextResultPanelComponent: Component {
presence: nil,
selectionState: .none,
hasNext: index != peers.count - 1,
action: { [weak self] peer in
action: { [weak self] peer, _ in
guard let self, let component = self.component else {
return
}
@ -299,7 +299,7 @@ final class ContextResultPanelComponent: Component {
presence: nil,
selectionState: .none,
hasNext: true,
action: { _ in
action: { _, _ in
}
)),
environment: {},

View file

@ -131,6 +131,8 @@ swift_library(
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/SolidRoundedButtonNode",
"//submodules/MediaPickerUI",
"//submodules/AttachmentUI",
],
visibility = [
"//visibility:public",

View file

@ -98,6 +98,8 @@ import PeerNameColorScreen
import PeerAllowedReactionsScreen
import ChatMessageSelectionInputPanelNode
import ChatHistorySearchContainerNode
import MediaPickerUI
import AttachmentUI
public enum PeerInfoAvatarEditingMode {
case generic
@ -483,6 +485,7 @@ private enum PeerInfoSettingsSection {
case language
case stickers
case premium
case premiumGift
case passport
case watch
case support
@ -527,6 +530,7 @@ private final class PeerInfoInteraction {
let performBotCommand: (PeerInfoBotCommand) -> Void
let editingOpenPublicLinkSetup: () -> Void
let editingOpenNameColorSetup: () -> Void
let editingOpenPeerWallpaperSetup: () -> Void
let editingOpenInviteLinksSetup: () -> Void
let editingOpenDiscussionGroupSetup: () -> Void
let editingToggleMessageSignatures: (Bool) -> Void
@ -582,6 +586,7 @@ private final class PeerInfoInteraction {
performBotCommand: @escaping (PeerInfoBotCommand) -> Void,
editingOpenPublicLinkSetup: @escaping () -> Void,
editingOpenNameColorSetup: @escaping () -> Void,
editingOpenPeerWallpaperSetup: @escaping () -> Void,
editingOpenInviteLinksSetup: @escaping () -> Void,
editingOpenDiscussionGroupSetup: @escaping () -> Void,
editingToggleMessageSignatures: @escaping (Bool) -> Void,
@ -636,6 +641,7 @@ private final class PeerInfoInteraction {
self.performBotCommand = performBotCommand
self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup
self.editingOpenNameColorSetup = editingOpenNameColorSetup
self.editingOpenPeerWallpaperSetup = editingOpenPeerWallpaperSetup
self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup
self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup
self.editingToggleMessageSignatures = editingToggleMessageSignatures
@ -910,6 +916,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: {
interaction.openSettings(.premium)
}))
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {
interaction.openSettings(.premiumGift)
}))
}
/*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: "Payment Method", icon: PresentationResourcesSettings.language, action: {
@ -1570,19 +1579,20 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
switch channel.info {
case .broadcast:
let ItemUsername = 1
let ItemNameColor = 2
let ItemInviteLinks = 3
let ItemDiscussionGroup = 4
let ItemSignMessages = 5
let ItemSignMessagesHelp = 6
let ItemDeleteChannel = 7
let ItemReactions = 8
let ItemAdmins = 9
let ItemMembers = 10
let ItemMemberRequests = 11
let ItemStats = 12
let ItemBanned = 13
let ItemRecentActions = 14
let ItemPeerColor = 2
let ItemPeerWallpaper = 3
let ItemInviteLinks = 4
let ItemDiscussionGroup = 5
let ItemSignMessages = 6
let ItemSignMessagesHelp = 7
let ItemDeleteChannel = 8
let ItemReactions = 9
let ItemAdmins = 10
let ItemMembers = 11
let ItemMemberRequests = 12
let ItemStats = 13
let ItemBanned = 14
let ItemRecentActions = 15
let isCreator = channel.flags.contains(.isCreator)
@ -1655,10 +1665,22 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
}
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
let colors = context.peerNameColors.get(data.peer?.nameColor ?? .blue, dark: presentationData.theme.overallDarkAppearance)
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, colors.main), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: {
var colors: [PeerNameColors.Colors] = []
if let nameColor = channel.nameColor.flatMap({ context.peerNameColors.get($0, dark: presentationData.theme.overallDarkAppearance) }) {
colors.append(nameColor)
}
if let profileColor = channel.profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: presentationData.theme.overallDarkAppearance, subject: .palette) }) {
colors.append(profileColor)
}
let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors)
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: {
interaction.editingOpenNameColorSetup()
}))
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerWallpaper, label: .none, text: "Wallpaper", icon: UIImage(bundleImageName: "Settings/Menu/Appearance"), action: {
interaction.editingOpenPeerWallpaperSetup()
}))
}
if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendSomething)) {
@ -2336,6 +2358,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
editingOpenNameColorSetup: { [weak self] in
self?.editingOpenNameColorSetup()
},
editingOpenPeerWallpaperSetup: { [weak self] in
self?.editingOpenPeerWallpaperSetup()
},
editingOpenInviteLinksSetup: { [weak self] in
self?.editingOpenInviteLinksSetup()
},
@ -5412,7 +5437,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let strongSelf = self {
var pushControllerImpl: ((ViewController) -> Void)?
let controller = PremiumGiftScreen(context: strongSelf.context, peerId: strongSelf.peerId, options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in
let controller = PremiumGiftScreen(context: strongSelf.context, peerIds: [strongSelf.peerId], options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in
pushControllerImpl?(c)
}, completion: { [weak self] in
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
@ -7180,6 +7205,79 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
private func editingOpenPeerWallpaperSetup() {
// let link = status.url
// let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: {
// UIPasteboard.general.string = link
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }))
// return true
// }, openStats: nil, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? {
// let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
// pushImpl?(controller)
// } : nil)
// pushImpl?(controller)
let dismissControllers = { [weak self] in
if let self, let navigationController = self.controller?.navigationController as? NavigationController {
let controllers = navigationController.viewControllers.filter({ controller in
if controller is WallpaperGalleryController || controller is AttachmentController || controller is PeerInfoScreenImpl {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: true)
}
}
var openWallpaperPickerImpl: ((Bool) -> Void)?
let openWallpaperPicker: (Bool) -> Void = { [weak self] animateAppearance in
guard let self, let peer = self.data?.peer else {
return
}
let controller = wallpaperMediaPickerController(
context: self.context,
updatedPresentationData: self.controller?.updatedPresentationData,
peer: EnginePeer(peer),
animateAppearance: true,
completion: { [weak self] _, result in
guard let strongSelf = self, let asset = result as? PHAsset else {
return
}
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false))
controller.navigationPresentation = .modal
controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in
if let strongSelf = self {
uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peer.id, forBoth: forBoth, completion: {
Queue.mainQueue().after(0.3, {
dismissControllers()
})
})
}
}
strongSelf.controller?.push(controller)
},
openColors: { [weak self] in
guard let strongSelf = self else {
return
}
let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in
if let strongSelf = self {
strongSelf.controller?.push(controller)
}
}, openGallery: {
openWallpaperPickerImpl?(false)
})
controller.navigationPresentation = .flatModal
strongSelf.controller?.push(controller)
}
)
controller.navigationPresentation = .flatModal
self.controller?.push(controller)
}
openWallpaperPickerImpl = openWallpaperPicker
openWallpaperPicker(true)
}
private func editingOpenInviteLinksSetup() {
self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil))
}
@ -8626,6 +8724,63 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
push(LocalizationListController(context: self.context))
case .premium:
self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings))
case .premiumGift:
let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, mode: .premiumGifting, options: [], isPeerEnabled: { peer in
if case let .user(user) = peer, user.botInfo == nil {
return true
} else {
return false
}
}))
self.controller?.push(controller)
self.activeActionDisposable.set((controller.result
|> deliverOnMainQueue).startStrict(next: { [weak self, weak controller] result in
guard let self, let controller else {
return
}
var peerIds: [PeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue.compactMap({ peerId in
if case let .peer(peerId) = peerId {
return peerId
} else {
return nil
}
})
}
let maxCount = 10
if peerIds.count > maxCount {
self.hapticFeedback.error()
controller.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: self.presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(maxCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return
}
let giftController = PremiumGiftScreen(context: self.context, peerIds: peerIds, options: [], source: .settings, pushController: { [weak self] c in
self?.controller?.push(c)
}, completion: { [weak self] in
if let self, let navigationController = self.controller?.navigationController as? NavigationController, peerIds.count == 1, let peerId = peerIds.first {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is PeerInfoScreen) && !($0 is PremiumGiftScreen) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
})
controller.push(giftController)
}))
case .stickers:
if let settings = self.data?.globalSettings {
push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in

View file

@ -1337,7 +1337,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.controller?.containerLayoutUpdated(layout, transition: .immediate)
}
} else {
let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false)))
let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false, topPeers: false)))
self.contactListNode = contactListNode
contactListNode.enableUpdates = true
contactListNode.selectionStateUpdated = { [weak self] selectionState in

View file

@ -1066,7 +1066,7 @@ final class ShareWithPeersScreenComponent: Component {
rightAccessory: accessory,
selectionState: .none,
hasNext: i < peers.count - 1,
action: { [weak self] peer in
action: { [weak self] peer, _ in
guard let self, let component = self.component else {
return
}
@ -1414,7 +1414,7 @@ final class ShareWithPeersScreenComponent: Component {
presence: stateValue.presences[peer.id],
selectionState: .editing(isSelected: isSelected, isTinted: false),
hasNext: true,
action: { [weak self] peer in
action: { [weak self] peer, _ in
guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
return
}
@ -2143,7 +2143,7 @@ final class ShareWithPeersScreenComponent: Component {
presence: nil,
selectionState: .editing(isSelected: false, isTinted: false),
hasNext: true,
action: { _ in
action: { _, _ in
}
)),
environment: {},

View file

@ -23,7 +23,7 @@ import PhotoResources
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
private let repostIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderRepost"), color: .white)?.withRenderingMode(.alwaysTemplate)
private let forwardIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderRepost"), color: .white)?.withRenderingMode(.alwaysTemplate)
private let forwardIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderForward"), color: .white)?.withRenderingMode(.alwaysTemplate)
private let checkImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)?.withRenderingMode(.alwaysTemplate)
private let disclosureImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate)
@ -116,7 +116,7 @@ public final class PeerListItemComponent: Component {
let selectionPosition: SelectionPosition
let isEnabled: Bool
let hasNext: Bool
let action: (EnginePeer) -> Void
let action: (EnginePeer, EngineMessage.Id?) -> Void
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let openStories: ((EnginePeer, AvatarNode) -> Void)?
let openStory: ((EnginePeer, Int32, UIView) -> Void)?
@ -141,7 +141,7 @@ public final class PeerListItemComponent: Component {
selectionPosition: SelectionPosition = .left,
isEnabled: Bool = true,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void,
action: @escaping (EnginePeer, EngineMessage.Id?) -> Void,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil,
openStory: ((EnginePeer, Int32, UIView) -> Void)? = nil
@ -353,7 +353,7 @@ public final class PeerListItemComponent: Component {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
component.action(peer, component.message?.id)
}
@objc private func avatarButtonPressed() {

View file

@ -1707,6 +1707,7 @@ public final class StoryItemSetContainerComponent: Component {
context: component.context,
theme: component.theme,
strings: component.strings,
peer: component.slice.peer,
storyItem: item.storyItem,
myReaction: item.storyItem.myReaction.flatMap { value -> StoryFooterPanelComponent.MyReaction? in
var centerAnimation: TelegramMediaFile?
@ -3327,6 +3328,12 @@ public final class StoryItemSetContainerComponent: Component {
}
self.navigateToPeer(peer: peer, chat: false)
},
openMessage: { [weak self] peer, messageId in
guard let self else {
return
}
self.navigateToPeer(peer: peer, chat: true, subject: .message(id: .id(messageId), highlight: nil, timecode: nil))
},
peerContextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return

View file

@ -69,6 +69,7 @@ final class StoryItemSetViewListComponent: Component {
let deleteAction: () -> Void
let moreAction: (UIView, ContextGesture?) -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EnginePeer, EngineMessage.Id) -> Void
let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
let openPeerStories: (EnginePeer, AvatarNode) -> Void
let openStory: (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void
@ -94,6 +95,7 @@ final class StoryItemSetViewListComponent: Component {
deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EnginePeer, EngineMessage.Id) -> Void,
peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void,
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void,
openStory: @escaping (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void,
@ -118,6 +120,7 @@ final class StoryItemSetViewListComponent: Component {
self.deleteAction = deleteAction
self.moreAction = moreAction
self.openPeer = openPeer
self.openMessage = openMessage
self.peerContextAction = peerContextAction
self.openPeerStories = openPeerStories
self.openStory = openStory
@ -237,6 +240,17 @@ final class StoryItemSetViewListComponent: Component {
case repostsFirst = 0
case reactionsFirst = 1
case recentFirst = 2
var sortMode: EngineStoryViewListContext.SortMode {
switch self {
case .repostsFirst:
return .repostsFirst
case .reactionsFirst:
return .reactionsFirst
case .recentFirst:
return .recentFirst
}
}
}
private struct ContentConfigurationKey: Equatable {
@ -527,11 +541,15 @@ final class StoryItemSetViewListComponent: Component {
message: item.message,
selectionState: .none,
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
action: { [weak self] peer in
action: { [weak self] peer, messageId in
guard let self, let component = self.component else {
return
}
component.openPeer(peer)
if let messageId {
component.openMessage(peer, messageId)
} else {
component.openPeer(peer)
}
},
contextAction: { peer, view, gesture in
component.peerContextAction(peer, view, gesture)
@ -727,15 +745,6 @@ final class StoryItemSetViewListComponent: Component {
case .contacts:
mappedListMode = .contacts
}
let mappedSortMode: EngineStoryViewListContext.SortMode
switch self.configuration.sortMode {
case .repostsFirst:
mappedSortMode = .repostsFirst
case .reactionsFirst:
mappedSortMode = .reactionsFirst
case .recentFirst:
mappedSortMode = .recentFirst
}
var parentSource: EngineStoryViewListContext?
if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil {
@ -745,16 +754,22 @@ final class StoryItemSetViewListComponent: Component {
parentSource = nil
}
self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, searchQuery: query, parentSource: parentSource)
self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, searchQuery: query, parentSource: parentSource)
}
}
} else {
if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: .reactionsFirst) {
let defaultSortMode: SortMode
if component.peerId.isGroupOrChannel {
defaultSortMode = .recentFirst
} else {
defaultSortMode = .reactionsFirst
}
if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: defaultSortMode) {
let viewList: EngineStoryViewListContext
if let current = component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] {
viewList = current
} else {
viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: .reactionsFirst)
viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: defaultSortMode.sortMode)
component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] = viewList
}
self.viewList = viewList
@ -766,16 +781,7 @@ final class StoryItemSetViewListComponent: Component {
case .contacts:
mappedListMode = .contacts
}
let mappedSortMode: EngineStoryViewListContext.SortMode
switch self.configuration.sortMode {
case .repostsFirst:
mappedSortMode = .repostsFirst
case .reactionsFirst:
mappedSortMode = .reactionsFirst
case .recentFirst:
mappedSortMode = .recentFirst
}
self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)])
self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)])
}
}
}
@ -886,7 +892,7 @@ final class StoryItemSetViewListComponent: Component {
presence: nil,
selectionState: .none,
hasNext: true,
action: { _ in
action: { _, _ in
}
)),
environment: {},
@ -1351,25 +1357,26 @@ final class StoryItemSetViewListComponent: Component {
self.state?.updated(transition: .immediate)
}
})))
} else {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor)
}, additionalLeftIcon: { theme in
if sortMode != .reactionsFirst {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
if self.sortMode != .reactionsFirst {
self.sortMode = .reactionsFirst
self.state?.updated(transition: .immediate)
}
})))
}
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor)
}, additionalLeftIcon: { theme in
if sortMode != .reactionsFirst {
return nil
}
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
if self.sortMode != .reactionsFirst {
self.sortMode = .reactionsFirst
self.state?.updated(transition: .immediate)
}
})))
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortRecent, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Time"), color: theme.contextMenu.primaryColor)
}, additionalLeftIcon: { theme in
@ -1392,7 +1399,8 @@ final class StoryItemSetViewListComponent: Component {
items.append(.separator)
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
items.append(.action(ContextMenuActionItem(text: component.peerId.isGroupOrChannel ? component.strings.Story_ViewList_ContextSortChannelInfo : component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
let contextItems = ContextController.Items(content: .list(items))
@ -1432,6 +1440,12 @@ final class StoryItemSetViewListComponent: Component {
var updateSubState = false
if self.mainViewList == nil {
if component.peerId.isGroupOrChannel {
self.sortMode = .recentFirst
} else {
self.sortMode = .reactionsFirst
}
self.mainViewListDisposable?.dispose()
self.mainViewListDisposable = nil

View file

@ -38,6 +38,7 @@ public final class StoryFooterPanelComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let peer: EnginePeer
public let storyItem: EngineStoryItem
public let myReaction: MyReaction?
public let isChannel: Bool
@ -56,6 +57,7 @@ public final class StoryFooterPanelComponent: Component {
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer,
storyItem: EngineStoryItem,
myReaction: MyReaction?,
isChannel: Bool,
@ -73,6 +75,7 @@ public final class StoryFooterPanelComponent: Component {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.storyItem = storyItem
self.myReaction = myReaction
self.isChannel = isChannel
@ -98,6 +101,9 @@ public final class StoryFooterPanelComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.storyItem != rhs.storyItem {
return false
}
@ -186,6 +192,7 @@ public final class StoryFooterPanelComponent: Component {
self.avatarsView.alpha = 0.7
self.viewStatsCountText.alpha = 0.7
self.viewStatsLabelText.view?.alpha = 0.7
self.viewsIconView.alpha = 0.7
self.reactionStatsIcon?.alpha = 0.7
self.reactionStatsText?.alpha = 0.7
self.repostStatsIcon?.alpha = 0.7
@ -194,6 +201,7 @@ public final class StoryFooterPanelComponent: Component {
self.avatarsView.alpha = 1.0
self.viewStatsCountText.alpha = 1.0
self.viewStatsLabelText.view?.alpha = 1.0
self.viewsIconView.alpha = 1.0
self.reactionStatsIcon?.alpha = 1.0
self.reactionStatsText?.alpha = 1.0
self.repostStatsIcon?.alpha = 1.0
@ -202,6 +210,7 @@ public final class StoryFooterPanelComponent: Component {
self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.viewStatsCountText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.viewStatsLabelText.view?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.viewsIconView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
self.repostStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
@ -383,7 +392,14 @@ public final class StoryFooterPanelComponent: Component {
}
}
self.viewStatsButton.isEnabled = viewCount != 0 && !component.isChannel
var displayViewLists = false
if case let .channel(channel) = component.peer, channel.flags.contains(.isCreator) || channel.adminRights?.rights.contains(.canPostStories) == true {
displayViewLists = reactionCount != 0 || forwardCount != 0
} else {
displayViewLists = viewCount != 0 && !component.isChannel
}
self.viewStatsButton.isEnabled = displayViewLists
var rightContentOffset: CGFloat = availableSize.width - 12.0

View file

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

View file

@ -0,0 +1,351 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 13.000000 12.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
1.000000 0.000000 -0.000000 1.000000 1.330078 -0.721191 cm
5.315626 8.533691 m
5.315626 10.192076 l
5.315626 10.556320 5.315626 10.738442 5.388546 10.825513 c
5.451862 10.901115 5.547148 10.942396 5.645606 10.936879 c
5.759001 10.930527 5.891866 10.805965 6.157596 10.556844 c
9.926538 7.023460 l
10.061858 6.896598 10.129518 6.833168 10.154650 6.759085 c
10.176737 6.693978 10.176737 6.623405 10.154650 6.558298 c
10.129518 6.484215 10.061858 6.420784 9.926539 6.293923 c
6.157597 2.760540 l
5.891867 2.511417 5.759001 2.386856 5.645606 2.380504 c
5.547148 2.374987 5.451862 2.416267 5.388546 2.491870 c
5.315626 2.578940 5.315626 2.761063 5.315626 3.125307 c
5.315626 4.783691 l
2.581792 4.783691 1.121608 3.510042 0.458909 2.676931 c
0.280517 2.452666 0.191321 2.340534 0.141585 2.333771 c
0.096309 2.327615 0.057156 2.342980 0.028153 2.378287 c
-0.003707 2.417074 0.006321 2.548758 0.026378 2.812124 c
0.161226 4.582819 0.951126 8.533691 5.315626 8.533691 c
h
0.000000 0.000000 0.000000 scn
f*
n
endstream
endobj
2 0 obj
1028
endobj
3 0 obj
<< /Length 4 0 R
/FunctionType 4
/Domain [ 0.000000 1.000000 ]
/Range [ 0.000000 1.000000 ]
>>
stream
{ 0 gt { 0 } { 1 } ifelse }
endstream
endobj
4 0 obj
27
endobj
5 0 obj
<< /ExtGState << /E1 << /SMask << /Type /Mask
/G 1 0 R
/S /Alpha
/TR 3 0 R
>>
/Type /ExtGState
>> >> >>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
q
1.000000 0.000000 -0.000000 1.000000 1.330078 -0.721191 cm
0.000000 0.000000 0.000000 scn
5.315626 8.533691 m
5.315626 7.203691 l
6.050164 7.203691 6.645626 7.799152 6.645626 8.533691 c
5.315626 8.533691 l
h
5.315626 4.783691 m
6.645626 4.783691 l
6.645626 5.136429 6.505501 5.474720 6.256078 5.724144 c
6.006654 5.973567 5.668364 6.113691 5.315626 6.113691 c
5.315626 4.783691 l
h
0.458909 2.676931 m
-0.581952 3.504886 l
0.458909 2.676931 l
h
0.141585 2.333771 m
-0.037609 3.651644 l
0.141585 2.333771 l
h
0.028153 2.378287 m
1.055869 3.222504 l
0.028153 2.378287 l
h
5.645606 2.380504 m
5.571209 3.708421 l
5.645606 2.380504 l
h
5.388546 2.491870 m
4.368898 1.637928 l
5.388546 2.491870 l
h
9.926538 7.023460 m
9.016897 6.053176 l
9.926538 7.023460 l
h
10.154650 6.759085 m
8.895151 6.331811 l
10.154650 6.759085 l
h
9.926539 6.293923 m
10.836181 5.323638 l
9.926539 6.293923 l
h
10.154650 6.558298 m
11.414148 6.131024 l
10.154650 6.558298 l
h
5.388546 10.825513 m
6.408195 9.971571 l
5.388546 10.825513 l
h
5.645606 10.936879 m
5.720003 12.264797 l
5.645606 10.936879 l
h
6.645626 8.533691 m
6.645626 10.192076 l
3.985626 10.192076 l
3.985626 8.533691 l
6.645626 8.533691 l
h
5.247954 9.586559 m
9.016897 6.053176 l
10.836181 7.993745 l
7.067238 11.527128 l
5.247954 9.586559 l
h
9.016897 7.264207 m
5.247955 3.730824 l
7.067239 1.790256 l
10.836181 5.323638 l
9.016897 7.264207 l
h
6.645626 3.125307 m
6.645626 4.783691 l
3.985626 4.783691 l
3.985626 3.125307 l
6.645626 3.125307 l
h
5.315626 6.113691 m
2.097498 6.113691 0.279446 4.587790 -0.581952 3.504886 c
1.499771 1.848978 l
1.963770 2.432294 3.066086 3.453691 5.315626 3.453691 c
5.315626 6.113691 l
h
1.352538 2.711129 m
1.413560 3.512409 1.620141 4.689168 2.210945 5.629186 c
2.747699 6.483207 3.627555 7.203691 5.315626 7.203691 c
5.315626 9.863691 l
2.639197 9.863691 0.941854 8.608740 -0.041179 7.044650 c
-0.970163 5.566558 -1.225956 3.882534 -1.299782 2.913118 c
1.352538 2.711129 l
h
-0.581952 3.504886 m
-0.682445 3.378551 -0.720813 3.331185 -0.740086 3.309600 c
-0.750286 3.298176 -0.718989 3.335414 -0.659294 3.384659 c
-0.627593 3.410811 -0.566207 3.458532 -0.478809 3.506605 c
-0.394197 3.553145 -0.242563 3.623776 -0.037609 3.651644 c
0.320778 1.015898 l
0.717179 1.069798 0.973219 1.283091 1.033420 1.332752 c
1.129403 1.411933 1.203473 1.492488 1.244089 1.537976 c
1.326445 1.630214 1.421872 1.751047 1.499771 1.848978 c
-0.581952 3.504886 l
h
-1.299782 2.913118 m
-1.308334 2.800830 -1.320285 2.652165 -1.322062 2.530396 c
-1.322950 2.469575 -1.322850 2.358324 -1.304198 2.231652 c
-1.290535 2.138864 -1.241301 1.828356 -0.999563 1.534072 c
1.055869 3.222504 l
1.177860 3.073997 1.240899 2.929724 1.273622 2.836102 c
1.306749 2.741327 1.320904 2.663443 1.327426 2.619150 c
1.334278 2.572617 1.336326 2.537593 1.337061 2.520598 c
1.337841 2.502546 1.337658 2.491820 1.337655 2.491581 c
1.337645 2.490877 1.337704 2.495204 1.338235 2.506417 c
1.338756 2.517421 1.339593 2.532197 1.340889 2.552054 c
1.343585 2.593356 1.347357 2.643099 1.352538 2.711129 c
-1.299782 2.913118 l
h
-0.037609 3.651644 m
0.141479 3.675995 0.351744 3.662107 0.564653 3.578549 c
0.777561 3.494993 0.941147 3.362162 1.055869 3.222504 c
-0.999563 1.534072 l
-0.677662 1.142203 -0.181727 0.947571 0.320778 1.015898 c
-0.037609 3.651644 l
h
5.247955 3.730824 m
5.179151 3.666320 5.129662 3.619975 5.087789 3.581986 c
5.067758 3.563814 5.052710 3.550512 5.041348 3.540738 c
5.029866 3.530859 5.024831 3.526900 5.024859 3.526922 c
5.025069 3.527086 5.027243 3.528785 5.031198 3.531670 c
5.035140 3.534546 5.041450 3.539039 5.049938 3.544694 c
5.066150 3.555496 5.095299 3.573881 5.135852 3.594389 c
5.216256 3.635052 5.367123 3.696987 5.571209 3.708421 c
5.720003 1.052586 l
6.213777 1.080250 6.554987 1.346669 6.664093 1.432044 c
6.802816 1.540595 6.953648 1.683764 7.067239 1.790256 c
5.247955 3.730824 l
h
3.985626 3.125307 m
3.985626 2.969605 3.984338 2.761649 4.000025 2.586203 c
4.012363 2.448214 4.051368 2.017074 4.368898 1.637928 c
6.408195 3.345812 l
6.539436 3.189104 6.597436 3.036680 6.622764 2.950212 c
6.635538 2.906600 6.642061 2.872761 6.645269 2.853546 c
6.646949 2.843485 6.647987 2.835810 6.648585 2.830967 c
6.649184 2.826108 6.649432 2.823360 6.649456 2.823094 c
6.649459 2.823059 6.648904 2.829440 6.648258 2.844573 c
6.647618 2.859546 6.647029 2.879622 6.646587 2.906664 c
6.645662 2.963193 6.645626 3.030995 6.645626 3.125307 c
3.985626 3.125307 l
h
5.571209 3.708421 m
5.891789 3.726381 6.202040 3.591971 6.408195 3.345812 c
4.368898 1.637928 l
4.701684 1.240564 5.202506 1.023593 5.720003 1.052586 c
5.571209 3.708421 l
h
9.016897 6.053176 m
9.052996 6.019333 9.074108 5.999492 9.090629 5.983498 c
9.107223 5.967433 9.104925 5.968908 9.094450 5.980518 c
9.075613 6.001396 8.965572 6.124225 8.895151 6.331811 c
11.414148 7.186358 l
11.318595 7.468027 11.162159 7.659614 11.069380 7.762442 c
10.986407 7.854402 10.887315 7.945806 10.836181 7.993745 c
9.016897 6.053176 l
h
10.836181 5.323638 m
10.887316 5.371577 10.986407 5.462982 11.069380 5.554941 c
11.162159 5.657770 11.318595 5.849356 11.414148 6.131024 c
8.895151 6.985572 l
8.965572 7.193157 9.075612 7.315987 9.094449 7.336864 c
9.104925 7.348475 9.107223 7.349950 9.090629 7.333885 c
9.074109 7.317891 9.052996 7.298050 9.016897 7.264207 c
10.836181 5.323638 l
h
8.895151 6.331811 m
8.823236 6.543798 8.823236 6.773584 8.895151 6.985572 c
11.414148 6.131024 l
11.530237 6.473225 11.530237 6.844158 11.414148 7.186358 c
8.895151 6.331811 l
h
6.645626 10.192076 m
6.645626 10.286387 6.645662 10.354190 6.646587 10.410719 c
6.647029 10.437760 6.647618 10.457837 6.648258 10.472810 c
6.648904 10.487943 6.649459 10.494324 6.649456 10.494288 c
6.649432 10.494022 6.649184 10.491275 6.648585 10.486416 c
6.647987 10.481573 6.646949 10.473898 6.645269 10.463837 c
6.642061 10.444622 6.635538 10.410783 6.622764 10.367170 c
6.597436 10.280703 6.539436 10.128279 6.408195 9.971571 c
4.368898 11.679455 l
4.051368 11.300309 4.012363 10.869169 4.000025 10.731180 c
3.984338 10.555734 3.985626 10.347777 3.985626 10.192076 c
6.645626 10.192076 l
h
7.067238 11.527128 m
6.953648 11.633619 6.802816 11.776789 6.664093 11.885339 c
6.554986 11.970715 6.213776 12.237133 5.720003 12.264797 c
5.571209 9.608962 l
5.367124 9.620396 5.216256 9.682331 5.135852 9.722993 c
5.095299 9.743502 5.066151 9.761887 5.049939 9.772688 c
5.041450 9.778344 5.035140 9.782837 5.031198 9.785712 c
5.027243 9.788598 5.025069 9.790297 5.024859 9.790462 c
5.024831 9.790483 5.029865 9.786524 5.041348 9.776645 c
5.052709 9.766871 5.067758 9.753570 5.087788 9.735397 c
5.129661 9.697409 5.179150 9.651064 5.247954 9.586559 c
7.067238 11.527128 l
h
6.408195 9.971571 m
6.202040 9.725411 5.891789 9.591002 5.571209 9.608962 c
5.720003 12.264797 l
5.202506 12.293790 4.701684 12.076818 4.368898 11.679455 c
6.408195 9.971571 l
h
f
n
Q
Q
endstream
endobj
7 0 obj
6868
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 13.000000 12.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000001286 00000 n
0000001309 00000 n
0000001484 00000 n
0000001505 00000 n
0000001817 00000 n
0000008741 00000 n
0000008764 00000 n
0000008937 00000 n
0000009011 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
9071
%%EOF

View file

@ -7310,8 +7310,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let chatWallpaper = self.context.account.viewTracker.peerView(peerId)
|> take(1)
|> map { view -> TelegramWallpaper? in
if let cachedUserData = view.cachedData as? CachedUserData {
return cachedUserData.wallpaper
if let cachedData = view.cachedData as? CachedUserData {
return cachedData.wallpaper
} else if let cachedData = view.cachedData as? CachedChannelData {
return cachedData.wallpaper
} else {
return nil
}
@ -7463,6 +7465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
themeEmoticon = cachedData.themeEmoticon
} else if let cachedData = cachedData as? CachedChannelData {
themeEmoticon = cachedData.themeEmoticon
chatWallpaper = cachedData.wallpaper
}
strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon))
@ -13274,7 +13277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .gift:
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty {
let controller = PremiumGiftAttachmentScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
if let strongSelf = self {
strongSelf.push(c)
}

View file

@ -51,7 +51,7 @@ final class ComposeControllerNode: ASDisplayNode {
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: .generic(UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")!), action: {
openCreateNewChannelImpl?()
})
], includeChatList: false)), displayPermissionPlaceholder: false)
], includeChatList: false, topPeers: false)), displayPermissionPlaceholder: false)
super.init()

View file

@ -201,6 +201,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = true //count != 0 || self.params.alwaysEnabled
case .premiumGifting:
let maxCount: Int32 = 10
var count = 0
if case let .contacts(contactsNode) = self.contactsNode.contentNode {
count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0
}
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)")
case .channelCreation:
self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
@ -310,7 +317,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled
case .channelCreation:
case .channelCreation, .premiumGifting:
break
}
@ -318,6 +325,9 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .premiumGifting:
let maxCount: Int32 = 10
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation, .chatSelection:
break
}
@ -389,13 +399,16 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled
case .channelCreation:
case .channelCreation, .premiumGifting:
break
}
switch strongSelf.mode {
case .groupCreation:
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .premiumGifting:
let maxCount: Int32 = 10
strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)")
case .peerSelection, .channelCreation, .chatSelection:
break
}
@ -500,8 +513,14 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
}
self.contactsNode.complete = { [weak self] in
if let strongSelf = self, let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem, rightBarButtonItem.isEnabled {
strongSelf.rightNavigationButtonPressed()
if let strongSelf = self {
var available = true
if let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem {
available = rightBarButtonItem.isEnabled
}
if available {
strongSelf.rightNavigationButtonPressed()
}
}
}

View file

@ -12,6 +12,7 @@ import ChatListUI
import AnimationCache
import MultiAnimationRenderer
import EditableTokenListNode
import SolidRoundedButtonNode
private struct SearchResultEntry: Identifiable {
let index: Int
@ -72,6 +73,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let footerPanelNode: FooterPanelNode?
private let isPeerEnabled: ((EnginePeer) -> Bool)?
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer) -> Void)?, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) {
@ -85,18 +88,28 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.isPeerEnabled = isPeerEnabled
var proceedImpl: (() -> Void)?
var placeholder: String
var includeChatList = false
switch mode {
case let .peerSelection(_, searchGroups, searchChannels):
includeChatList = searchGroups || searchChannels
if searchGroups {
placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel
} else {
placeholder = self.presentationData.strings.Contacts_SearchLabel
}
default:
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
case let .peerSelection(_, searchGroups, searchChannels):
includeChatList = searchGroups || searchChannels
if searchGroups {
placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel
} else {
placeholder = self.presentationData.strings.Contacts_SearchLabel
}
self.footerPanelNode = nil
case .premiumGifting:
placeholder = self.presentationData.strings.Premium_Gift_ContactSelection_Placeholder
self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: {
proceedImpl?()
})
default:
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
self.footerPanelNode = nil
}
if case let .chatSelection(chatSelection) = mode {
@ -132,7 +145,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
}
self.contentNode = .chats(chatListNode)
} else {
self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState()))
var displayTopPeers = false
if case .premiumGifting = mode {
displayTopPeers = true
}
self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, selectionState: ContactListNodeGroupSelectionState()))
}
self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder)
@ -215,6 +232,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
searchGroups = true
searchChannels = true
globalSearch = false
case .premiumGifting:
searchChatList = true
}
let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch)), filters: filters, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true)
searchResultsNode.openPeer = { peer, _ in
@ -248,6 +267,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.tokenListNode.textReturned = { [weak self] in
self?.complete?()
}
if let footerPanelNode = self.footerPanelNode {
proceedImpl = { [weak self] in
self?.complete?()
}
self.addSubnode(footerPanelNode)
}
}
deinit {
@ -284,6 +310,21 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
insets.top += tokenListHeight
headerInsets.top += tokenListHeight
if let footerPanelNode = self.footerPanelNode {
var count = 0
if case let .contacts(contactListNode) = self.contentNode {
count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0
}
footerPanelNode.count = count
let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
if count == 0 {
transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)))
} else {
insets.bottom += panelHeight
transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)))
}
}
switch self.contentNode {
case let .contacts(contactsNode):
contactsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition)
@ -318,3 +359,68 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
})
}
}
private final class FooterPanelNode: ASDisplayNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let separatorNode: ASDisplayNode
private let button: SolidRoundedButtonView
private var validLayout: (CGFloat, CGFloat, CGFloat)?
var count: Int = 0 {
didSet {
if self.count != oldValue && self.count > 0 {
self.button.title = self.strings.Premium_Gift_ContactSelection_Proceed
self.button.badge = "\(self.count)"
if let (width, sideInset, bottomInset) = self.validLayout {
let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate)
}
}
}
}
init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0)
super.init()
self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor
self.addSubnode(self.separatorNode)
self.button.pressed = {
action()
}
}
override func didLoad() {
super.didLoad()
self.view.addSubview(self.button)
}
func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = (width, sideInset, bottomInset)
let topInset: CGFloat = 9.0
var bottomInset = bottomInset
bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0)
let buttonInset: CGFloat = 16.0 + sideInset
let buttonWidth = width - buttonInset * 2.0
let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition)
transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
return topInset + buttonHeight + bottomInset
}
}

View file

@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.filters = filters
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false, topPeers: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
contextActionImpl?(peer, node, gesture, nil)
} : nil, multipleSelection: multipleSelection)