Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-08-12 12:30:41 +02:00
commit 53d3ecc6a6
27 changed files with 633 additions and 123 deletions

View File

@ -12779,3 +12779,29 @@ Sorry for the inconvenience.";
"ChatList.SubscriptionsLowBalance.Multiple.Title" = "%@ for your subscriptions";
"ChatList.SubscriptionsLowBalance.Multiple.Text" = "Insufficient funds to cover your subscriptions.";
"ChatList.Search.SectionApps" = "APPS";
"Channel.ShowAuthors" = "Show Authors' Profiles";
"Channel.ShowAuthorsFooter" = "Add names and photos of admins to the messages they post, linking to their profiles.";
"SendStarReactions.Title" = "React with Stars";
"SendStarReactions.Balance" = "Balance";
"SendStarReactions.UserLabelAnonymous" = "Anonymous";
"SendStarReactions.SliderTop" = "TOP";
"SendStarReactions.TextSentStars_1" = "You sent **1** star to support this post.";
"SendStarReactions.TextSentStars_any" = "You sent **%d** stars to support this post.";
"SendStarReactions.TextGeneric" = "Choose how many stars you want to send to **%@** to support this post.";
"SendStarReactions.SectionTop" = "Top Senders";
"SendStarReactions.ShowMyselfInTop" = "Show me in Top Senders";
"SendStarReactions.SendButtonTitle" = "Send # %@";
"SendStarReactions.TermsOfServiceFooter" = "By sending Stars you agree to the [Terms of Service](https://telegram.org/privacy)";
"PeerInfo.AllowedReactions.StarReactions" = "Enable Paid Reactions";
"PeerInfo.AllowedReactions.StarReactionsFooter" = "Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)";
"Chat.ToastStarsSent.Title_1" = "Star sent!";
"Chat.ToastStarsSent.Title_any" = "Stars sent!";
"Chat.ToastStarsSent.Text" = "You have reacted with %1$@ %2$@.";
"Chat.ToastStarsSent.TextStarAmount_1" = "star";
"Chat.ToastStarsSent.TextStarAmount_any" = "stars";

View File

@ -94,8 +94,12 @@ private func calculateColors(context: AccountContext?, explicitColorIndex: Int?,
colors = AvatarNode.repostColors
} else if case .repliesIcon = icon {
colors = AvatarNode.savedMessagesColors
} else if case .anonymousSavedMessagesIcon = icon {
colors = AvatarNode.savedMessagesColors
} else if case let .anonymousSavedMessagesIcon(isColored) = icon {
if isColored {
colors = AvatarNode.savedMessagesColors
} else {
colors = AvatarNode.grayscaleColors
}
} else if case .myNotesIcon = icon {
colors = AvatarNode.savedMessagesColors
} else if case .editAvatarIcon = icon, let theme {
@ -178,7 +182,7 @@ private enum AvatarNodeIcon: Equatable {
case none
case savedMessagesIcon
case repliesIcon
case anonymousSavedMessagesIcon
case anonymousSavedMessagesIcon(isColored: Bool)
case myNotesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
@ -192,7 +196,7 @@ public enum AvatarNodeImageOverride: Equatable {
case image(TelegramMediaImageRepresentation)
case savedMessagesIcon
case repliesIcon
case anonymousSavedMessagesIcon
case anonymousSavedMessagesIcon(isColored: Bool)
case myNotesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon(forceNone: Bool)
@ -506,9 +510,9 @@ public final class AvatarNode: ASDisplayNode {
case .repliesIcon:
representation = nil
icon = .repliesIcon
case .anonymousSavedMessagesIcon:
case let .anonymousSavedMessagesIcon(isColored):
representation = nil
icon = .anonymousSavedMessagesIcon
icon = .anonymousSavedMessagesIcon(isColored: isColored)
case .myNotesIcon:
representation = nil
icon = .myNotesIcon
@ -681,9 +685,9 @@ public final class AvatarNode: ASDisplayNode {
case .repliesIcon:
representation = nil
icon = .repliesIcon
case .anonymousSavedMessagesIcon:
case let .anonymousSavedMessagesIcon(isColored):
representation = nil
icon = .anonymousSavedMessagesIcon
icon = .anonymousSavedMessagesIcon(isColored: isColored)
case .myNotesIcon:
representation = nil
icon = .myNotesIcon

View File

@ -742,8 +742,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
if case .channels = key {
headerType = .channels
} else if case .apps = key {
//TODO:localize
headerType = .text("APPS", AnyHashable("apps"))
headerType = .text(strings.ChatList_Search_SectionApps, AnyHashable("apps"))
} else {
if filter.contains(.onlyGroups) {
headerType = .chats

View File

@ -1661,7 +1661,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if peer.id.isReplies {
overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages {
overrideImage = .anonymousSavedMessagesIcon
overrideImage = .anonymousSavedMessagesIcon(isColored: true)
} else if peer.id == item.context.account.peerId && !displayAsMessage {
if case .savedMessagesChats = item.chatListLocation {
overrideImage = .myNotesIcon

View File

@ -1093,7 +1093,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} else if peer.id.isReplies, case .generalSearch = item.peerMode {
overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages, case .generalSearch = item.peerMode {
overrideImage = .anonymousSavedMessagesIcon
overrideImage = .anonymousSavedMessagesIcon(isColored: true)
} else if peer.isDeleted {
overrideImage = .deletedIcon
}

View File

@ -36,7 +36,7 @@ final class ReactionPreviewView: UIView {
size: size,
placeholderColor: .clear,
themeColor: .white,
loopMode: .count(0)
loopMode: .forever
),
isVisibleForAnimations: true,
action: nil

View File

@ -85,7 +85,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} else if chatPeer.id.isReplies {
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon)
} else if chatPeer.id.isAnonymousSavedMessages {
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon)
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon(isColored: true))
} else {
var overrideImage: AvatarNodeImageOverride?
if chatPeer.isDeleted {

View File

@ -496,9 +496,10 @@ private func channelAdminsControllerEntries(presentationData: PresentationData,
if !isGroup && peer.hasPermission(.sendSomething) {
entries.append(.signMessages(presentationData.theme, presentationData.strings.Channel_SignMessages, signMessagesEnabled, showAuthorProfilesEnabled))
//TODO:localize
entries.append(.showAuthorProfiles(presentationData.theme, "Show Authors' Profiles", showAuthorProfilesEnabled, signMessagesEnabled))
entries.append(.signMessagesInfo(presentationData.theme, "Add names and photos of admins to the messages they post, linking to their profiles."))
entries.append(.showAuthorProfiles(presentationData.theme, presentationData.strings.Channel_ShowAuthors, showAuthorProfilesEnabled, signMessagesEnabled))
if signMessagesEnabled {
entries.append(.signMessagesInfo(presentationData.theme, presentationData.strings.Channel_ShowAuthorsFooter))
}
}
}
} else if case let .legacyGroup(peer) = peer {

View File

@ -2622,6 +2622,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
}
}
foundItemNode?.animateHideEffects()
if let customReactionSource = self.customReactionSource {
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, icon: .none, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
if let contents = customReactionSource.layer.contents {
@ -2673,7 +2675,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
if case .builtin = itemNode.item.reaction.rawValue {
selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5)
} else if case .stars = itemNode.item.reaction.rawValue {
selfTargetBounds = selfTargetBounds.insetBy(dx: selfTargetBounds.width * 0.0, dy: selfTargetBounds.height * 0.0)
selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.13, dy: -selfTargetBounds.height * 0.13).offsetBy(dx: -0.5, dy: -0.5)
}
let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView)
@ -2905,7 +2907,14 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
})
if !switchToInlineImmediately {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: {
let maxDuration: Double
if case .stars = value {
maxDuration = 3.0
} else {
maxDuration = 2.0
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, maxDuration * UIView.animationDurationFactor()), execute: {
if self.didTriggerExpandedReaction {
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation {
@ -3986,8 +3995,6 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
starView.isHidden = true
} else {
targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate)
//TODO:release
//targetView.addSubnode(itemNode)
}
} else if let targetView = targetView as? UIImageView {
starView.isHidden = true

View File

@ -296,6 +296,12 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
self.customContentsNode?.contents = contents
}
public func animateHideEffects() {
if let starsEffectLayer = self.starsEffectLayer {
starsEffectLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
public func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
let intrinsicSize = size

View File

@ -64,8 +64,9 @@ extension ReactionsMessageAttribute {
peerId: peerId?.peerId,
count: count,
isTop: (flags & (1 << 0)) != 0,
isMy: (flags & (1 << 1)) != 0)
)
isMy: (flags & (1 << 1)) != 0,
isAnonymous: (flags & (1 << 2)) != 0
))
}
}
}
@ -210,10 +211,16 @@ public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool)
updatedCount += reactions[index].count
reactions.remove(at: index)
}
var topPeers = result.topPeers
if let index = topPeers.firstIndex(where: { $0.isMy }) {
topPeers[index].count += pendingStars.count
} else {
topPeers.append(ReactionsMessageAttribute.TopPeer(peerId: pendingStars.accountPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: pendingStars.isAnonymous))
}
reactions.insert(MessageReaction(value: .stars, count: updatedCount, chosenOrder: -1), at: 0)
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers, topPeers: result.topPeers)
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers, topPeers: topPeers)
} else {
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: [], topPeers: [])
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: [], topPeers: [ReactionsMessageAttribute.TopPeer(peerId: pendingStars.accountPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: pendingStars.isAnonymous)])
}
} else {
return result
@ -254,8 +261,9 @@ extension ReactionsMessageAttribute {
peerId: peerId?.peerId,
count: count,
isTop: (flags & (1 << 0)) != 0,
isMy: (flags & (1 << 1)) != 0)
)
isMy: (flags & (1 << 1)) != 0,
isAnonymous: (flags & (1 << 2)) != 0
))
}
}
}

View File

@ -172,7 +172,7 @@ public func updateMessageReactionsInteractively(account: Account, messageIds: [M
|> ignoreValues
}
public func sendStarsReactionsInteractively(account: Account, messageId: MessageId, count: Int) -> Signal<Never, NoError> {
public func sendStarsReactionsInteractively(account: Account, messageId: MessageId, count: Int, isAnonymous: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: SendStarsReactionsAction(randomId: Int64.random(in: Int64.min ... Int64.max)))
transaction.updateMessage(messageId, update: { currentMessage in
@ -190,7 +190,7 @@ public func sendStarsReactionsInteractively(account: Account, messageId: Message
}
}
attributes.append(PendingStarsReactionsMessageAttribute(accountPeerId: account.peerId, count: mappedCount))
attributes.append(PendingStarsReactionsMessageAttribute(accountPeerId: account.peerId, count: mappedCount, isAnonymous: isAnonymous))
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
@ -220,6 +220,41 @@ func cancelPendingSendStarsReactionInteractively(account: Account, messageId: Me
|> ignoreValues
}
func _internal_updateStarsReactionIsAnonymous(account: Account, messageId: MessageId, isAnonymous: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
for j in (0 ..< attributes.count).reversed() {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
var updatedTopPeers = attribute.topPeers
if let index = updatedTopPeers.firstIndex(where: { $0.isMy }) {
updatedTopPeers[index].isAnonymous = isAnonymous
}
attributes[j] = ReactionsMessageAttribute(canViewList: attribute.canViewList, isTags: attribute.isTags, reactions: attribute.reactions, recentPeers: attribute.recentPeers, topPeers: updatedTopPeers)
}
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer else {
return .complete()
}
return account.network.request(Api.functions.messages.togglePaidReactionPrivacy(peer: inputPeer, msgId: messageId.id, private: isAnonymous ? .boolTrue : .boolFalse))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
}
private enum RequestUpdateMessageReactionError {
case generic
}
@ -308,7 +343,7 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
}
private func requestSendStarsReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, RequestUpdateMessageReactionError> {
return postbox.transaction { transaction -> (Peer, Int32)? in
return postbox.transaction { transaction -> (Peer, Int32, Bool)? in
guard let peer = transaction.getPeer(messageId.peerId) else {
return nil
}
@ -316,17 +351,19 @@ private func requestSendStarsReaction(postbox: Postbox, network: Network, stateM
return nil
}
var count: Int32 = 0
var isAnonymous = false
for attribute in message.attributes {
if let attribute = attribute as? PendingStarsReactionsMessageAttribute {
count += attribute.count
isAnonymous = attribute.isAnonymous
break
}
}
return (peer, count)
return (peer, count, isAnonymous)
}
|> castError(RequestUpdateMessageReactionError.self)
|> mapToSignal { peerAndValue in
guard let (peer, count) = peerAndValue else {
guard let (peer, count, isAnonymous) = peerAndValue else {
return .fail(.generic)
}
guard let inputPeer = apiInputPeer(peer) else {
@ -341,6 +378,11 @@ private func requestSendStarsReaction(postbox: Postbox, network: Network, stateM
let timestampPart = UInt64(UInt32(bitPattern: Int32(Date().timeIntervalSince1970)))
let randomId = (timestampPart << 32) | randomPartId
var flags: Int32 = 0
if isAnonymous {
flags |= 1 << 0
}
let signal: Signal<Never, RequestUpdateMessageReactionError> = network.request(Api.functions.messages.sendPaidReaction(flags: 0, peer: inputPeer, msgId: messageId.id, count: count, randomId: Int64(bitPattern: randomId)))
|> mapError { _ -> RequestUpdateMessageReactionError in
return .generic

View File

@ -333,12 +333,14 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
public var count: Int32
public var isTop: Bool
public var isMy: Bool
public var isAnonymous: Bool
public init(peerId: PeerId?, count: Int32, isTop: Bool, isMy: Bool) {
public init(peerId: PeerId?, count: Int32, isTop: Bool, isMy: Bool, isAnonymous: Bool) {
self.peerId = peerId
self.count = count
self.isMy = isMy
self.isTop = isTop
self.isAnonymous = isAnonymous
}
public init(decoder: PostboxDecoder) {
@ -350,6 +352,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
self.isTop = decoder.decodeBoolForKey("t", orElse: false)
self.isMy = decoder.decodeBoolForKey("m", orElse: false)
self.isAnonymous = decoder.decodeBoolForKey("anon", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
@ -361,6 +364,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
encoder.encodeInt32(self.count, forKey: "c")
encoder.encodeBool(self.isTop, forKey: "t")
encoder.encodeBool(self.isMy, forKey: "m")
encoder.encodeBool(self.isAnonymous, forKey: "anon")
}
}
@ -561,6 +565,7 @@ public final class PendingReactionsMessageAttribute: MessageAttribute {
public final class PendingStarsReactionsMessageAttribute: MessageAttribute {
public let accountPeerId: PeerId?
public let count: Int32
public let isAnonymous: Bool
public var associatedPeerIds: [PeerId] {
var peerIds: [PeerId] = []
@ -570,14 +575,16 @@ public final class PendingStarsReactionsMessageAttribute: MessageAttribute {
return peerIds
}
public init(accountPeerId: PeerId?, count: Int32) {
public init(accountPeerId: PeerId?, count: Int32, isAnonymous: Bool) {
self.accountPeerId = accountPeerId
self.count = count
self.isAnonymous = isAnonymous
}
required public init(decoder: PostboxDecoder) {
self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init)
self.count = decoder.decodeInt32ForKey("cnt", orElse: 1)
self.isAnonymous = decoder.decodeBoolForKey("anon", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
@ -587,5 +594,6 @@ public final class PendingStarsReactionsMessageAttribute: MessageAttribute {
encoder.encodeNil(forKey: "ap")
}
encoder.encodeInt32(self.count, forKey: "cnt")
encoder.encodeBool(self.isAnonymous, forKey: "anon")
}
}

View File

@ -334,13 +334,17 @@ public extension TelegramEngine {
).startStandalone()
}
public func sendStarsReaction(id: EngineMessage.Id, count: Int) {
let _ = sendStarsReactionsInteractively(account: self.account, messageId: id, count: count).startStandalone()
public func sendStarsReaction(id: EngineMessage.Id, count: Int, isAnonymous: Bool) {
let _ = sendStarsReactionsInteractively(account: self.account, messageId: id, count: count, isAnonymous: isAnonymous).startStandalone()
}
public func cancelPendingSendStarsReaction(id: EngineMessage.Id) {
let _ = cancelPendingSendStarsReactionInteractively(account: self.account, messageId: id).startStandalone()
}
public func updateStarsReactionIsAnonymous(id: EngineMessage.Id, isAnonymous: Bool) -> Signal<Never, NoError> {
return _internal_updateStarsReactionIsAnonymous(account: self.account, messageId: id, isAnonymous: isAnonymous)
}
public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)

View File

@ -1550,7 +1550,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
//TODO:release
if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, firstMessage.author?.id != channel.id {
if info.flags.contains(.messagesShouldHaveProfiles) {
var allowAuthor = incoming

View File

@ -127,7 +127,6 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
if let author = message.author as? TelegramUser {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
//TODO:release
} else {
authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
@ -137,7 +136,6 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
} else {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
//TODO:release
} else {
for attribute in message.attributes {
if let attribute = attribute as? AuthorSignatureMessageAttribute {

View File

@ -345,7 +345,6 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
if !isBroadcastChannel {
hasAvatar = true
} else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id {
//TODO:release
if info.flags.contains(.messagesShouldHaveProfiles) {
hasAvatar = true
effectiveAuthor = message.author

View File

@ -754,7 +754,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
strongSelf.textAccessibilityOverlayNode.frame = textFrame
//TODO:localize
//TODO:release
//strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
strongSelf.updateIsTranslating(isTranslating)

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/AvatarNode",
"//submodules/Components/BundleIconComponent",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",

View File

@ -18,6 +18,7 @@ import SliderComponent
import RoundedRectWithTailPath
import AvatarNode
import BundleIconComponent
import CheckNode
private final class BalanceComponent: CombinedComponent {
let context: AccountContext
@ -61,10 +62,9 @@ private final class BalanceComponent: CombinedComponent {
return { context in
var size = CGSize(width: 0.0, height: 0.0)
//TODO:localize
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: context.component.strings.SendStarReactions_Balance, font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor))
),
availableSize: context.availableSize,
transition: .immediate
@ -536,9 +536,9 @@ private final class PeerComponent: Component {
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize)
avatarNode.frame = avatarFrame
if let peer = component.peer {
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true)
} else {
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: false), synchronousLoad: true)
}
avatarNode.updateSize(size: avatarFrame.size)
@ -565,8 +565,7 @@ private final class PeerComponent: Component {
if let peer = component.peer {
peerTitle = peer.compactDisplayTitle
} else {
//TODO:localize
peerTitle = "Anonymous"
peerTitle = component.strings.SendStarReactions_UserLabelAnonymous
}
let titleSize = self.title.update(
@ -600,15 +599,18 @@ private final class PeerComponent: Component {
private final class SliderBackgroundComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let value: CGFloat
let topCutoff: CGFloat?
init(
theme: PresentationTheme,
strings: PresentationStrings,
value: CGFloat,
topCutoff: CGFloat?
) {
self.theme = theme
self.strings = strings
self.value = value
self.topCutoff = topCutoff
}
@ -617,6 +619,9 @@ private final class SliderBackgroundComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.value != rhs.value {
return false
}
@ -626,6 +631,37 @@ private final class SliderBackgroundComponent: Component {
return true
}
private enum TopTextOverflowState {
case left
case center
case right
func animates(from: TopTextOverflowState) -> Bool {
switch self {
case .left:
return false
case .center:
switch from {
case .left:
return false
case .center:
return false
case .right:
return true
}
case .right:
switch from {
case .left:
return false
case .center:
return true
case .right:
return false
}
}
}
}
final class View: UIView {
private let sliderBackground = UIView()
private let sliderForeground = UIView()
@ -636,6 +672,8 @@ private final class SliderBackgroundComponent: Component {
private let topForegroundText = ComponentView<Empty>()
private let topBackgroundText = ComponentView<Empty>()
private var topTextOverflowState: TopTextOverflowState?
override init(frame: CGRect) {
super.init(frame: frame)
@ -656,7 +694,7 @@ private final class SliderBackgroundComponent: Component {
}
func update(component: SliderBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF)
self.sliderBackground.backgroundColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(component.theme.overallDarkAppearance ? 0.2 : 0.07)
self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D)
self.topForegroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
self.topBackgroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor
@ -678,15 +716,37 @@ private final class SliderBackgroundComponent: Component {
let topCutoff = component.topCutoff ?? 0.0
let topLineFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(sliderAreaWidth * topCutoff), y: 0.0), size: CGSize(width: 1.0, height: availableSize.height))
transition.setFrame(layer: self.topForegroundLine, frame: topLineFrame)
transition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame)
let topX = floorToScreenPixels(sliderAreaWidth * topCutoff)
let topLineAvoidDistance = 6.0
let knobWidth: CGFloat = 30.0
let topLineClosestEdge = min(abs(sliderForegroundFrame.maxX - topX), abs(sliderForegroundFrame.maxX - knobWidth - topX))
var topLineOverlayFactor = topLineClosestEdge / topLineAvoidDistance
topLineOverlayFactor = max(0.0, min(1.0, topLineOverlayFactor))
if sliderForegroundFrame.maxX - knobWidth <= topX && sliderForegroundFrame.maxX >= topX {
topLineOverlayFactor = 0.0
}
let topLineHeight: CGFloat = availableSize.height
let topLineAlpha: CGFloat = topLineOverlayFactor * topLineOverlayFactor
let topLineFrameTransition = transition
let topLineAlphaTransition = transition
/*if transition.userData(ChatSendStarsScreenComponent.IsAdjustingAmountHint.self) != nil {
topLineFrameTransition = .easeInOut(duration: 0.12)
topLineAlphaTransition = .easeInOut(duration: 0.12)
}*/
let topLineFrame = CGRect(origin: CGPoint(x: topX, y: (availableSize.height - topLineHeight) * 0.5), size: CGSize(width: 1.0, height: topLineHeight))
topLineFrameTransition.setFrame(layer: self.topForegroundLine, frame: topLineFrame)
topLineAlphaTransition.setAlpha(layer: self.topForegroundLine, alpha: topLineAlpha)
topLineFrameTransition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame)
topLineAlphaTransition.setAlpha(layer: self.topBackgroundLine, alpha: topLineAlpha)
//TODO:localize
let topTextSize = self.topForegroundText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 1.0, alpha: 0.4)))
text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: UIColor(white: 1.0, alpha: 0.4)))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
@ -694,12 +754,30 @@ private final class SliderBackgroundComponent: Component {
let _ = self.topBackgroundText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "TOP", font: Font.medium(17.0), textColor: UIColor(white: 0.0, alpha: 0.1)))
text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: component.theme.overallDarkAppearance ? UIColor(white: 1.0, alpha: 0.22) : UIColor(white: 0.0, alpha: 0.2)))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize)
var topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize)
let topTextFrameTransition = transition
let topTextLeftInset: CGFloat = 4.0
var topTextOverflowWidth: CGFloat = 0.0
let topTextOverflowState: TopTextOverflowState
if sliderForegroundFrame.maxX < topTextFrame.minX - topTextLeftInset {
topTextOverflowState = .left
} else if sliderForegroundFrame.maxX >= topTextFrame.minX - topTextLeftInset && sliderForegroundFrame.maxX - knobWidth < topTextFrame.maxX + topTextLeftInset {
topTextOverflowWidth = sliderForegroundFrame.maxX - (topTextFrame.minX - topTextLeftInset)
topTextOverflowState = .center
} else {
topTextOverflowState = .right
}
topTextFrame.origin.x += topTextOverflowWidth
if let topForegroundTextView = self.topForegroundText.view, let topBackgroundTextView = self.topBackgroundText.view {
if topForegroundTextView.superview == nil {
topBackgroundTextView.layer.anchorPoint = CGPoint()
@ -709,17 +787,30 @@ private final class SliderBackgroundComponent: Component {
self.sliderForeground.addSubview(topForegroundTextView)
}
transition.setPosition(view: topForegroundTextView, position: topTextFrame.origin)
transition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin)
var animateTopTextAdditionalX: CGFloat = 0.0
if transition.userData(ChatSendStarsScreenComponent.IsAdjustingAmountHint.self) != nil {
if let previousState = self.topTextOverflowState, previousState != topTextOverflowState, topTextOverflowState.animates(from: previousState) {
animateTopTextAdditionalX = topForegroundTextView.center.x - topTextFrame.origin.x
}
}
topTextFrameTransition.setPosition(view: topForegroundTextView, position: topTextFrame.origin)
topTextFrameTransition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin)
topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
topBackgroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size)
topForegroundTextView.isHidden = component.topCutoff == nil || topTextFrame.minX <= 10.0 || topTextFrame.maxX >= availableSize.width - 4.0
if animateTopTextAdditionalX != 0.0 {
topForegroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true)
topBackgroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true)
}
topForegroundTextView.isHidden = component.topCutoff == nil
topBackgroundTextView.isHidden = topForegroundTextView.isHidden
self.topBackgroundLine.isHidden = topForegroundTextView.isHidden
self.topForegroundLine.isHidden = topForegroundTextView.isHidden
self.topBackgroundLine.isHidden = topX < 10.0
self.topForegroundLine.isHidden = self.topBackgroundLine.isHidden
}
self.topTextOverflowState = topTextOverflowState
return availableSize
}
@ -735,31 +826,43 @@ private final class SliderBackgroundComponent: Component {
}
private final class ChatSendStarsScreenComponent: Component {
final class IsAdjustingAmountHint {
}
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let myPeer: EnginePeer
let messageId: EngineMessage.Id
let maxAmount: Int
let balance: Int64?
let currentSentAmount: Int?
let topPeers: [ChatSendStarsScreen.TopPeer]
let completion: (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
let myTopPeer: ChatSendStarsScreen.TopPeer?
let completion: (Int64, Bool, Bool, ChatSendStarsScreen.TransitionOut) -> Void
init(
context: AccountContext,
peer: EnginePeer,
myPeer: EnginePeer,
messageId: EngineMessage.Id,
maxAmount: Int,
balance: Int64?,
currentSentAmount: Int?,
topPeers: [ChatSendStarsScreen.TopPeer],
completion: @escaping (Int64, Bool, ChatSendStarsScreen.TransitionOut) -> Void
myTopPeer: ChatSendStarsScreen.TopPeer?,
completion: @escaping (Int64, Bool, Bool, ChatSendStarsScreen.TransitionOut) -> Void
) {
self.context = context
self.peer = peer
self.myPeer = myPeer
self.messageId = messageId
self.maxAmount = maxAmount
self.balance = balance
self.currentSentAmount = currentSentAmount
self.topPeers = topPeers
self.myTopPeer = myTopPeer
self.completion = completion
}
@ -770,6 +873,9 @@ private final class ChatSendStarsScreenComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.myPeer != rhs.myPeer {
return false
}
if lhs.maxAmount != rhs.maxAmount {
return false
}
@ -782,6 +888,9 @@ private final class ChatSendStarsScreenComponent: Component {
if lhs.topPeers != rhs.topPeers {
return false
}
if lhs.myTopPeer != rhs.myTopPeer {
return false
}
return true
}
@ -829,6 +938,9 @@ private final class ChatSendStarsScreenComponent: Component {
private var topPeersTitleBackground: SimpleLayer?
private var topPeersTitle: ComponentView<Empty>?
private var anonymousSeparator = SimpleLayer()
private var anonymousContents = ComponentView<Empty>()
private var topPeerItems: [ChatSendStarsScreen.TopPeer.Id: ComponentView<Empty>] = [:]
private let actionButton = ComponentView<Empty>()
@ -846,6 +958,7 @@ private final class ChatSendStarsScreenComponent: Component {
private var topOffsetDistance: CGFloat?
private var amount: Int64 = 1
private var isAnonymous: Bool = false
private var cachedStarImage: (UIImage, PresentationTheme)?
private var cachedCloseImage: UIImage?
@ -1014,6 +1127,9 @@ private final class ChatSendStarsScreenComponent: Component {
if self.component == nil {
self.amount = 50
if let myTopPeer = component.myTopPeer {
self.isAnonymous = myTopPeer.isAnonymous
}
}
self.component = component
@ -1054,7 +1170,7 @@ private final class ChatSendStarsScreenComponent: Component {
return
}
self.amount = 1 + Int64(value)
self.state?.updated(transition: .immediate)
self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint()))
let sliderValue = Float(value) / Float(component.maxAmount)
let currentTimestamp = CACurrentMediaTime()
@ -1112,7 +1228,18 @@ private final class ChatSendStarsScreenComponent: Component {
let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(component.maxAmount - 1)
let topCount = component.topPeers.max(by: { $0.count < $1.count })?.count
let topOthersCount: Int? = component.topPeers.filter({ !$0.isMy }).max(by: { $0.count < $1.count })?.count
var topCount: Int?
if let topOthersCount {
if let myTopPeer = component.myTopPeer {
topCount = max(0, topOthersCount - myTopPeer.count + 1)
} else {
topCount = topOthersCount
}
if topCount == 0 {
topCount = nil
}
}
var topCutoffFraction: CGFloat?
if let topCount {
@ -1132,6 +1259,7 @@ private final class ChatSendStarsScreenComponent: Component {
transition: transition,
component: AnyComponent(SliderBackgroundComponent(
theme: environment.theme,
strings: environment.strings,
value: progressFraction,
topCutoff: topCutoffFraction
)),
@ -1264,7 +1392,7 @@ private final class ChatSendStarsScreenComponent: Component {
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
@ -1282,9 +1410,9 @@ private final class ChatSendStarsScreenComponent: Component {
let text: String
if let currentSentAmount = component.currentSentAmount {
text = "You sent **\(currentSentAmount)** stars to support this post."
text = environment.strings.SendStarReactions_TextSentStars(Int32(currentSentAmount))
} else {
text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post."
text = environment.strings.SendStarReactions_TextGeneric(component.peer.debugDisplayTitle).string
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
@ -1358,11 +1486,10 @@ private final class ChatSendStarsScreenComponent: Component {
topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
//TODO:localize
let topPeersTitleSize = topPeersTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white))
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 300.0, height: 100.0)
@ -1387,9 +1514,36 @@ private final class ChatSendStarsScreenComponent: Component {
transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel)))
transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel)))
var mappedTopPeers = component.topPeers
if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) {
mappedTopPeers.remove(at: index)
}
var myCount = Int(self.amount)
if let myTopPeer = component.myTopPeer {
myCount += myTopPeer.count
}
mappedTopPeers.append(ChatSendStarsScreen.TopPeer(
peer: self.isAnonymous ? nil : component.myPeer,
isMy: true,
count: myCount
))
mappedTopPeers.sort(by: { $0.count > $1.count })
if mappedTopPeers.count > 3 {
mappedTopPeers = Array(mappedTopPeers.prefix(3))
}
var animateItems = false
var itemPositionTransition = transition
var itemAlphaTransition = transition
if transition.userData(IsAdjustingAmountHint.self) != nil {
animateItems = true
itemPositionTransition = .spring(duration: 0.3)
itemAlphaTransition = .easeInOut(duration: 0.15)
}
var validIds: [ChatSendStarsScreen.TopPeer.Id] = []
var items: [(itemView: ComponentView<Empty>, size: CGSize)] = []
for topPeer in component.topPeers {
for topPeer in mappedTopPeers {
validIds.append(topPeer.id)
let itemView: ComponentView<Empty>
@ -1439,7 +1593,17 @@ private final class ChatSendStarsScreenComponent: Component {
for (id, itemView) in self.topPeerItems {
if !validIds.contains(id) {
removedIds.append(id)
itemView.view?.removeFromSuperview()
if animateItems {
if let itemComponentView = itemView.view {
itemPositionTransition.setScale(view: itemComponentView, scale: 0.001)
itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
} else {
itemView.view?.removeFromSuperview()
}
}
}
for id in removedIds {
@ -1459,10 +1623,26 @@ private final class ChatSendStarsScreenComponent: Component {
var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing
for (itemView, itemSize) in items {
if let itemComponentView = itemView.view {
var animateItem = animateItems
if itemComponentView.superview == nil {
self.scrollContentView.addSubview(itemComponentView)
animateItem = false
ComponentTransition.immediate.setScale(view: itemComponentView, scale: 0.001)
itemComponentView.alpha = 0.0
}
itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize)
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize)
if animateItem {
itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center)
itemPositionTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
} else {
itemComponentView.center = itemFrame.center
itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
}
itemPositionTransition.setScale(view: itemComponentView, scale: 1.0)
itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 1.0)
}
itemX += itemSize.width + itemSpacing
}
@ -1470,13 +1650,80 @@ private final class ChatSendStarsScreenComponent: Component {
contentHeight += 161.0
}
do {
if !component.topPeers.isEmpty {
contentHeight += 2.0
}
if self.anonymousSeparator.superlayer == nil {
self.scrollContentView.layer.addSublayer(self.anonymousSeparator)
}
self.anonymousSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
let checkTheme = CheckComponent.Theme(
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
borderColor: environment.theme.list.itemCheckColors.strokeColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
)
let anonymousContentsSize = self.anonymousContents.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
theme: checkTheme,
selected: !self.isAnonymous
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_ShowMyselfInTop, font: Font.regular(16.0), textColor: environment.theme.list.itemPrimaryTextColor))
)))
],
spacing: 10.0
)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
self.isAnonymous = !self.isAnonymous
self.state?.updated(transition: .easeInOut(duration: 0.2))
if component.myTopPeer != nil {
let _ = component.context.engine.messages.updateStarsReactionIsAnonymous(id: component.messageId, isAnonymous: self.isAnonymous).startStandalone()
}
},
animateAlpha: false,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
transition.setFrame(layer: self.anonymousSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)))
contentHeight += 21.0
let anonymousContentsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - anonymousContentsSize.width) * 0.5), y: contentHeight), size: anonymousContentsSize)
if let anonymousContentsView = self.anonymousContents.view {
if anonymousContentsView.superview == nil {
self.scrollContentView.addSubview(anonymousContentsView)
}
transition.setFrame(view: anonymousContentsView, frame: anonymousContentsFrame)
}
contentHeight += anonymousContentsSize.height + 27.0
}
initialContentHeight = contentHeight
if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme {
self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme)
}
let buttonString = "Send # \(self.amount)"
let buttonString = environment.strings.SendStarReactions_SendButtonTitle("\(self.amount)").string
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
@ -1541,6 +1788,7 @@ private final class ChatSendStarsScreenComponent: Component {
component.completion(
self.amount,
self.isAnonymous,
isBecomingTop,
ChatSendStarsScreen.TransitionOut(
sourceView: badgeView.badgeIcon
@ -1556,7 +1804,7 @@ private final class ChatSendStarsScreenComponent: Component {
let buttonDescriptionTextSize = self.buttonDescriptionText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes(
text: .markdown(text: environment.strings.SendStarReactions_TermsOfServiceFooter, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
@ -1639,41 +1887,60 @@ private final class ChatSendStarsScreenComponent: Component {
public class ChatSendStarsScreen: ViewControllerComponentContainer {
public final class InitialData {
fileprivate let peer: EnginePeer
fileprivate let myPeer: EnginePeer
fileprivate let messageId: EngineMessage.Id
fileprivate let balance: Int64?
fileprivate let currentSentAmount: Int?
fileprivate let topPeers: [ChatSendStarsScreen.TopPeer]
fileprivate let myTopPeer: ChatSendStarsScreen.TopPeer?
fileprivate init(
peer: EnginePeer,
myPeer: EnginePeer,
messageId: EngineMessage.Id,
balance: Int64?,
currentSentAmount: Int?,
topPeers: [ChatSendStarsScreen.TopPeer]
topPeers: [ChatSendStarsScreen.TopPeer],
myTopPeer: ChatSendStarsScreen.TopPeer?
) {
self.peer = peer
self.myPeer = myPeer
self.messageId = messageId
self.balance = balance
self.currentSentAmount = currentSentAmount
self.topPeers = topPeers
self.myTopPeer = myTopPeer
}
}
fileprivate final class TopPeer: Equatable {
struct Id: Hashable {
var value: EnginePeer.Id?
init(_ value: EnginePeer.Id?) {
self.value = value
}
enum Id: Hashable {
case anonymous
case my
case peer(EnginePeer.Id)
}
var id: Id {
return Id(self.peer?.id)
if self.isMy {
return .my
} else if let peer = self.peer {
return .peer(peer.id)
} else {
return .anonymous
}
}
var isAnonymous: Bool {
return self.peer == nil
}
let peer: EnginePeer?
let isMy: Bool
let count: Int
init(peer: EnginePeer?, count: Int) {
init(peer: EnginePeer?, isMy: Bool, count: Int) {
self.peer = peer
self.isMy = isMy
self.count = count
}
@ -1681,6 +1948,9 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
if lhs.peer != rhs.peer {
return false
}
if lhs.isMy != rhs.isMy {
return false
}
if lhs.count != rhs.count {
return false
}
@ -1703,7 +1973,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
private var presenceDisposable: Disposable?
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, TransitionOut) -> Void) {
public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, Bool, TransitionOut) -> Void) {
self.context = context
var maxAmount = 2500
@ -1714,10 +1984,13 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
super.init(context: context, component: ChatSendStarsScreenComponent(
context: context,
peer: initialData.peer,
myPeer: initialData.myPeer,
messageId: initialData.messageId,
maxAmount: maxAmount,
balance: initialData.balance,
currentSentAmount: initialData.currentSentAmount,
topPeers: initialData.topPeers,
myTopPeer: initialData.myTopPeer,
completion: completion
), navigationBarAppearance: .none)
@ -1748,7 +2021,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
}
}
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal<InitialData?, NoError> {
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, messageId: EngineMessage.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal<InitialData?, NoError> {
let balance: Signal<Int64?, NoError>
if let starsContext = context.starsContext {
balance = starsContext.state
@ -1761,10 +2034,14 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
}
var currentSentAmount: Int?
var myTopPeer: ReactionsMessageAttribute.TopPeer?
if let myPeer = topPeers.first(where: { $0.isMy }) {
myTopPeer = myPeer
currentSentAmount = Int(myPeer.count)
}
let allPeerIds = topPeers.compactMap(\.peerId)
var topPeers = topPeers.sorted(by: { $0.count > $1.count })
if topPeers.count > 3 {
topPeers = Array(topPeers.prefix(3))
@ -1773,26 +2050,28 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
EngineDataMap(topPeers.map(\.peerId).compactMap {
$0.flatMap(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
})
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
EngineDataMap(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
),
balance
)
|> map { peerAndTopPeerMap, balance -> InitialData? in
let (peer, topPeerMap) = peerAndTopPeerMap
guard let peer else {
let (peer, myPeer, topPeerMap) = peerAndTopPeerMap
guard let peer, let myPeer else {
return nil
}
return InitialData(
peer: peer,
myPeer: myPeer,
messageId: messageId,
balance: balance,
currentSentAmount: currentSentAmount,
topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in
guard let topPeerId = topPeer.peerId else {
return ChatSendStarsScreen.TopPeer(
peer: nil,
isMy: topPeer.isMy,
count: Int(topPeer.count)
)
}
@ -1803,7 +2082,28 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
return nil
}
return ChatSendStarsScreen.TopPeer(
peer: topPeerValue,
peer: topPeer.isAnonymous ? nil : topPeerValue,
isMy: topPeer.isMy,
count: Int(topPeer.count)
)
},
myTopPeer: myTopPeer.flatMap { topPeer -> ChatSendStarsScreen.TopPeer? in
guard let topPeerId = topPeer.peerId else {
return ChatSendStarsScreen.TopPeer(
peer: nil,
isMy: topPeer.isMy,
count: Int(topPeer.count)
)
}
guard let topPeerValue = topPeerMap[topPeerId] else {
return nil
}
guard let topPeerValue else {
return nil
}
return ChatSendStarsScreen.TopPeer(
peer: topPeer.isAnonymous ? nil : topPeerValue,
isMy: topPeer.isMy,
count: Int(topPeer.count)
)
}
@ -2042,3 +2342,97 @@ private final class SliderStarsView: UIView {
self.emitterLayer.emitterSize = size
}
}
private final class CheckComponent: Component {
struct Theme: Equatable {
public let backgroundColor: UIColor
public let strokeColor: UIColor
public let borderColor: UIColor
public let overlayBorder: Bool
public let hasInset: Bool
public let hasShadow: Bool
public let filledBorder: Bool
public let borderWidth: CGFloat?
public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) {
self.backgroundColor = backgroundColor
self.strokeColor = strokeColor
self.borderColor = borderColor
self.overlayBorder = overlayBorder
self.hasInset = hasInset
self.hasShadow = hasShadow
self.filledBorder = filledBorder
self.borderWidth = borderWidth
}
var checkNodeTheme: CheckNodeTheme {
return CheckNodeTheme(
backgroundColor: self.backgroundColor,
strokeColor: self.strokeColor,
borderColor: self.borderColor,
overlayBorder: self.overlayBorder,
hasInset: self.hasInset,
hasShadow: self.hasShadow,
filledBorder: self.filledBorder,
borderWidth: self.borderWidth
)
}
}
let theme: Theme
let selected: Bool
init(
theme: Theme,
selected: Bool
) {
self.theme = theme
self.selected = selected
}
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.selected != rhs.selected {
return false
}
return true
}
final class View: UIView {
private var currentValue: CGFloat?
private var animator: DisplayLinkAnimator?
private var checkLayer: CheckLayer {
return self.layer as! CheckLayer
}
override class var layerClass: AnyClass {
return CheckLayer.self
}
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: CheckComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.checkLayer.setSelected(component.selected, animated: true)
self.checkLayer.theme = component.theme.checkNodeTheme
return CGSize(width: 22.0, height: 22.0)
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -907,8 +907,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
self.paidReactionsSection = paidReactionsSection
}
//TODO:localize
let parsedString = parseMarkdownIntoAttributedString("Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)", attributes: MarkdownAttributes(
let parsedString = parseMarkdownIntoAttributedString(environment.strings.PeerInfo_AllowedReactions_StarReactionsFooter, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
@ -925,7 +924,6 @@ final class PeerAllowedReactionsScreenComponent: Component {
paidReactionsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: paidReactionsFooterText.string))
}
//TODO:localize
let paidReactionsSectionSize = paidReactionsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
@ -953,7 +951,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListSwitchItemComponent(
theme: environment.theme,
title: "Enable Paid Reactions",
title: environment.strings.PeerInfo_AllowedReactions_StarReactions,
value: self.areStarsReactionsEnabled,
valueUpdated: { [weak self] value in
guard let self, let component = self.component else {

View File

@ -438,7 +438,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
let params = RippleParams(amplitude: 10.0, frequency: 15.0, decay: 8.0, speed: 1400.0)
let params = RippleParams(amplitude: 10.0, frequency: 15.0, decay: 5.5, speed: 1400.0)
if let currentCloneView = self.currentCloneView {
currentCloneView.removeFromSuperview()

View File

@ -1655,7 +1655,7 @@ private final class StoryContainerScreenComponent: Component {
}
if case let .user(user) = slice.peer, user.botInfo != nil {
//TODO:localize
//TODO:release
let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone()
} else {
let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone()

View File

@ -387,7 +387,7 @@ extension ChatControllerImpl {
}
}
self.context.engine.messages.sendStarsReaction(id: message.id, count: 1)
self.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: false)
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
} else {
let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction

View File

@ -1729,7 +1729,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1)
strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: false)
strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1)
})
} else {
@ -5092,7 +5092,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if peer.id.isReplies {
imageOverride = .repliesIcon
} else if peer.id.isAnonymousSavedMessages {
imageOverride = .anonymousSavedMessagesIcon
imageOverride = .anonymousSavedMessagesIcon(isColored: true)
} else if peer.isDeleted {
imageOverride = .deletedIcon
} else {
@ -5888,7 +5888,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if savedMessagesPeerId.isReplies {
imageOverride = .repliesIcon
} else if savedMessagesPeerId.isAnonymousSavedMessages {
imageOverride = .anonymousSavedMessagesIcon
imageOverride = .anonymousSavedMessagesIcon(isColored: true)
} else if let peer = savedMessagesPeer?.peer, peer.isDeleted {
imageOverride = .deletedIcon
} else {

View File

@ -369,16 +369,14 @@ extension ChatControllerImpl {
}
func openMessageSendStarsScreen(message: Message) {
guard let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false) else {
return
}
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, topPeers: reactionsAttribute.topPeers)
let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false)
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, messageId: message.id, topPeers: reactionsAttribute?.topPeers ?? [])
|> deliverOnMainQueue).start(next: { [weak self] initialData in
guard let self, let initialData else {
return
}
HapticFeedback().tap()
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isBecomingTop, transitionOut in
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isAnonymous, isBecomingTop, transitionOut in
guard let self, amount > 0 else {
return
}
@ -463,10 +461,7 @@ extension ChatControllerImpl {
}
}
#if !DEBUG
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount))
#endif
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), isAnonymous: isAnonymous)
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount))
}))
})
@ -486,21 +481,14 @@ extension ChatControllerImpl {
self.currentSendStarsUndoCount = count
}
//TODO:localize
let title: String
if self.currentSendStarsUndoCount == 1 {
title = "Star sent!"
} else {
title = "Stars sent!"
}
let title: String = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount))
var textItems: [AnimatedTextComponent.Item] = []
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text("You have reacted with ")))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), content: .number(self.currentSendStarsUndoCount, minDigits: 1)))
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(2), isUnbreakable: true, content: .text(self.currentSendStarsUndoCount == 1 ? " star." : " stars.")))
let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
0: .number(self.currentSendStarsUndoCount, minDigits: 1),
1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount)))
])
self.currentSendStarsUndoMessageId = messageId
//TODO:localize
if let current = self.currentSendStarsUndoController {
current.content = .starsSent(context: self.context, title: title, text: textItems)
} else {
@ -518,3 +506,31 @@ extension ChatControllerImpl {
}
}
}
private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] {
var textItems: [AnimatedTextComponent.Item] = []
var previousIndex = 0
let nsString = string.string as NSString
for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) {
if range.range.lowerBound > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex)))))
}
if let value = mapping[range.index] {
let isUnbreakable: Bool
switch value {
case .text:
isUnbreakable = true
case .number:
isUnbreakable = false
}
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value))
}
previousIndex = range.range.upperBound
}
if nsString.length > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex)))))
}
return textItems
}

View File

@ -223,7 +223,7 @@ private enum PollResultsEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .solutionText(text, entities):
let _ = entities
//TODO:localize
//TODO:release
return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
case let .optionPeer(optionId, _, peer, optionText, optionTextEntities, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption):
let font = Font.regular(13.0)