diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index abd952c3f9..b7908c3952 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -934,21 +934,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if isStream { messageLifetime = Int32.max - - if self.isStream { - var allowLiveChat = false - if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data { - if let dev = data["dev"] as? Double, dev != 0.0 { - allowLiveChat = true - } - if data["ios_can_join_streams"] != nil { - allowLiveChat = true - } - } - if !allowLiveChat { - preconditionFailure() - } - } } self.messagesContext = accountContext.engine.messages.groupCallMessages( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 2d21de2fd2..1edcb22928 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -520,23 +520,27 @@ public enum Stories { case timestamp case expirationTimestamp case isCloseFriends = "clf" + case isLiveItem = "liv" } public let id: Int32 public let timestamp: Int32 public let expirationTimestamp: Int32 public let isCloseFriends: Bool + public let isLiveItem: Bool public init( id: Int32, timestamp: Int32, expirationTimestamp: Int32, - isCloseFriends: Bool + isCloseFriends: Bool, + isLiveItem: Bool ) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.isCloseFriends = isCloseFriends + self.isLiveItem = isLiveItem } public init(from decoder: Decoder) throws { @@ -546,6 +550,7 @@ public enum Stories { self.timestamp = try container.decode(Int32.self, forKey: .timestamp) self.expirationTimestamp = try container.decode(Int32.self, forKey: .expirationTimestamp) self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false + self.isLiveItem = try container.decodeIfPresent(Bool.self, forKey: .isLiveItem) ?? false } public func encode(to encoder: Encoder) throws { @@ -555,6 +560,7 @@ public enum Stories { try container.encode(self.timestamp, forKey: .timestamp) try container.encode(self.expirationTimestamp, forKey: .expirationTimestamp) try container.encode(self.isCloseFriends, forKey: .isCloseFriends) + try container.encode(self.isLiveItem, forKey: .isLiveItem) } public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool { @@ -570,6 +576,9 @@ public enum Stories { if lhs.isCloseFriends != rhs.isCloseFriends { return false } + if lhs.isLiveItem != rhs.isLiveItem { + return false + } return true } } @@ -628,8 +637,8 @@ public enum Stories { switch self { case let .item(item): return item.media is TelegramMediaLiveStream - case .placeholder: - return false + case let .placeholder(placeholder): + return placeholder.isLiveItem } } @@ -2318,7 +2327,8 @@ extension Stories.StoredItem { } case let .storyItemSkipped(flags, id, date, expireDate): let isCloseFriends = (flags & (1 << 8)) != 0 - self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends)) + let isLiveItem = (flags & (1 << 9)) != 0 + self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends, isLiveItem: isLiveItem)) case .storyItemDeleted: return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 2aef0a98f6..46661824be 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -2803,7 +2803,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, myPeer: EnginePeer? = nil, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -2875,7 +2875,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { defaultPrivacyPeer ) |> map { peerAndTopPeerMap, balance, channelsForPublicReaction, defaultPrivacyPeer -> InitialData? in - let (peer, myPeer, topPeerMap) = peerAndTopPeerMap + let (peer, myPeerValue, topPeerMap) = peerAndTopPeerMap + let myPeer = myPeer ?? myPeerValue guard let peer, let myPeer else { return nil } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 5ce7a5256f..8d049bc116 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -112,6 +112,7 @@ swift_library( "//submodules/TelegramUI/Components/AdminUserActionsSheet", "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Utils/LokiRng", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift index 97adc3d522..26168cf9bc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -7,14 +7,97 @@ import AvatarNode import AppBundle import AccountContext import HierarchyTrackingLayer +import LokiRng + +private let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], +] + +private func avatarViewLettersImage(size: CGSize, peerId: EnginePeer.Id, letters: [String], isStory: Bool) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 2.0) + let context = UIGraphicsGetCurrentContext() + + context?.beginPath() + if isStory { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height).insetBy(dx: 4.0, dy: 4.0)) + } else { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + } + context?.clip() + + let colorIndex: Int + if peerId.namespace == .max { + colorIndex = 0 + } else { + colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value())) + } + + let colorsArray = gradientColors[colorIndex % gradientColors.count] + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context?.setBlendMode(.normal) + + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 8.0), NSAttributedString.Key.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) + let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + + context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context?.scaleBy(x: 1.0, y: -1.0) + context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) + if let context = context { + CTLineDraw(line, context) + } + context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + + if isStory { + context?.resetClip() + + let lineWidth: CGFloat = 2.0 + context?.setLineWidth(lineWidth) + context?.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height)).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context?.replacePathWithStrokedPath() + context?.clip() + + let colors: [CGColor] = [ + UIColor(rgb: 0x34C76F).cgColor, + UIColor(rgb: 0x3DA1FD).cgColor + ] + var locations: [CGFloat] = [0.0, 1.0] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: Int) async -> UIImage { let avatarSize: CGFloat = 16.0 let avatarInset: CGFloat = 2.0 let avatarIconSpacing: CGFloat = 2.0 - let iconTextSpacing: CGFloat = 2.0 - let iconSize: CGFloat = 8.0 - let rightInset: CGFloat = 4.0 + let iconTextSpacing: CGFloat = 1.0 + let iconSize: CGFloat = 10.0 + let rightInset: CGFloat = 2.0 let text = NSAttributedString(string: "\(count)", font: Font.semibold(10.0), textColor: .white) var textSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size @@ -33,6 +116,15 @@ private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) context.fillPath() + if let image = avatarViewLettersImage(size: CGSize(width: avatarSize, height: avatarSize), peerId: peer.id, letters: peer.displayLetters, isStory: false) { + image.draw(in: CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: CGSize(width: avatarSize, height: avatarSize))) + } + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white) { + let iconFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing, y: floorToScreenPixels((size.height - iconSize) * 0.5) + 1.0), size: CGSize(width: iconSize, height: iconSize)) + image.draw(in: iconFrame) + } + text.draw(at: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5))) })! } @@ -81,6 +173,7 @@ final class LiveChatReactionStreamView: UIView { super.init() self.contents = image.cgImage + self.allowsEdgeAntialiasing = true } override init(layer: Any) { @@ -157,20 +250,16 @@ final class LiveChatReactionStreamView: UIView { } private func addRenderedItem(image: UIImage) { - if "".isEmpty { - return - } - let id = self.nextId self.nextId += 1 let itemLayer = ItemLayer(image: image) - itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 10.0, y: -image.size.height * 0.5), size: image.size) + itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 30.0, y: -image.size.height * 0.5), size: image.size).offsetBy(dx: 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0)) - 0.5, dy: 0.0) + itemLayer.transform = CATransform3DMakeRotation(CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 1, seed2: 0) - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) self.itemLayers[id] = itemLayer self.itemLayerContainer.addSublayer(itemLayer) - let transition = ComponentTransition(animation: .curve(duration: 2.0, curve: .linear)) - transition.setPosition(layer: itemLayer, position: CGPoint(x: itemLayer.position.x, y: -300.0)) + //itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, removeOnCompletion: false, completion: { [weak self] _ in guard let self else { @@ -188,7 +277,59 @@ final class LiveChatReactionStreamView: UIView { let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) self.previousPhysicsTimestamp = timestamp - let _ = dt + let cellSize: CGFloat = 16.0 + let forceScale: CGFloat = 60.0 + let falloffDistance: CGFloat = 24.0 + + for (id, itemLayer) in self.itemLayers { + let px = itemLayer.position.x + let py = itemLayer.position.y + + // Grid coordinates (no abs; keep sign, use floor) + let gx = Int(floor(px / cellSize)) + let gy = Int(floor(py / cellSize)) + + // Fractional position within the cell + let fx = (px / cellSize) - CGFloat(gx) + let fy = (py / cellSize) - CGFloat(gy) + + // Bilinear weights for the 4 corners + let w00 = (1 - fx) * (1 - fy) + let w10 = (fx) * (1 - fy) + let w01 = (1 - fx) * (fy) + let w11 = (fx) * (fy) + + func n(_ ix: Int, _ iy: Int) -> CGFloat { + // random in [0,1), shift to [-0.5, 0.5) + let r = LokiRng.random( + withSeed0: UInt(abs(ix)), + seed1: UInt(abs(iy)), + seed2: UInt(id) + ) + return CGFloat(r) - 0.5 + } + + let n00x = n(gx + 0, gy + 0) + let n10x = n(gx + 1, gy + 0) + let n01x = n(gx + 0, gy + 1) + let n11x = n(gx + 1, gy + 1) + + // Bilinear interpolation (smooth, limited cancellation) + var fxForce = w00*n00x + w10*n10x + w01*n01x + w11*n11x + + // Optional local radial falloff from the nearest lattice center + // (invert the original: strongest at center) + let cx = (CGFloat(gx) + 0.5) * cellSize + let cy = (CGFloat(gy) + 0.5) * cellSize + let d = hypot(px - cx, py - cy) + let t = max(0.0, 1.0 - d / falloffDistance) + let weight = t * t + fxForce *= weight + + // Apply force directly to position (or integrate velocity if you have it) + itemLayer.position.x += fxForce * forceScale * dt + itemLayer.position.y -= dt * 100.0 + } } func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index f903b6fd96..719bad7ac6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -4008,6 +4008,7 @@ final class StoryItemSetContainerSendMessage { let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, + myPeer: (self.currentSendAsPeer?.peer).flatMap(EnginePeer.init), reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount)), topPeers: topPeers, completion: { [weak self, weak view] amount, _, _, _ in diff --git a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h index e1c0abb7c3..73b623d0f9 100644 --- a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h +++ b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h @@ -8,6 +8,7 @@ - (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; - (float)next; ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; @end diff --git a/submodules/Utils/LokiRng/Sources/LokiRng.mm b/submodules/Utils/LokiRng/Sources/LokiRng.mm index 3f8240d361..957af6d795 100644 --- a/submodules/Utils/LokiRng/Sources/LokiRng.mm +++ b/submodules/Utils/LokiRng/Sources/LokiRng.mm @@ -61,4 +61,34 @@ static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, c return old_seed; } ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 { + uint32_t seed = ((uint32_t)seed0) * 1099087573U; + uint32_t seedb = ((uint32_t)seed1) * 1099087573U; + uint32_t seedc = ((uint32_t)seed2) * 1099087573U; + + // Round 1: Randomise seed + uint32_t z1 = tausStep(seed,13,19,12,429496729U); + uint32_t z2 = tausStep(seed,2,25,4,4294967288U); + uint32_t z3 = tausStep(seed,3,11,17,429496280U); + uint32_t z4 = (1664525*seed + 1013904223U); + + // Round 2: Randomise seed again using second seed + uint32_t r1 = (z1^z2^z3^z4^seedb); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + // Round 3: Randomise seed again using third seed + r1 = (z1^z2^z3^z4^seedc); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + return (z1^z2^z3^z4) * 2.3283064365387e-10f; +} + @end