mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Stories
This commit is contained in:
parent
aa59524a09
commit
70702914b4
8 changed files with 202 additions and 32 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/Utils/LokiRng",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue