This commit is contained in:
Isaac 2025-10-28 16:56:08 +04:00
parent aa59524a09
commit 70702914b4
8 changed files with 202 additions and 32 deletions

View file

@ -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(

View file

@ -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
}

View file

@ -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<InitialData?, NoError> {
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<InitialData?, NoError> {
let balance: Signal<StarsAmount?, NoError>
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
}

View file

@ -112,6 +112,7 @@ swift_library(
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/Utils/LokiRng",
],
visibility = [
"//visibility:public",

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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