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

This commit is contained in:
Ilya Laktyushin 2025-10-24 23:06:35 +04:00
commit bc1598bb92
27 changed files with 2117 additions and 351 deletions

6
MODULE.bazel.lock generated
View File

@ -11,7 +11,6 @@
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
@ -24,6 +23,7 @@
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
@ -144,8 +144,8 @@
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",

View File

@ -1306,6 +1306,10 @@ public struct ComponentTransition {
}
public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
if case .none = self.animation {
return
}
if let blurFilter = CALayer.blur() {
blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius")
layer.filters = [blurFilter]

View File

@ -303,6 +303,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }
dict[-674602536] = { return Api.GroupCall.parse_groupCall($0) }
dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) }
dict[-297595771] = { return Api.GroupCallDonor.parse_groupCallDonor($0) }
dict[445316222] = { return Api.GroupCallMessage.parse_groupCallMessage($0) }
dict[708691884] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
dict[1735736008] = { return Api.GroupCallParticipantVideo.parse_groupCallParticipantVideo($0) }
@ -1485,6 +1486,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) }
dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) }
dict[-1636664659] = { return Api.phone.GroupCall.parse_groupCall($0) }
dict[-1658995418] = { return Api.phone.GroupCallStars.parse_groupCallStars($0) }
dict[-790330702] = { return Api.phone.GroupCallStreamChannels.parse_groupCallStreamChannels($0) }
dict[767505458] = { return Api.phone.GroupCallStreamRtmpUrl.parse_groupCallStreamRtmpUrl($0) }
dict[-193506890] = { return Api.phone.GroupParticipants.parse_groupParticipants($0) }
@ -1820,6 +1822,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCall:
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCallDonor:
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCallMessage:
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCallParticipant:
@ -2640,6 +2644,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCall:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStars:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStreamChannels:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStreamRtmpUrl:

View File

@ -1094,6 +1094,72 @@ public extension Api.phone {
}
}
public extension Api.phone {
enum GroupCallStars: TypeConstructorDescription {
case groupCallStars(totalStars: Int64, topDonors: [Api.GroupCallDonor], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallStars(let totalStars, let topDonors, let chats, let users):
if boxed {
buffer.appendInt32(-1658995418)
}
serializeInt64(totalStars, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topDonors.count))
for item in topDonors {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallStars(let totalStars, let topDonors, let chats, let users):
return ("groupCallStars", [("totalStars", totalStars as Any), ("topDonors", topDonors as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_groupCallStars(_ reader: BufferReader) -> GroupCallStars? {
var _1: Int64?
_1 = reader.readInt64()
var _2: [Api.GroupCallDonor]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallDonor.self)
}
var _3: [Api.Chat]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _4: [Api.User]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.phone.GroupCallStars.groupCallStars(totalStars: _1!, topDonors: _2!, chats: _3!, users: _4!)
}
else {
return nil
}
}
}
}
public extension Api.phone {
enum GroupCallStreamChannels: TypeConstructorDescription {
case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel])
@ -1650,65 +1716,3 @@ public extension Api.premium {
}
}
public extension Api.premium {
enum MyBoosts: TypeConstructorDescription {
case myBoosts(myBoosts: [Api.MyBoost], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .myBoosts(let myBoosts, let chats, let users):
if boxed {
buffer.appendInt32(-1696454430)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(myBoosts.count))
for item in myBoosts {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .myBoosts(let myBoosts, let chats, let users):
return ("myBoosts", [("myBoosts", myBoosts as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_myBoosts(_ reader: BufferReader) -> MyBoosts? {
var _1: [Api.MyBoost]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MyBoost.self)
}
var _2: [Api.Chat]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _3: [Api.User]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.premium.MyBoosts.myBoosts(myBoosts: _1!, chats: _2!, users: _3!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,65 @@
public extension Api.premium {
enum MyBoosts: TypeConstructorDescription {
case myBoosts(myBoosts: [Api.MyBoost], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .myBoosts(let myBoosts, let chats, let users):
if boxed {
buffer.appendInt32(-1696454430)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(myBoosts.count))
for item in myBoosts {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .myBoosts(let myBoosts, let chats, let users):
return ("myBoosts", [("myBoosts", myBoosts as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_myBoosts(_ reader: BufferReader) -> MyBoosts? {
var _1: [Api.MyBoost]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MyBoost.self)
}
var _2: [Api.Chat]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _3: [Api.User]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.premium.MyBoosts.myBoosts(myBoosts: _1!, chats: _2!, users: _3!)
}
else {
return nil
}
}
}
}
public extension Api.smsjobs {
enum EligibilityToJoin: TypeConstructorDescription {
case eligibleToJoin(termsUrl: String, monthlySentSms: Int32)

View File

@ -10593,6 +10593,21 @@ public extension Api.functions.phone {
})
}
}
public extension Api.functions.phone {
static func getGroupCallStars(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.GroupCallStars>) {
let buffer = Buffer()
buffer.appendInt32(1868784386)
call.serialize(buffer, true)
return (FunctionDescription(name: "phone.getGroupCallStars", parameters: [("call", String(describing: call))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.phone.GroupCallStars? in
let reader = BufferReader(buffer)
var result: Api.phone.GroupCallStars?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.phone.GroupCallStars
}
return result
})
}
}
public extension Api.functions.phone {
static func getGroupCallStreamChannels(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.GroupCallStreamChannels>) {
let buffer = Buffer()

View File

@ -1332,6 +1332,52 @@ public extension Api {
}
}
public extension Api {
enum GroupCallDonor: TypeConstructorDescription {
case groupCallDonor(flags: Int32, peerId: Api.Peer?, stars: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallDonor(let flags, let peerId, let stars):
if boxed {
buffer.appendInt32(-297595771)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {peerId!.serialize(buffer, true)}
serializeInt64(stars, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallDonor(let flags, let peerId, let stars):
return ("groupCallDonor", [("flags", flags as Any), ("peerId", peerId as Any), ("stars", stars as Any)])
}
}
public static func parse_groupCallDonor(_ reader: BufferReader) -> GroupCallDonor? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _3: Int64?
_3 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.GroupCallDonor.groupCallDonor(flags: _1!, peerId: _2, stars: _3!)
}
else {
return nil
}
}
}
}
public extension Api {
enum GroupCallMessage: TypeConstructorDescription {
case groupCallMessage(flags: Int32, id: Int32, fromId: Api.Peer, date: Int32, message: Api.TextWithEntities, paidMessageStars: Int64?)
@ -1392,85 +1438,3 @@ public extension Api {
}
}
public extension Api {
enum GroupCallParticipant: TypeConstructorDescription {
case groupCallParticipant(flags: Int32, peer: Api.Peer, date: Int32, activeDate: Int32?, source: Int32, volume: Int32?, about: String?, raiseHandRating: Int64?, video: Api.GroupCallParticipantVideo?, presentation: Api.GroupCallParticipantVideo?, paidStarsTotal: Int64?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation, let paidStarsTotal):
if boxed {
buffer.appendInt32(708691884)
}
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
serializeInt32(date, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(activeDate!, buffer: buffer, boxed: false)}
serializeInt32(source, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 7) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 11) != 0 {serializeString(about!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {serializeInt64(raiseHandRating!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 6) != 0 {video!.serialize(buffer, true)}
if Int(flags) & Int(1 << 14) != 0 {presentation!.serialize(buffer, true)}
if Int(flags) & Int(1 << 16) != 0 {serializeInt64(paidStarsTotal!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation, let paidStarsTotal):
return ("groupCallParticipant", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("activeDate", activeDate as Any), ("source", source as Any), ("volume", volume as Any), ("about", about as Any), ("raiseHandRating", raiseHandRating as Any), ("video", video as Any), ("presentation", presentation as Any), ("paidStarsTotal", paidStarsTotal as Any)])
}
}
public static func parse_groupCallParticipant(_ reader: BufferReader) -> GroupCallParticipant? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() }
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
if Int(_1!) & Int(1 << 7) != 0 {_6 = reader.readInt32() }
var _7: String?
if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) }
var _8: Int64?
if Int(_1!) & Int(1 << 13) != 0 {_8 = reader.readInt64() }
var _9: Api.GroupCallParticipantVideo?
if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo
} }
var _10: Api.GroupCallParticipantVideo?
if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo
} }
var _11: Int64?
if Int(_1!) & Int(1 << 16) != 0 {_11 = reader.readInt64() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 16) == 0) || _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, peer: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, about: _7, raiseHandRating: _8, video: _9, presentation: _10, paidStarsTotal: _11)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,85 @@
public extension Api {
enum GroupCallParticipant: TypeConstructorDescription {
case groupCallParticipant(flags: Int32, peer: Api.Peer, date: Int32, activeDate: Int32?, source: Int32, volume: Int32?, about: String?, raiseHandRating: Int64?, video: Api.GroupCallParticipantVideo?, presentation: Api.GroupCallParticipantVideo?, paidStarsTotal: Int64?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation, let paidStarsTotal):
if boxed {
buffer.appendInt32(708691884)
}
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
serializeInt32(date, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(activeDate!, buffer: buffer, boxed: false)}
serializeInt32(source, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 7) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 11) != 0 {serializeString(about!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {serializeInt64(raiseHandRating!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 6) != 0 {video!.serialize(buffer, true)}
if Int(flags) & Int(1 << 14) != 0 {presentation!.serialize(buffer, true)}
if Int(flags) & Int(1 << 16) != 0 {serializeInt64(paidStarsTotal!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation, let paidStarsTotal):
return ("groupCallParticipant", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("activeDate", activeDate as Any), ("source", source as Any), ("volume", volume as Any), ("about", about as Any), ("raiseHandRating", raiseHandRating as Any), ("video", video as Any), ("presentation", presentation as Any), ("paidStarsTotal", paidStarsTotal as Any)])
}
}
public static func parse_groupCallParticipant(_ reader: BufferReader) -> GroupCallParticipant? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() }
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
if Int(_1!) & Int(1 << 7) != 0 {_6 = reader.readInt32() }
var _7: String?
if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) }
var _8: Int64?
if Int(_1!) & Int(1 << 13) != 0 {_8 = reader.readInt64() }
var _9: Api.GroupCallParticipantVideo?
if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo
} }
var _10: Api.GroupCallParticipantVideo?
if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo
} }
var _11: Int64?
if Int(_1!) & Int(1 << 16) != 0 {_11 = reader.readInt64() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 16) == 0) || _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, peer: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, about: _7, raiseHandRating: _8, video: _9, presentation: _10, paidStarsTotal: _11)
}
else {
return nil
}
}
}
}
public extension Api {
enum GroupCallParticipantVideo: TypeConstructorDescription {
case groupCallParticipantVideo(flags: Int32, endpoint: String, sourceGroups: [Api.GroupCallParticipantVideoSourceGroup], audioSource: Int32?)
@ -1190,59 +1272,3 @@ public extension Api {
}
}
public extension Api {
enum InputBusinessBotRecipients: TypeConstructorDescription {
case inputBusinessBotRecipients(flags: Int32, users: [Api.InputUser]?, excludeUsers: [Api.InputUser]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputBusinessBotRecipients(let flags, let users, let excludeUsers):
if boxed {
buffer.appendInt32(-991587810)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users!.count))
for item in users! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(excludeUsers!.count))
for item in excludeUsers! {
item.serialize(buffer, true)
}}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputBusinessBotRecipients(let flags, let users, let excludeUsers):
return ("inputBusinessBotRecipients", [("flags", flags as Any), ("users", users as Any), ("excludeUsers", excludeUsers as Any)])
}
}
public static func parse_inputBusinessBotRecipients(_ reader: BufferReader) -> InputBusinessBotRecipients? {
var _1: Int32?
_1 = reader.readInt32()
var _2: [Api.InputUser]?
if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
} }
var _3: [Api.InputUser]?
if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
} }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 6) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.InputBusinessBotRecipients.inputBusinessBotRecipients(flags: _1!, users: _2, excludeUsers: _3)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,59 @@
public extension Api {
enum InputBusinessBotRecipients: TypeConstructorDescription {
case inputBusinessBotRecipients(flags: Int32, users: [Api.InputUser]?, excludeUsers: [Api.InputUser]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputBusinessBotRecipients(let flags, let users, let excludeUsers):
if boxed {
buffer.appendInt32(-991587810)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users!.count))
for item in users! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(excludeUsers!.count))
for item in excludeUsers! {
item.serialize(buffer, true)
}}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputBusinessBotRecipients(let flags, let users, let excludeUsers):
return ("inputBusinessBotRecipients", [("flags", flags as Any), ("users", users as Any), ("excludeUsers", excludeUsers as Any)])
}
}
public static func parse_inputBusinessBotRecipients(_ reader: BufferReader) -> InputBusinessBotRecipients? {
var _1: Int32?
_1 = reader.readInt32()
var _2: [Api.InputUser]?
if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
} }
var _3: [Api.InputUser]?
if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
} }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 6) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.InputBusinessBotRecipients.inputBusinessBotRecipients(flags: _1!, users: _2, excludeUsers: _3)
}
else {
return nil
}
}
}
}
public extension Api {
enum InputBusinessChatLink: TypeConstructorDescription {
case inputBusinessChatLink(flags: Int32, message: String, entities: [Api.MessageEntity]?, title: String?)

View File

@ -832,7 +832,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
}
private let messagesStatePromise = Promise<GroupCallMessagesContext.State>(GroupCallMessagesContext.State(messages: [], pinnedMessages: []))
private let messagesStatePromise = Promise<GroupCallMessagesContext.State>(GroupCallMessagesContext.State(messages: [], pinnedMessages: [], topStars: [], totalStars: 0, pendingMyStars: 0))
public var messagesState: Signal<GroupCallMessagesContext.State, NoError> {
return self.messagesStatePromise.get()
}
@ -4061,9 +4061,27 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
public func deleteMessage(id: GroupCallMessagesContext.Message.Id) {
public func sendStars(amount: Int64, delay: Bool) {
if let messagesContext = self.messagesContext {
messagesContext.deleteMessage(id: id)
messagesContext.sendStars(fromId: self.joinAsPeerId, amount: amount, delay: delay)
}
}
public func cancelSendStars() {
if let messagesContext = self.messagesContext {
messagesContext.cancelSendStars()
}
}
public func commitSendStars() {
if let messagesContext = self.messagesContext {
messagesContext.commitSendStars()
}
}
public func deleteMessage(id: GroupCallMessagesContext.Message.Id, reportSpam: Bool) {
if let messagesContext = self.messagesContext {
messagesContext.deleteMessage(id: id, reportSpam: reportSpam)
}
}
}

View File

@ -3761,13 +3761,54 @@ public final class GroupCallMessagesContext {
}
}
public final class TopStarsItem: Equatable {
public let peerId: EnginePeer.Id?
public let amount: Int64
public let isTop: Bool
public let isMy: Bool
public let isAnonymous: Bool
public init(peerId: EnginePeer.Id?, amount: Int64, isTop: Bool, isMy: Bool, isAnonymous: Bool) {
self.peerId = peerId
self.amount = amount
self.isTop = isTop
self.isMy = isMy
self.isAnonymous = isAnonymous
}
public static func ==(lhs: TopStarsItem, rhs: TopStarsItem) -> Bool {
if lhs.peerId != rhs.peerId {
return false
}
if lhs.amount != rhs.amount {
return false
}
if lhs.isTop != rhs.isTop {
return false
}
if lhs.isMy != rhs.isMy {
return false
}
if lhs.isAnonymous != rhs.isAnonymous {
return false
}
return true
}
}
public struct State: Equatable {
public var messages: [Message]
public var pinnedMessages: [Message]
public var topStars: [TopStarsItem]
public var totalStars: Int64
public var pendingMyStars: Int64
public init(messages: [Message], pinnedMessages: [Message]) {
public init(messages: [Message], pinnedMessages: [Message], topStars: [TopStarsItem], totalStars: Int64, pendingMyStars: Int64) {
self.messages = messages
self.pinnedMessages = pinnedMessages
self.topStars = topStars
self.totalStars = totalStars
self.pendingMyStars = pendingMyStars
}
}
@ -3789,12 +3830,19 @@ public final class GroupCallMessagesContext {
let stateValue = ValuePromise<State>()
var updatesDisposable: Disposable?
var didInitializeTopStars: Bool = false
var pollTopStarsDisposable: Disposable?
let sendMessageDisposables = DisposableSet()
var processedIds = Set<Int64>()
private var messageLifeTimer: SwiftSignalKit.Timer?
private var pendingSendStars: (fromId: PeerId, messageId: Int64, amount: Int64)?
private var pendingSendStarsTimer: SwiftSignalKit.Timer?
init(queue: Queue, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) {
self.queue = queue
self.account = account
@ -3804,9 +3852,10 @@ public final class GroupCallMessagesContext {
self.messageLifetime = messageLifetime
self.isLiveStream = isLiveStream
self.state = State(messages: [], pinnedMessages: [])
self.state = State(messages: [], pinnedMessages: [], topStars: [], totalStars: 0, pendingMyStars: 0)
self.stateValue.set(self.state)
let accountPeerId = account.peerId
self.updatesDisposable = (account.stateManager.groupCallMessageUpdates
|> deliverOn(self.queue)).startStrict(next: { [weak self] updates in
guard let self else {
@ -3913,13 +3962,20 @@ public final class GroupCallMessagesContext {
}
existingIds.insert(message.id)
state.messages.append(message)
if self.isLiveStream && message.paidStars != nil {
if self.isLiveStream, let paidStars = message.paidStars {
if message.date + message.lifetime >= currentTime {
state.pinnedMessages.append(message)
}
if let author = message.author {
if self.didInitializeTopStars {
Impl.addStateStars(state: &state, peerId: author.id, isMy: author.id == accountPeerId, amount: paidStars)
}
}
}
}
self.state = state
self.didInitializeTopStars = true
})
}
})
@ -3929,12 +3985,76 @@ public final class GroupCallMessagesContext {
}, queue: self.queue)
self.messageLifeTimer = timer
timer.start()
self.pollTopStars()
}
deinit {
self.updatesDisposable?.dispose()
self.sendMessageDisposables.dispose()
self.messageLifeTimer?.invalidate()
self.pollTopStarsDisposable?.dispose()
self.pendingSendStarsTimer?.invalidate()
}
private func pollTopStars() {
let accountPeerId = self.account.peerId
let postbox = self.account.postbox
self.pollTopStarsDisposable?.dispose()
self.pollTopStarsDisposable = ((self.account.network.request(Api.functions.phone.getGroupCallStars(call: self.reference.apiInputGroupCall))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.phone.GroupCallStars?, NoError> in
return .single(nil)
}
|> then(Signal<Api.phone.GroupCallStars?, NoError>.complete() |> delay(30.0, queue: self.queue))) |> restart
|> mapToSignal { result -> Signal<(Api.phone.GroupCallStars, [PeerId: Peer])?, NoError> in
guard let result else {
return .single(nil)
}
return postbox.transaction { transaction -> (Api.phone.GroupCallStars, [PeerId: Peer])? in
var peers: [PeerId: Peer] = [:]
switch result {
case let .groupCallStars(_, topDonors, chats, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(chats: chats, users: users))
for topDonor in topDonors {
switch topDonor {
case let .groupCallDonor(_, peerId, _):
if let peerId {
if peers[peerId.peerId] == nil, let peer = transaction.getPeer(peerId.peerId) {
peers[peer.id] = peer
}
}
}
}
}
return (result, peers)
}
}
|> deliverOn(self.queue)).startStrict(next: { [weak self] result in
guard let self else {
return
}
if let (result, _) = result {
switch result {
case let .groupCallStars(totalStars, topDonors, _, _):
var state = self.state
state.topStars = topDonors.map { topDonor in
switch topDonor {
case let .groupCallDonor(flags, peerId, stars):
return TopStarsItem(
peerId: peerId?.peerId,
amount: stars,
isTop: (flags & (1 << 0)) != 0,
isMy: (flags & (1 << 1)) != 0 || peerId?.peerId == accountPeerId,
isAnonymous: (flags & (1 << 2)) != 0
)
}
}
state.totalStars = totalStars
self.state = state
}
}
})
}
private func messageLifetimeTick() {
@ -3967,6 +4087,96 @@ public final class GroupCallMessagesContext {
}
}
static func addStateStars(state: inout State, peerId: EnginePeer.Id, isMy: Bool, amount: Int64) {
state.totalStars += amount
var totalMyAmount: Int64 = amount
if isMy {
if let index = state.topStars.firstIndex(where: { $0.isMy }) {
totalMyAmount += state.topStars[index].amount
state.topStars[index] = TopStarsItem(
peerId: peerId,
amount: totalMyAmount,
isTop: false,
isMy: true,
isAnonymous: state.topStars[index].isAnonymous
)
} else {
state.topStars.append(TopStarsItem(
peerId: peerId,
amount: totalMyAmount,
isTop: false,
isMy: true,
isAnonymous: false
))
}
} else {
if let index = state.topStars.firstIndex(where: { $0.peerId == peerId }) {
totalMyAmount += state.topStars[index].amount
state.topStars[index] = TopStarsItem(
peerId: peerId,
amount: totalMyAmount,
isTop: false,
isMy: false,
isAnonymous: state.topStars[index].isAnonymous
)
} else {
state.topStars.append(TopStarsItem(
peerId: peerId,
amount: totalMyAmount,
isTop: false,
isMy: false,
isAnonymous: false
))
}
}
state.topStars.sort(by: { lhs, rhs in
if lhs.amount != rhs.amount {
return lhs.amount > rhs.amount
}
if let lhsPeer = lhs.peerId, let rhsPeer = rhs.peerId {
return lhsPeer < rhsPeer
}
if (lhs.peerId == nil) != (rhs.peerId == nil) {
return lhs.peerId != nil
}
return !lhs.isAnonymous
})
if let index = state.topStars.firstIndex(where: { item in
if isMy {
return item.isMy
} else {
return item.peerId == peerId
}
}) {
let item = state.topStars[index]
if index > 3 {
if isMy {
state.topStars[index] = TopStarsItem(
peerId: item.peerId,
amount: item.amount,
isTop: false,
isMy: item.isMy,
isAnonymous: item.isAnonymous
)
} else {
state.topStars.remove(at: index)
}
} else {
state.topStars[index] = TopStarsItem(
peerId: item.peerId,
amount: item.amount,
isTop: true,
isMy: item.isMy,
isAnonymous: item.isAnonymous
)
}
}
}
func send(fromId: EnginePeer.Id, randomId requestedRandomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId)
@ -4004,19 +4214,15 @@ public final class GroupCallMessagesContext {
)
state.messages.append(message)
if self.isLiveStream {
if paidStars != nil {
if let paidStars {
state.pinnedMessages.append(message)
if let fromPeer {
Impl.addStateStars(state: &state, peerId: fromPeer.id, isMy: true, amount: paidStars)
}
}
}
self.state = state
#if DEBUG
var paidStars = paidStars
if "".isEmpty {
paidStars = nil
}
#endif
self.processedIds.insert(randomId)
if let e2eContext = self.e2eContext, let messageData = serializeGroupCallMessage(randomId: randomId, text: text, entities: entities) {
@ -4044,7 +4250,7 @@ public final class GroupCallMessagesContext {
flags |= 1 << 0
}
self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage(
flags: 0,
flags: flags,
call: self.reference.apiInputGroupCall,
randomId: randomId,
message: .textWithEntities(
@ -4080,7 +4286,186 @@ public final class GroupCallMessagesContext {
})
}
func deleteMessage(id: Message.Id) {
func commitSendStars() {
guard let pendingSendStars = self.pendingSendStars else {
return
}
self.pendingSendStars = nil
if let _ = self.e2eContext {
return
}
if let pendingSendStarsTimer = self.pendingSendStarsTimer {
self.pendingSendStarsTimer = nil
pendingSendStarsTimer.invalidate()
}
var flags: Int32 = 0
flags |= 1 << 0
self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage(
flags: flags,
call: self.reference.apiInputGroupCall,
randomId: pendingSendStars.messageId,
message: .textWithEntities(
text: "",
entities: []
),
allowPaidStars: pendingSendStars.amount
)) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in
guard let self else {
return
}
self.account.stateManager.addUpdates(updates)
for update in updates.allUpdates {
if case let .updateMessageID(id, randomIdValue) = update {
if randomIdValue == pendingSendStars.messageId {
self.processedIds.insert(Int64(id))
var state = self.state
if let index = state.messages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStars.messageId) }) {
state.messages[index] = state.messages[index].withId(Message.Id(space: .remote, id: Int64(id)))
}
if let index = state.pinnedMessages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStars.messageId) }) {
state.pinnedMessages[index] = state.pinnedMessages[index].withId(Message.Id(space: .remote, id: Int64(id)))
}
Impl.addStateStars(state: &state, peerId: pendingSendStars.fromId, isMy: true, amount: pendingSendStars.amount)
state.pendingMyStars = 0
self.state = state
break
}
}
}
}, error: { _ in
}))
}
func cancelSendStars() {
if let pendingSendStarsTimer = self.pendingSendStarsTimer {
self.pendingSendStarsTimer = nil
pendingSendStarsTimer.invalidate()
}
if let pendingSendStars = self.pendingSendStars {
self.pendingSendStars = nil
var state = self.state
state.pendingMyStars = 0
if let index = state.messages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStars.messageId) }) {
state.messages.remove(at: index)
}
if let index = state.pinnedMessages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStars.messageId) }) {
state.pinnedMessages.remove(at: index)
}
self.state = state
}
}
func sendStars(fromId: EnginePeer.Id, amount: Int64, delay: Bool) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId)
}
|> deliverOn(self.queue)).startStandalone(next: { [weak self] fromPeer in
guard let self else {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let totalAmount: Int64
if let pendingSendStarsValue = self.pendingSendStars {
totalAmount = pendingSendStarsValue.amount + amount
self.pendingSendStars = (
fromId: fromId,
messageId: pendingSendStarsValue.messageId,
amount: totalAmount
)
} else {
totalAmount = amount
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
self.pendingSendStars = (
fromId: fromId,
messageId: randomId,
amount: amount
)
self.processedIds.insert(randomId)
}
let lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(value: totalAmount).period)
var state = self.state
if let pendingSendStarsValue = self.pendingSendStars {
if let index = state.messages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStarsValue.messageId) }) {
let message = state.messages[index]
state.messages.remove(at: index)
state.messages.append(Message(
id: message.id,
author: message.author,
text: message.text,
entities: message.entities,
date: currentTime,
lifetime: lifetime,
paidStars: totalAmount
))
} else {
state.messages.append(Message(
id: Message.Id(space: .local, id: pendingSendStarsValue.messageId),
author: fromPeer.flatMap(EnginePeer.init),
text: "",
entities: [],
date: currentTime,
lifetime: lifetime,
paidStars: totalAmount
))
}
if let index = state.pinnedMessages.firstIndex(where: { $0.id == Message.Id(space: .local, id: pendingSendStarsValue.messageId) }) {
let message = state.pinnedMessages[index]
state.pinnedMessages.remove(at: index)
state.pinnedMessages.append(Message(
id: message.id,
author: message.author,
text: message.text,
entities: message.entities,
date: currentTime,
lifetime: lifetime,
paidStars: totalAmount
))
} else {
state.pinnedMessages.append(Message(
id: Message.Id(space: .local, id: pendingSendStarsValue.messageId),
author: fromPeer.flatMap(EnginePeer.init),
text: "",
entities: [],
date: currentTime,
lifetime: lifetime,
paidStars: totalAmount
))
}
}
if delay {
state.pendingMyStars += amount
self.state = state
self.pendingSendStarsTimer?.invalidate()
self.pendingSendStarsTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: false, completion: { [weak self] in
guard let self else {
return
}
self.commitSendStars()
}, queue: self.queue)
self.pendingSendStarsTimer?.start()
} else {
self.state = state
self.commitSendStars()
}
})
}
func deleteMessage(id: Message.Id, reportSpam: Bool) {
var updatedState: State?
if let index = self.state.messages.firstIndex(where: { $0.id == id }) {
if updatedState == nil {
@ -4097,6 +4482,12 @@ public final class GroupCallMessagesContext {
if let updatedState {
self.state = updatedState
}
var flags: Int32 = 0
if reportSpam {
flags |= 1 << 0
}
let _ = self.account.network.request(Api.functions.phone.deleteGroupCallMessages(flags: flags, call: self.reference.apiInputGroupCall, messages: [Int32(clamping: id.id)])).startStandalone()
}
}
@ -4123,9 +4514,27 @@ public final class GroupCallMessagesContext {
}
}
public func deleteMessage(id: Message.Id) {
public func sendStars(fromId: EnginePeer.Id, amount: Int64, delay: Bool) {
self.impl.with { impl in
impl.deleteMessage(id: id)
impl.sendStars(fromId: fromId, amount: amount, delay: delay)
}
}
public func cancelSendStars() {
self.impl.with { impl in
impl.cancelSendStars()
}
}
public func commitSendStars() {
self.impl.with { impl in
impl.commitSendStars()
}
}
public func deleteMessage(id: Message.Id, reportSpam: Bool) {
self.impl.with { impl in
impl.deleteMessage(id: id, reportSpam: reportSpam)
}
}

View File

@ -427,6 +427,14 @@ private final class AdminUserActionsSheetComponent: Component {
)
}
private func calculateLiveStreamResult() -> AdminUserActionsSheet.LiveStreamResult {
return AdminUserActionsSheet.LiveStreamResult(
reportSpam: !self.optionReportSelectedPeers.isEmpty,
deleteAll: !self.optionDeleteAllSelectedPeers.isEmpty,
ban: !self.optionBanSelectedPeers.isEmpty
)
}
private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return
@ -578,7 +586,7 @@ private final class AdminUserActionsSheetComponent: Component {
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor
self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
@ -663,6 +671,9 @@ private final class AdminUserActionsSheetComponent: Component {
}
}
}
case .liveStream:
availableOptions.append(.deleteAll)
availableOptions.append(.ban)
}
let optionsItem: (OptionsSection) -> AnyComponentWithIdentity<Empty> = { section in
@ -912,6 +923,13 @@ private final class AdminUserActionsSheetComponent: Component {
titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount))
}
}
case let .liveStream(messageCount, deleteAllMessageCount, _):
titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount))
if let deleteAllMessageCount {
if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) {
titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount))
}
}
}
let titleSize = self.title.update(
@ -965,6 +983,7 @@ private final class AdminUserActionsSheetComponent: Component {
transition: optionsSectionTransition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader,
@ -1042,6 +1061,7 @@ private final class AdminUserActionsSheetComponent: Component {
}
if case let .channel(channel) = component.chatPeer, channel.isMonoForum {
} else if case .liveStream = component.mode {
} else {
var allConfigItems: [(ConfigItem, Bool)] = []
if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty {
@ -1362,9 +1382,11 @@ private final class AdminUserActionsSheetComponent: Component {
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 54.0 * 0.5
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
@ -1389,11 +1411,13 @@ private final class AdminUserActionsSheetComponent: Component {
completion(self.calculateMonoforumResult())
case let .chat(_, _, completion):
completion(self.calculateChatResult())
case let .liveStream(_, _, completion):
completion(self.calculateLiveStreamResult())
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 54.0)
)
let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
@ -1462,6 +1486,7 @@ private final class AdminUserActionsSheetComponent: Component {
public class AdminUserActionsSheet: ViewControllerComponentContainer {
public enum Mode {
case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void)
case liveStream(messageCount: Int, deleteAllMessageCount: Int?, completion: (LiveStreamResult) -> Void)
case monoforum(completion: (MonoforumResult) -> Void)
}
@ -1479,6 +1504,18 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer {
}
}
public final class LiveStreamResult {
public let reportSpam: Bool
public let deleteAll: Bool
public let ban: Bool
init(reportSpam: Bool, deleteAll: Bool, ban: Bool) {
self.reportSpam = reportSpam
self.deleteAll = deleteAll
self.ban = ban
}
}
public final class MonoforumResult {
public let ban: Bool
public let reportSpam: Bool
@ -1493,9 +1530,9 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer {
private var isDismissed: Bool = false
public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode) {
public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode, customTheme: PresentationTheme? = nil) {
self.context = context
super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), navigationBarAppearance: .none)
super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), navigationBarAppearance: .none, theme: customTheme.flatMap({ .custom($0) }) ?? .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal

View File

@ -27,6 +27,9 @@ swift_library(
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -18,8 +18,11 @@ import TelegramNotices
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
import GlassControls
import BundleIconComponent
import MultilineTextComponent
private enum SubscriberAction: Equatable {
private enum SubscriberAction: Equatable, Hashable {
case join
case joinGroup
case applyToJoin
@ -143,7 +146,10 @@ private func actionForPeer(context: AccountContext, peer: Peer, interfaceState:
private let badgeFont = Font.regular(14.0)
public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let buttonBackgroundView: GlassBackgroundView
private let panelContainer = UIView()
private let panel = ComponentView<Empty>()
/*private let buttonBackgroundView: GlassBackgroundView
private let button: HighlightableButton
private let buttonTitle: ImmediateTextNode
private let buttonTintTitle: ImmediateTextNode
@ -158,7 +164,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let suggestedPostButtonBackgroundView: GlassBackgroundView
private let suggestedPostButton: HighlightableButton
private let suggestedPostButtonIconView: UIImageView
private let suggestedPostButtonIconView: UIImageView*/
private var action: SubscriberAction?
@ -171,7 +177,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)?
public override init() {
self.button = HighlightableButton()
/*self.button = HighlightableButton()
self.buttonBackgroundView = GlassBackgroundView()
self.buttonBackgroundView.isUserInteractionEnabled = false
self.buttonTitle = ImmediateTextNode()
@ -203,18 +209,20 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView()
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView)
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton)
self.suggestedPostButtonBackgroundView.isHidden = true
self.suggestedPostButtonBackgroundView.isHidden = true*/
super.init()
self.view.addSubview(self.buttonBackgroundView)
/*self.view.addSubview(self.buttonBackgroundView)
self.view.addSubview(self.helpButtonBackgroundView)
self.view.addSubview(self.giftButtonBackgroundView)
self.view.addSubview(self.suggestedPostButtonBackgroundView)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside)
self.giftButton.addTarget(self, action: #selector(self.giftPressed), for: .touchUpInside)
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)*/
self.view.addSubview(self.panelContainer)
}
deinit {
@ -330,11 +338,17 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
}
#endif*/
if giftCount < 2 && !self.giftButton.isHidden {
let giftItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("gift"))
let suggestPostItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("suggestPost"))
if giftCount < 2, let giftItemView {
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, {
let absoluteFrame = self.giftButton.convert(self.giftButton.bounds, to: parentController.view)
Queue.mainQueue().after(0.4, { [weak giftItemView] in
guard let giftItemView else {
return
}
let absoluteFrame = giftItemView.convert(giftItemView.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -357,11 +371,14 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
} else if suggestCount < 2 && !self.suggestedPostButton.isHidden {
} else if suggestCount < 2, let suggestPostItemView {
let _ = ApplicationSpecificNotice.incrementChannelSuggestTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, {
let absoluteFrame = self.suggestedPostButton.convert(self.suggestedPostButton.bounds, to: parentController.view)
Queue.mainQueue().after(0.4, { [weak suggestPostItemView] in
guard let suggestPostItemView else {
return
}
let absoluteFrame = suggestPostItemView.convert(suggestPostItemView.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -394,7 +411,133 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let isFirstTime = self.layoutData == nil
self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics)
if self.presentationInterfaceState != interfaceState || force {
var transition = transition
if !isFirstTime && !transition.isAnimated {
transition = .animated(duration: 0.4, curve: .spring)
}
self.presentationInterfaceState = interfaceState
var centerAction: (title: String, isAccent: Bool)?
if let context = self.context, let peer = interfaceState.renderedPeer?.peer, let action = actionForPeer(context: context, peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) {
self.action = action
let (title, _) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings)
var isAccent = false
if case .join = self.action {
isAccent = true
}
centerAction = (title, isAccent)
}
var displayGift = false
var displaySuggestPost = false
var displayHelp = false
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel {
if case .broadcast = peer.info, interfaceState.starGiftsAvailable {
displayGift = true
}
if case let .broadcast(broadcastInfo) = peer.info, broadcastInfo.flags.contains(.hasMonoforum) {
displaySuggestPost = true
}
if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
displayHelp = true
}
}
var leftInset = leftInset + 8.0
var rightInset = rightInset + 8.0
if bottomInset <= 32.0 {
leftInset += 18.0
rightInset += 18.0
}
var leftPanelItems: [GlassControlGroupComponent.Item] = []
if displaySuggestPost {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "suggestPost",
content: .icon("Chat/Input/Accessory Panels/SuggestPost"),
action: { [weak self] in
self?.suggestedPostPressed()
}
))
}
if displayGift {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "gift",
content: .icon("Chat/Input/Accessory Panels/Gift"),
action: { [weak self] in
self?.giftPressed()
}
))
}
if displayHelp {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "help",
content: .icon("Chat/Input/Accessory Panels/Help"),
action: { [weak self] in
self?.helpPressed()
}
))
}
var centerPanelItem: GlassControlPanelComponent.Item?
if let centerAction {
centerPanelItem = GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: 0,
content: .text(centerAction.title),
action: { [weak self] in
self?.buttonPressed()
}
)],
background: centerAction.isAccent ? .activeTint : .panel
)
}
var rightPanelItems: [GlassControlGroupComponent.Item] = []
rightPanelItems.append(GlassControlGroupComponent.Item(
id: "search",
content: .icon("Chat List/SearchIcon"),
action: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.beginMessageSearch(.everything, "")
}
))
let panelHeight = defaultHeight(metrics: metrics)
let _ = isFirstTime
let panelFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight))
let _ = self.panel.update(
transition: ComponentTransition(transition),
component: AnyComponent(GlassControlPanelComponent(
theme: interfaceState.theme,
leftItem: leftPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: leftPanelItems,
background: .panel
),
centralItem: centerPanelItem,
rightItem: rightPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: rightPanelItems,
background: .panel
)
)),
environment: {},
containerSize: panelFrame.size
)
if let panelView = self.panel.view {
if panelView.superview == nil {
self.panelContainer.addSubview(panelView)
}
transition.updateFrame(view: self.panelContainer, frame: panelFrame)
transition.updateFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: panelFrame.size))
}
/*if self.presentationInterfaceState != interfaceState || force {
let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState
@ -504,7 +647,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
transition.updateFrame(view: self.suggestedPostButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size)))
}
transition.updateFrame(view: self.suggestedPostButton, frame: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size))
self.suggestedPostButtonBackgroundView.update(size: suggestedPostButtonFrame.size, cornerRadius: suggestedPostButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
self.suggestedPostButtonBackgroundView.update(size: suggestedPostButtonFrame.size, cornerRadius: suggestedPostButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))*/
return panelHeight
}

View File

@ -257,13 +257,16 @@ private final class BadgeComponent: Component {
private final class PeerBadgeComponent: Component {
let theme: PresentationTheme
let title: String
let color: UIColor
init(
theme: PresentationTheme,
title: String
title: String,
color: UIColor
) {
self.theme = theme
self.title = title
self.color = color
}
static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool {
@ -273,6 +276,9 @@ private final class PeerBadgeComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
@ -324,7 +330,7 @@ private final class PeerBadgeComponent: Component {
let size = CGSize(width: contentSize.width + sideInset * 2.0, height: contentSize.height + 3.0 * 2.0)
self.backgroundMaskLayer.backgroundColor = component.theme.overallDarkAppearance ? component.theme.list.blocksBackgroundColor.cgColor : component.theme.list.plainBackgroundColor.cgColor
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor
self.backgroundLayer.backgroundColor = component.color.cgColor
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.frame = backgroundFrame
@ -370,19 +376,22 @@ private final class PeerComponent: Component {
let strings: PresentationStrings
let peer: EnginePeer?
let count: String
let color: UIColor
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer?,
count: String
count: String,
color: UIColor
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.count = count
self.color = color
}
static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool {
@ -401,6 +410,9 @@ private final class PeerComponent: Component {
if lhs.count != rhs.count {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
@ -445,7 +457,8 @@ private final class PeerComponent: Component {
transition: .immediate,
component: AnyComponent(PeerBadgeComponent(
theme: component.theme,
title: component.count
title: component.count,
color: component.color
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
@ -2015,72 +2028,76 @@ private final class ChatSendStarsScreenComponent: Component {
if !reactData.topPeers.isEmpty {
contentHeight += 3.0
let topPeersLeftSeparator: SimpleLayer
if let current = self.topPeersLeftSeparator {
topPeersLeftSeparator = current
} else {
topPeersLeftSeparator = SimpleLayer()
self.topPeersLeftSeparator = topPeersLeftSeparator
self.scrollContentView.layer.addSublayer(topPeersLeftSeparator)
}
let topPeersRightSeparator: SimpleLayer
if let current = self.topPeersRightSeparator {
topPeersRightSeparator = current
} else {
topPeersRightSeparator = SimpleLayer()
self.topPeersRightSeparator = topPeersRightSeparator
self.scrollContentView.layer.addSublayer(topPeersRightSeparator)
}
let topPeersTitleBackground: SimpleLayer
if let current = self.topPeersTitleBackground {
topPeersTitleBackground = current
} else {
topPeersTitleBackground = SimpleLayer()
self.topPeersTitleBackground = topPeersTitleBackground
self.scrollContentView.layer.addSublayer(topPeersTitleBackground)
}
let topPeersTitle: ComponentView<Empty>
if let current = self.topPeersTitle {
topPeersTitle = current
} else {
topPeersTitle = ComponentView()
self.topPeersTitle = topPeersTitle
}
topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
let topPeersTitleSize = topPeersTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 300.0, height: 100.0)
)
let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0)
let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize)
topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor
topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5
transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame)
let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize)
if let topPeersTitleView = topPeersTitle.view {
if topPeersTitleView.superview == nil {
self.scrollContentView.addSubview(topPeersTitleView)
if case .message = reactData.reactSubject {
let topPeersLeftSeparator: SimpleLayer
if let current = self.topPeersLeftSeparator {
topPeersLeftSeparator = current
} else {
topPeersLeftSeparator = SimpleLayer()
self.topPeersLeftSeparator = topPeersLeftSeparator
self.scrollContentView.layer.addSublayer(topPeersLeftSeparator)
}
transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame)
let topPeersRightSeparator: SimpleLayer
if let current = self.topPeersRightSeparator {
topPeersRightSeparator = current
} else {
topPeersRightSeparator = SimpleLayer()
self.topPeersRightSeparator = topPeersRightSeparator
self.scrollContentView.layer.addSublayer(topPeersRightSeparator)
}
let topPeersTitleBackground: SimpleLayer
if let current = self.topPeersTitleBackground {
topPeersTitleBackground = current
} else {
topPeersTitleBackground = SimpleLayer()
self.topPeersTitleBackground = topPeersTitleBackground
self.scrollContentView.layer.addSublayer(topPeersTitleBackground)
}
let topPeersTitle: ComponentView<Empty>
if let current = self.topPeersTitle {
topPeersTitle = current
} else {
topPeersTitle = ComponentView()
self.topPeersTitle = topPeersTitle
}
topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
let topPeersTitleSize = topPeersTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: 300.0, height: 100.0)
)
let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0)
let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize)
topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor
topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5
transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame)
let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize)
if let topPeersTitleView = topPeersTitle.view {
if topPeersTitleView.superview == nil {
self.scrollContentView.addSubview(topPeersTitleView)
}
transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame)
}
let separatorY = topPeersBackgroundFrame.midY
let separatorSpacing: CGFloat = 10.0
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)))
contentHeight += 60.0
}
let separatorY = topPeersBackgroundFrame.midY
let separatorSpacing: CGFloat = 10.0
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 = reactData.topPeers
if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) {
mappedTopPeers.remove(at: index)
@ -2142,6 +2159,12 @@ private final class ChatSendStarsScreenComponent: Component {
let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator)
var peerColor: UIColor = UIColor(rgb: 0xFFB10D)
if case .liveStream = reactData.reactSubject {
let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(topPeer.count)).color ?? .purple
peerColor = StoryLiveChatMessageComponent.getMessageColor(color: color)
}
let itemSize = itemView.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
@ -2150,7 +2173,8 @@ private final class ChatSendStarsScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
peer: topPeer.peer,
count: itemCountString
count: itemCountString,
color: peerColor
)),
effectAlignment: .center,
action: { [weak self] in
@ -2239,7 +2263,7 @@ private final class ChatSendStarsScreenComponent: Component {
itemComponentView.alpha = 0.0
}
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 60.0), size: itemSize)
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight), size: itemSize)
if animateItem {
itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center)
@ -2255,7 +2279,7 @@ private final class ChatSendStarsScreenComponent: Component {
itemX += itemSize.width + itemSpacing
}
contentHeight += 164.0
contentHeight += 104.0
}
if !reactData.topPeers.isEmpty {
@ -3197,8 +3221,8 @@ private final class SliderStarsView: UIView {
self.setupEmitter()
}
self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate")
self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity")
self.emitterLayer.setValue(20.0 + Float(value * 200.0), forKeyPath: "emitterCells.emitter.birthRate")
self.emitterLayer.setValue(15.0 + value * 250.0, forKeyPath: "emitterCells.emitter.velocity")
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)

View File

@ -217,6 +217,11 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre
}
public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate {
private enum AudioRecordingRemoveAnimationState {
case recordingToAttachButton
case previewToAttachButton
}
public let textPlaceholderNode: ImmediateTextNodeWithEntities
private let glassBackgroundContainer: GlassBackgroundContainerView
@ -271,7 +276,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
private var searchActivityIndicator: ActivityIndicator?
public var audioRecordingInfoContainerNode: ASDisplayNode?
public var audioRecordingDotView: UIImageView?
public var audioRecordingDotNodeDismissed = false
private var audioRecordingRemoveAnimationState: AudioRecordingRemoveAnimationState?
public var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
public var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
@ -803,6 +808,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if sendMedia {
interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce))
} else {
strongSelf.audioRecordingRemoveAnimationState = .recordingToAttachButton
interfaceInteraction.finishMediaRecording(.dismiss)
}
} else {
@ -2256,6 +2262,46 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
audioRecordingItemsAlpha = 0.0
}
if let audioRecordingRemoveAnimationState = self.audioRecordingRemoveAnimationState, case .previewToAttachButton = audioRecordingRemoveAnimationState {
self.audioRecordingRemoveAnimationState = nil
let dotAnimation = ComponentView<Empty>()
let dotAnimationSize = dotAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "BinBlue"),
color: interfaceState.theme.chat.inputPanel.panelControlColor,
startingPosition: .begin
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
if let dotAnimationView = dotAnimation.view as? LottieComponent.View {
self.attachmentButtonBackground.contentView.addSubview(dotAnimationView)
dotAnimationView.frame = dotAnimationSize.centered(in: self.attachmentButtonBackground.contentView.bounds)
self.attachmentButtonIcon.layer.opacity = 0.0
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
guard let self else {
return
}
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let dotAnimationView {
transition.setAlpha(view: dotAnimationView, alpha: 0.0, completion: { [weak dotAnimationView] _ in
dotAnimationView?.removeFromSuperview()
})
transition.setScale(view: dotAnimationView, scale: 0.001)
}
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
})
}
}
if let mediaRecordingState {
audioRecordingItemsAlpha = 0.0
@ -2290,9 +2336,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
animateCancelSlideIn = transition.isAnimated
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
self?.viewOnce = false
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
self?.tooltipController?.dismiss()
guard let self else {
return
}
self.viewOnce = false
self.audioRecordingRemoveAnimationState = .recordingToAttachButton
self.interfaceInteraction?.finishMediaRecording(.dismiss)
self.tooltipController?.dismiss()
})
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
self.textInputContainerBackgroundView.contentView.addSubview(audioRecordingCancelIndicator)
@ -2462,13 +2512,58 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let audioRecordingDotView = self.audioRecordingDotView {
self.audioRecordingDotView = nil
var dotFrame = audioRecordingDotView.bounds.size.centered(around: audioRecordingDotView.center)
dotFrame.origin.x = hideOffset.x + leftInset + textFieldInsets.left + 16.0
transition.updatePosition(layer: audioRecordingDotView.layer, position: dotFrame.center)
audioRecordingDotView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
audioRecordingDotView.layer.animateAlpha(from: CGFloat(audioRecordingDotView.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotView] _ in
audioRecordingDotView?.removeFromSuperview()
if let audioRecordingRemoveAnimationState = self.audioRecordingRemoveAnimationState, case .recordingToAttachButton = audioRecordingRemoveAnimationState {
self.audioRecordingRemoveAnimationState = nil
let sourceFrame = audioRecordingDotView.convert(audioRecordingDotView.bounds, to: self.attachmentButtonBackground.contentView)
audioRecordingDotView.removeFromSuperview()
let dotAnimation = ComponentView<Empty>()
let dotAnimationSize = dotAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "BinRed"),
color: UIColor(rgb: 0xFF3B30),
startingPosition: .begin
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
if let dotAnimationView = dotAnimation.view as? LottieComponent.View {
self.attachmentButtonBackground.contentView.addSubview(dotAnimationView)
dotAnimationView.frame = dotAnimationSize.centered(in: sourceFrame)
transition.updatePosition(layer: dotAnimationView.layer, position: self.attachmentButtonBackground.contentView.bounds.center)
self.attachmentButtonIcon.layer.opacity = 0.0
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
guard let self else {
return
}
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let dotAnimationView {
transition.setAlpha(view: dotAnimationView, alpha: 0.0, completion: { [weak dotAnimationView] _ in
dotAnimationView?.removeFromSuperview()
})
transition.setScale(view: dotAnimationView, scale: 0.001)
}
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
})
}
} else {
var dotFrame = audioRecordingDotView.bounds.size.centered(around: audioRecordingDotView.center)
dotFrame.origin.x = hideOffset.x + leftInset + textFieldInsets.left + 16.0
transition.updatePosition(layer: audioRecordingDotView.layer, position: dotFrame.center)
audioRecordingDotView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, delay: 0.0, removeOnCompletion: false)
audioRecordingDotView.layer.animateAlpha(from: CGFloat(audioRecordingDotView.layer.presentation()?.opacity ?? 1), to: 0.0, duration: 0.15, delay: 0.0, removeOnCompletion: false) { [weak audioRecordingDotView] _ in
audioRecordingDotView?.removeFromSuperview()
}
}
}
@ -2861,7 +2956,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updateFrame(node: self.textPlaceholderNode, frame: textPlaceholderFrame)
let sendAsButtonFrame = CGRect(origin: CGPoint(x: 3.0, y: textInputContainerBackgroundFrame.height - 3.0 - 34.0), size: CGSize(width: 34.0, height: 34.0))
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: sendAsButtonFrame)
transition.updatePosition(node: self.sendAsAvatarButtonNode, position: sendAsButtonFrame.center)
transition.updateBounds(node: self.sendAsAvatarButtonNode, bounds: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
transition.updateAlpha(layer: self.sendAsAvatarButtonNode.layer, alpha: audioRecordingItemsAlpha)
transition.updateTransformScale(layer: self.sendAsAvatarButtonNode.layer, scale: audioRecordingItemsAlpha == 0.0 ? 0.001 : 1.0)
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
transition.updateFrame(node: self.sendAsAvatarReferenceNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
transition.updatePosition(node: self.sendAsAvatarNode, position: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size).center)
@ -2905,7 +3003,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
var mediaActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX + 6.0, y: textInputContainerBackgroundFrame.maxY - mediaActionButtonsSize.height), size: mediaActionButtonsSize)
if inputHasText || self.extendedSearchLayout || hasMediaDraft {
if inputHasText || self.extendedSearchLayout || hasMediaDraft || interfaceState.interfaceState.forwardMessageIds != nil {
mediaActionButtonsFrame.origin.x = width + 8.0
}
transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame)
@ -4859,6 +4957,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
@objc func attachmentButtonPressed() {
if let presentationInterfaceState = self.presentationInterfaceState, presentationInterfaceState.interfaceState.mediaDraftState != nil {
self.viewOnce = false
self.audioRecordingRemoveAnimationState = .previewToAttachButton
self.interfaceInteraction?.deleteRecordedMedia()
} else {
self.displayAttachmentMenu()

View File

@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GlassControls",
module_name = "GlassControls",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,236 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import PlainButtonComponent
import BundleIconComponent
import MultilineTextComponent
public final class GlassControlGroupComponent: Component {
public final class Item: Equatable {
public enum Content: Hashable {
case icon(String)
case text(String)
}
public let id: AnyHashable
public let content: Content
public let action: (() -> Void)?
public init(id: AnyHashable, content: Content, action: (() -> Void)?) {
self.id = id
self.content = content
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.content != rhs.content {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
}
public enum Background {
case panel
case activeTint
}
public let theme: PresentationTheme
public let background: Background
public let items: [Item]
public let minWidth: CGFloat
public init(
theme: PresentationTheme,
background: Background,
items: [Item],
minWidth: CGFloat
) {
self.theme = theme
self.background = background
self.items = items
self.minWidth = minWidth
}
public static func ==(lhs: GlassControlGroupComponent, rhs: GlassControlGroupComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.minWidth != rhs.minWidth {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: GlassBackgroundView
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var component: GlassControlGroupComponent?
private weak var state: EmptyComponentState?
override public init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func itemView(id: AnyHashable) -> UIView? {
return self.itemViews[id]?.view
}
func update(component: GlassControlGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
self.component = component
self.state = state
struct ItemId: Hashable {
var id: AnyHashable
var contentId: AnyHashable
init(id: AnyHashable, contentId: AnyHashable) {
self.id = id
self.contentId = contentId
}
}
var contentsWidth: CGFloat = 0.0
var validIds: [AnyHashable] = []
var isInteractive = false
for item in component.items {
let itemId = ItemId(id: item.id, contentId: item.content)
validIds.append(itemId)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemView = ComponentView()
self.itemViews[itemId] = itemView
itemTransition = itemTransition.withAnimation(.none)
}
if item.action != nil {
isInteractive = true
}
let content: AnyComponent<Empty>
var itemInsets = UIEdgeInsets()
switch item.content {
case let .icon(name):
content = AnyComponent(BundleIconComponent(
name: name,
tintColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor
))
case let .text(string):
content = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: string, font: Font.semibold(15.0), textColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor))
))
itemInsets.left = 10.0
itemInsets.right = itemInsets.left
}
var minItemWidth: CGFloat = 40.0
if component.items.count == 1 {
minItemWidth = max(minItemWidth, component.minWidth)
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(PlainButtonComponent(
content: content,
minSize: CGSize(width: minItemWidth, height: 40.0),
contentInsets: itemInsets,
action: {
item.action?()
},
isEnabled: item.action != nil,
animateAlpha: false,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let itemFrame = CGRect(origin: CGPoint(x: contentsWidth, y: 0.0), size: itemSize)
if let itemComponentView = itemView.view {
var animateIn = false
if itemComponentView.superview == nil {
animateIn = true
self.backgroundView.contentView.addSubview(itemComponentView)
itemComponentView.alpha = 0.0
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
if animateIn {
alphaTransition.setAlpha(view: itemComponentView, alpha: 1.0)
alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 8.0, toRadius: 0.0)
}
}
contentsWidth += itemSize.width
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
alphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 0.0, toRadius: 8.0, removeOnCompletion: false)
}
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
let size = CGSize(width: contentsWidth, height: availableSize.height)
let tintColor: GlassBackgroundView.TintColor
switch component.background {
case .panel:
tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
case .activeTint:
tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: component.theme.list.itemCheckColors.fillColor)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,273 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
public final class GlassControlPanelComponent: Component {
public final class Item: Equatable {
public let items: [GlassControlGroupComponent.Item]
public let background: GlassControlGroupComponent.Background
public init(items: [GlassControlGroupComponent.Item], background: GlassControlGroupComponent.Background) {
self.items = items
self.background = background
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.background != rhs.background {
return false
}
return true
}
}
public let theme: PresentationTheme
public let leftItem: Item?
public let rightItem: Item?
public let centralItem: Item?
public init(
theme: PresentationTheme,
leftItem: Item?,
centralItem: Item?,
rightItem: Item?
) {
self.theme = theme
self.leftItem = leftItem
self.centralItem = centralItem
self.rightItem = rightItem
}
public static func ==(lhs: GlassControlPanelComponent, rhs: GlassControlPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.leftItem != rhs.leftItem {
return false
}
if lhs.centralItem != rhs.centralItem {
return false
}
if lhs.rightItem != rhs.rightItem {
return false
}
return true
}
public final class View: UIView {
private let glassContainerView: GlassBackgroundContainerView
private var leftItemComponent: ComponentView<Empty>?
private var centralItemComponent: ComponentView<Empty>?
private var rightItemComponent: ComponentView<Empty>?
private var component: GlassControlPanelComponent?
private weak var state: EmptyComponentState?
public var leftItemView: GlassControlGroupComponent.View? {
return self.leftItemComponent?.view as? GlassControlGroupComponent.View
}
public var centerItemView: GlassControlGroupComponent.View? {
return self.centralItemComponent?.view as? GlassControlGroupComponent.View
}
public var rightItemView: GlassControlGroupComponent.View? {
return self.rightItemComponent?.view as? GlassControlGroupComponent.View
}
override public init(frame: CGRect) {
self.glassContainerView = GlassBackgroundContainerView()
super.init(frame: frame)
self.addSubview(self.glassContainerView)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GlassControlPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let minSpacing: CGFloat = 8.0
var leftItemFrame: CGRect?
if let leftItem = component.leftItem {
let leftItemComponent: ComponentView<Empty>
var leftItemTransition = transition
if let current = self.leftItemComponent {
leftItemComponent = current
} else {
leftItemComponent = ComponentView()
self.leftItemComponent = leftItemComponent
leftItemTransition = transition.withAnimation(.none)
}
let leftItemSize = leftItemComponent.update(
transition: leftItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: leftItem.background,
items: leftItem.items,
minWidth: 40.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let leftItemFrameValue = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: leftItemSize)
leftItemFrame = leftItemFrameValue
if let leftItemComponentView = leftItemComponent.view {
var animateIn = false
if leftItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(leftItemComponentView)
ComponentTransition.immediate.setScale(view: leftItemComponentView, scale: 0.001)
}
leftItemTransition.setPosition(view: leftItemComponentView, position: leftItemFrameValue.center)
leftItemTransition.setBounds(view: leftItemComponentView, bounds: CGRect(origin: CGPoint(), size: leftItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: leftItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: leftItemComponentView, scale: 1.0)
}
}
} else if let leftItemComponent = self.leftItemComponent {
self.leftItemComponent = nil
if let leftItemComponentView = leftItemComponent.view {
transition.setScale(view: leftItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: leftItemComponentView, alpha: 0.0, completion: { [weak leftItemComponentView] _ in
leftItemComponentView?.removeFromSuperview()
})
}
}
var rightItemFrame: CGRect?
if let rightItem = component.rightItem {
let rightItemComponent: ComponentView<Empty>
var rightItemTransition = transition
if let current = self.rightItemComponent {
rightItemComponent = current
} else {
rightItemComponent = ComponentView()
self.rightItemComponent = rightItemComponent
rightItemTransition = transition.withAnimation(.none)
}
let rightItemSize = rightItemComponent.update(
transition: rightItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: rightItem.background,
items: rightItem.items,
minWidth: 40.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let rightItemFrameValue = CGRect(origin: CGPoint(x: availableSize.width - rightItemSize.width, y: 0.0), size: rightItemSize)
rightItemFrame = rightItemFrameValue
if let rightItemComponentView = rightItemComponent.view {
var animateIn = false
if rightItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(rightItemComponentView)
ComponentTransition.immediate.setScale(view: rightItemComponentView, scale: 0.001)
}
rightItemTransition.setPosition(view: rightItemComponentView, position: rightItemFrameValue.center)
rightItemTransition.setBounds(view: rightItemComponentView, bounds: CGRect(origin: CGPoint(), size: rightItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: rightItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: rightItemComponentView, scale: 1.0)
}
}
} else if let rightItemComponent = self.rightItemComponent {
self.rightItemComponent = nil
if let rightItemComponentView = rightItemComponent.view {
transition.setScale(view: rightItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: rightItemComponentView, alpha: 0.0, completion: { [weak rightItemComponentView] _ in
rightItemComponentView?.removeFromSuperview()
})
}
}
if let centralItem = component.centralItem {
let centralItemComponent: ComponentView<Empty>
var centralItemTransition = transition
if let current = self.centralItemComponent {
centralItemComponent = current
} else {
centralItemComponent = ComponentView()
self.centralItemComponent = centralItemComponent
centralItemTransition = transition.withAnimation(.none)
}
var maxCentralItemSize = CGSize(width: availableSize.width, height: availableSize.height)
var centralRightInset: CGFloat = 0.0
if let rightItemFrame {
centralRightInset = availableSize.width - rightItemFrame.minX + minSpacing
}
var centralLeftInset: CGFloat = 0.0
if let leftItemFrame {
centralLeftInset = leftItemFrame.maxX + minSpacing
}
maxCentralItemSize.width = max(1.0, availableSize.width - centralLeftInset - centralRightInset)
let centralItemSize = centralItemComponent.update(
transition: centralItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: centralItem.background,
items: centralItem.items,
minWidth: 165.0
)),
environment: {},
containerSize: maxCentralItemSize
)
let centralItemFrameValue = CGRect(origin: CGPoint(x: centralLeftInset + floor((availableSize.width - centralLeftInset - centralRightInset - centralItemSize.width) * 0.5), y: 0.0), size: centralItemSize)
if let centralItemComponentView = centralItemComponent.view {
var animateIn = false
if centralItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(centralItemComponentView)
ComponentTransition.immediate.setScale(view: centralItemComponentView, scale: 0.001)
}
centralItemTransition.setPosition(view: centralItemComponentView, position: centralItemFrameValue.center)
centralItemTransition.setBounds(view: centralItemComponentView, bounds: CGRect(origin: CGPoint(), size: centralItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: centralItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: centralItemComponentView, scale: 1.0)
}
}
} else if let centralItemComponent = self.centralItemComponent {
self.centralItemComponent = nil
if let centralItemComponentView = centralItemComponent.view {
transition.setScale(view: centralItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: centralItemComponentView, alpha: 0.0, completion: { [weak centralItemComponentView] _ in
centralItemComponentView?.removeFromSuperview()
})
}
}
transition.setFrame(view: self.glassContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.glassContainerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -331,9 +331,15 @@ public final class ListSectionComponent: Component {
case legacy
}
public enum BackgroundColor {
case base
case modal
}
public let theme: PresentationTheme
public let style: Style
public let background: Background
public let backgroundColor: BackgroundColor
public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>]
@ -345,6 +351,7 @@ public final class ListSectionComponent: Component {
theme: PresentationTheme,
style: Style = .legacy,
background: Background = .all,
backgroundColor: BackgroundColor = .base,
header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>],
@ -355,6 +362,7 @@ public final class ListSectionComponent: Component {
self.theme = theme
self.style = style
self.background = background
self.backgroundColor = backgroundColor
self.header = header
self.footer = footer
self.items = items
@ -373,6 +381,9 @@ public final class ListSectionComponent: Component {
if lhs.background != rhs.background {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.header != rhs.header {
return false
}

View File

@ -182,6 +182,16 @@ public final class MessageInputPanelComponent: Component {
}
}
public struct StarStats: Equatable {
public var myStars: Int64
public var totalStars: Int64
public init(myStars: Int64, totalStars: Int64) {
self.myStars = myStars
self.totalStars = totalStars
}
}
public let externalState: ExternalState
public let context: AccountContext
public let theme: PresentationTheme
@ -244,6 +254,7 @@ public final class MessageInputPanelComponent: Component {
public let liveChatState: LiveChatState?
public let toggleLiveChatExpanded: (() -> Void)?
public let sendStarsAction: ((UIView, Bool) -> Void)?
public let starStars: StarStats?
public init(
externalState: ExternalState,
@ -307,7 +318,8 @@ public final class MessageInputPanelComponent: Component {
chatLocation: ChatLocation?,
liveChatState: LiveChatState? = nil,
toggleLiveChatExpanded: (() -> Void)? = nil,
sendStarsAction: ((UIView, Bool) -> Void)? = nil
sendStarsAction: ((UIView, Bool) -> Void)? = nil,
starStars: StarStats? = nil
) {
self.externalState = externalState
self.context = context
@ -371,6 +383,7 @@ public final class MessageInputPanelComponent: Component {
self.liveChatState = liveChatState
self.toggleLiveChatExpanded = toggleLiveChatExpanded
self.sendStarsAction = sendStarsAction
self.starStars = starStars
}
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@ -503,6 +516,9 @@ public final class MessageInputPanelComponent: Component {
if lhs.liveChatState != rhs.liveChatState {
return false
}
if lhs.starStars != rhs.starStars {
return false
}
return true
}
@ -967,7 +983,7 @@ public final class MessageInputPanelComponent: Component {
}
component.toggleLiveChatExpanded?()
}),
rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.storyItem?.views?.reactions.first(where: { $0.value == .stars })?.count ?? 0), isFilled: component.myReaction?.reaction == .stars), action: { [weak self] sourceView in
rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.starStars?.totalStars ?? 0), isFilled: (component.starStars?.myStars ?? 0) != 0), action: { [weak self] sourceView in
guard let self, let component = self.component else {
return
}

View File

@ -108,6 +108,8 @@ swift_library(
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
"//submodules/TelegramUI/Components/StarsParticleEffect",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
],
visibility = [
"//visibility:public",

View File

@ -1945,7 +1945,7 @@ private final class StoryContainerScreenComponent: Component {
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: environment.statusBarHeight, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
intrinsicInsets: UIEdgeInsets(top: environment.statusBarHeight + 54.0, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
additionalInsets: UIEdgeInsets(),
statusBarHeight: nil,

View File

@ -17,6 +17,7 @@ import MultilineTextComponent
import ContextUI
import StarsParticleEffect
import StoryLiveChatMessageComponent
import AdminUserActionsSheet
private final class PinnedBarMessageComponent: Component {
let context: AccountContext
@ -374,6 +375,7 @@ final class StoryContentLiveChatComponent: Component {
let call: PresentationGroupCall
let storyPeerId: EnginePeer.Id
let insets: UIEdgeInsets
let controller: () -> ViewController?
init(
external: External,
@ -382,7 +384,8 @@ final class StoryContentLiveChatComponent: Component {
theme: PresentationTheme,
call: PresentationGroupCall,
storyPeerId: EnginePeer.Id,
insets: UIEdgeInsets
insets: UIEdgeInsets,
controller: @escaping () -> ViewController?
) {
self.external = external
self.context = context
@ -391,6 +394,7 @@ final class StoryContentLiveChatComponent: Component {
self.call = call
self.storyPeerId = storyPeerId
self.insets = insets
self.controller = controller
}
static func ==(lhs: StoryContentLiveChatComponent, rhs: StoryContentLiveChatComponent) -> Bool {
@ -437,6 +441,7 @@ final class StoryContentLiveChatComponent: Component {
private var stateDisposable: Disposable?
private var currentListIsEmpty: Bool = true
private var isMessageContextMenuOpen: Bool = false
public var isChatEmpty: Bool {
guard let messagesState = self.messagesState else {
@ -446,6 +451,17 @@ final class StoryContentLiveChatComponent: Component {
}
private(set) var isChatExpanded: Bool = false
public var starStars: (myStars: Int64, pendingMyStars: Int64, totalStars: Int64, topItems: [GroupCallMessagesContext.TopStarsItem])? {
guard let messagesState = self.messagesState else {
return nil
}
var myStars: Int64 = 0
if let item = messagesState.topStars.first(where: { $0.isMy }) {
myStars = item.amount
}
return (myStars + messagesState.pendingMyStars, pendingMyStars: messagesState.pendingMyStars, messagesState.totalStars + messagesState.pendingMyStars, messagesState.topStars)
}
override init(frame: CGRect) {
self.listContainer = UIView()
@ -506,7 +522,7 @@ final class StoryContentLiveChatComponent: Component {
self.addSubview(self.listShadowView)
self.addSubview(self.listContainer)
//self.isChatExpanded = true
self.isChatExpanded = true
}
required init?(coder: NSCoder) {
@ -540,6 +556,60 @@ final class StoryContentLiveChatComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
private func displayDeleteMessageAndBan(id: GroupCallMessagesContext.Message.Id) {
Task { @MainActor [weak self] in
guard let self, let component = self.component else {
return
}
guard let chatPeer = await component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.storyPeerId)
).get() else {
return
}
guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else {
return
}
guard let author = message.author else {
return
}
var totalCount = 0
for message in messagesState.messages {
if message.author?.id == author.id {
totalCount += 1
}
}
guard let controller = component.controller() else {
return
}
controller.push(AdminUserActionsSheet(
context: component.context,
chatPeer: chatPeer,
peers: [RenderedChannelParticipant(
participant: .member(
id: author.id,
invitedAt: 0,
adminInfo: nil,
banInfo: nil,
rank: nil,
subscriptionUntilDate: nil
),
peer: author._asPeer()
)],
mode: .liveStream(
messageCount: 1,
deleteAllMessageCount: totalCount,
completion: { [weak self] result in
guard let self else {
return
}
let _ = self
}
),
customTheme: defaultDarkColorPresentationTheme
))
}
}
private func openMessageContextMenu(id: GroupCallMessagesContext.Message.Id, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) {
Task { @MainActor [weak self] in
guard let self else {
@ -572,7 +642,52 @@ final class StoryContentLiveChatComponent: Component {
})))
let state = await (component.call.state |> take(1)).get()
if state.canManageCall || component.storyPeerId == component.context.account.peerId {
var isAdmin = state.canManageCall
if component.storyPeerId == component.context.account.peerId {
isAdmin = true
}
var canDelete = isAdmin
var isMyMessage = false
guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else {
return
}
if message.author?.id == component.context.account.peerId {
isMyMessage = true
canDelete = true
}
if !isMyMessage, let author = message.author {
items.append(.action(ContextMenuActionItem(text: "Open Profile", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
guard let self else {
return
}
c?.dismiss(completion: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: component.context,
chatLocation: .peer(author),
keepStack: .always
))
})
})))
}
#if DEBUG
if "".isEmpty {
isAdmin = true
canDelete = true
}
#endif
if canDelete {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in
guard let self else {
return
@ -583,7 +698,11 @@ final class StoryContentLiveChatComponent: Component {
return
}
if let call = component.call as? PresentationGroupCallImpl {
call.deleteMessage(id: id)
if isAdmin && !isMyMessage {
self.displayDeleteMessageAndBan(id: id)
} else {
call.deleteMessage(id: id, reportSpam: false)
}
}
})
})))
@ -604,17 +723,18 @@ final class StoryContentLiveChatComponent: Component {
guard let self else {
return
}
if let listView = self.list.view {
let transition: ComponentTransition = .easeInOut(duration: 0.2)
transition.setAlpha(view: listView, alpha: 1.0)
self.isMessageContextMenuOpen = false
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2), isLocal: true)
}
}
if let listView = self.list.view {
let transition: ComponentTransition = .easeInOut(duration: 0.2)
transition.setAlpha(view: listView, alpha: 0.25)
self.isMessageContextMenuOpen = true
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2), isLocal: true)
}
component.context.sharedContext.mainWindow?.presentInGlobalOverlay(contextController)
component.controller()?.presentInGlobalOverlay(contextController)
}
}
@ -782,7 +902,14 @@ final class StoryContentLiveChatComponent: Component {
}
transition.setPosition(view: listView, position: listFrame.offsetBy(dx: 0.0, dy: self.isChatExpanded ? 0.0 : listFrame.height).center)
transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size))
alphaTransition.setAlpha(view: listView, alpha: listItems.isEmpty ? 0.0 : 1.0)
let listAlpha: CGFloat
if self.isMessageContextMenuOpen {
listAlpha = 0.25
} else {
listAlpha = listItems.isEmpty ? 0.0 : 1.0
}
alphaTransition.setAlpha(view: listView, alpha: listAlpha)
}
transition.setFrame(view: self.listContainer, frame: CGRect(origin: CGPoint(), size: availableSize))

View File

@ -40,8 +40,9 @@ final class StoryItemContentComponent: Component {
let preferHighQuality: Bool
let isEmbeddedInCamera: Bool
let activateReaction: (UIView, MessageReaction.Reaction) -> Void
let controller: () -> ViewController?
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, baseRate: Double, isVideoBuffering: Bool, isCurrent: Bool, preferHighQuality: Bool, isEmbeddedInCamera: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, baseRate: Double, isVideoBuffering: Bool, isCurrent: Bool, preferHighQuality: Bool, isEmbeddedInCamera: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void, controller: @escaping () -> ViewController?) {
self.context = context
self.strings = strings
self.peer = peer
@ -55,6 +56,7 @@ final class StoryItemContentComponent: Component {
self.preferHighQuality = preferHighQuality
self.isEmbeddedInCamera = isEmbeddedInCamera
self.activateReaction = activateReaction
self.controller = controller
}
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
@ -167,6 +169,13 @@ final class StoryItemContentComponent: Component {
)
}
public var starStars: (myStars: Int64, pendingMyStars: Int64, totalStars: Int64, topItems: [GroupCallMessagesContext.TopStarsItem])? {
guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else {
return nil
}
return liveChatView.starStars
}
public func toggleLiveChatExpanded() {
guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else {
return
@ -852,7 +861,13 @@ final class StoryItemContentComponent: Component {
theme: environment.theme,
call: mediaStreamCall,
storyPeerId: component.peer.id,
insets: environment.containerInsets
insets: environment.containerInsets,
controller: { [weak self] in
guard let self, let component = self.component else {
return nil
}
return component.controller()
}
)),
environment: {},
containerSize: availableSize

View File

@ -1628,6 +1628,12 @@ public final class StoryItemSetContainerComponent: Component {
return
}
self.sendMessageContext.activateInlineReaction(view: self, reactionView: reactionView, reaction: reaction)
},
controller: { [weak self] in
guard let self, let component = self.component else {
return nil
}
return component.controller()
}
)),
environment: {
@ -2964,6 +2970,7 @@ public final class StoryItemSetContainerComponent: Component {
}
var liveChatState: MessageInputPanelComponent.LiveChatState?
var starStats: MessageInputPanelComponent.StarStats?
if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
liveChatState = visibleItemView.liveChatState.flatMap { liveChatState in
return MessageInputPanelComponent.LiveChatState(
@ -2971,6 +2978,12 @@ public final class StoryItemSetContainerComponent: Component {
hasUnseenMessages: liveChatState.hasUnseenMessages
)
}
starStats = visibleItemView.starStars.flatMap { starStats in
return MessageInputPanelComponent.StarStats(
myStars: starStats.myStars,
totalStars: starStats.totalStars
)
}
}
inputPanelSize = self.inputPanel.update(
@ -3220,7 +3233,8 @@ public final class StoryItemSetContainerComponent: Component {
} else {
self.sendMessageContext.performSendStars(view: self, buttonView: sourceView, count: 1, isFromExpandedView: false)
}
} : nil
} : nil,
starStars: starStats
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)

View File

@ -52,6 +52,7 @@ import StoryQualityUpgradeSheetScreen
import AudioWaveform
import ChatMessagePaymentAlertController
import ChatSendStarsScreen
import AnimatedTextComponent
private var ObjCKey_DeinitWatcher: Int?
@ -102,6 +103,7 @@ final class StoryItemSetContainerSendMessage {
var currentSpeechHolder: SpeechSynthesizerHolder?
var currentLiveStreamMessageStars: StarsAmount?
weak var currentSendStarsUndoController: UndoOverlayController?
private(set) var isMediaRecordingLocked: Bool = false
var wasRecordingDismissed: Bool = false
@ -422,7 +424,7 @@ final class StoryItemSetContainerSendMessage {
return
}
self.currentLiveStreamMessageStars = nil
view.state?.updated(transition: .spring(duration: 0.3))
view.state?.updated(transition: .spring(duration: 0.4))
})))
}
} else {
@ -677,7 +679,7 @@ final class StoryItemSetContainerSendMessage {
self.currentInputMode = .text
self.currentLiveStreamMessageStars = nil
view.state?.updated(transition: .spring(duration: 0.3))
view.state?.updated(transition: .spring(duration: 0.4))
let controller = component.controller() as? StoryContainerScreen
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
@ -759,7 +761,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) {
view.endEditing(true)
} else {
view.state?.updated(transition: .spring(duration: 0.3))
view.state?.updated(transition: .spring(duration: 0.4))
}
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
}
@ -826,7 +828,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) {
view.endEditing(true)
} else {
view.state?.updated(transition: .spring(duration: 0.3))
view.state?.updated(transition: .spring(duration: 0.4))
}
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
})
@ -886,7 +888,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) {
view.endEditing(true)
} else {
view.state?.updated(transition: .spring(duration: 0.3))
view.state?.updated(transition: .spring(duration: 0.4))
}
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
})
@ -3890,11 +3892,26 @@ final class StoryItemSetContainerSendMessage {
return
}
var topPeers: [ReactionsMessageAttribute.TopPeer] = []
if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
if let topItems = visibleItemView.starStars?.topItems {
topPeers = topItems.map { item -> ReactionsMessageAttribute.TopPeer in
return ReactionsMessageAttribute.TopPeer(
peerId: item.peerId,
count: Int32(item.amount),
isTop: item.isTop,
isMy: item.isMy,
isAnonymous: item.isAnonymous
)
}
}
}
let initialData = await ChatSendStarsScreen.initialData(
context: component.context,
peerId: peerId,
reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id),
topPeers: [],
topPeers: topPeers,
completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in
guard let view, let component = view.component else {
return
@ -3916,7 +3933,168 @@ final class StoryItemSetContainerSendMessage {
return
}
let _ = component.context.engine.messages.sendStoryStars(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, count: count).startStandalone()
if isFromExpandedView {
self.commitSendStars(view: view, count: count, delay: false)
} else {
Task { @MainActor [weak view] in
guard let view, let component = view.component else {
return
}
var reactionItem: ReactionItem?
if let availableReactions = await component.context.availableReactions.get() {
for item in availableReactions.reactions {
if item.value == .stars {
guard let centerAnimation = item.centerAnimation else {
continue
}
guard let aroundAnimation = item.aroundAnimation else {
continue
}
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: item.value),
appearAnimation: item.appearAnimation,
stillAnimation: item.selectAnimation,
listAnimation: centerAnimation,
largeListAnimation: item.activateAnimation,
applicationAnimation: aroundAnimation,
largeApplicationAnimation: item.effectAnimation,
isCustom: false
)
break
}
}
}
if let reactionItem {
let targetFrame = buttonView.convert(buttonView.bounds, to: view)
let targetView = UIView(frame: targetFrame)
targetView.isUserInteractionEnabled = false
view.addSubview(targetView)
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
let standaloneReactionAnimationView = standaloneReactionAnimation.view
standaloneReactionAnimation.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimationView] _ in
standaloneReactionAnimationView?.removeFromSuperview()
})
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
standaloneReactionAnimation.animateReactionSelection(
context: component.context,
theme: component.theme,
animationCache: component.context.animationCache,
reaction: reactionItem,
avatarPeers: [],
playHaptic: true,
isLarge: false,
hideCenterAnimation: true,
targetView: targetView,
addStandaloneReactionAnimation: { [weak view] standaloneReactionAnimation in
guard let view else {
return
}
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
standaloneReactionAnimation.view.removeFromSuperview()
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
},
completion: { [weak targetView, weak standaloneReactionAnimation] in
targetView?.removeFromSuperview()
standaloneReactionAnimation?.view.removeFromSuperview()
}
)
}
}
self.commitSendStars(view: view, count: count, delay: true)
var totalStars = count
if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
if let pendingMyStars = visibleItemView.starStars?.pendingMyStars {
totalStars += Int(pendingMyStars)
}
}
let title: String
/*if case .anonymous = privacy {
title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount))
} else if case .peer = privacy, let privacyPeer {
let rawTitle = self.presentationData.strings.Chat_ToastStarsSent_TitleChannel(Int32(self.currentSendStarsUndoCount))
title = rawTitle.replacingOccurrences(of: "{name}", with: privacyPeer.compactDisplayTitle)
} else*/ do {
title = component.strings.Chat_ToastStarsSent_Title(Int32(totalStars))
}
let textItems = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [
0: .number(totalStars, minDigits: 1),
1: .text(component.strings.Chat_ToastStarsSent_TextStarAmount(Int32(totalStars)))
])
if let current = self.currentSendStarsUndoController {
current.content = .starsSent(context: component.context, title: title, text: textItems, hasUndo: true)
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let controller = UndoOverlayController(presentationData: presentationData, content: .starsSent(context: component.context, title: title, text: textItems, hasUndo: true), elevatedLayout: false, position: .top, action: { [weak view] action in
guard let view else {
return false
}
if case .undo = action {
guard let component = view.component else {
return false
}
guard case .liveStream = component.slice.item.storyItem.media else {
return false
}
guard let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return false
}
guard let call = itemView.mediaStreamCall else {
return false
}
call.cancelSendStars()
}
return false
})
self.currentSendStarsUndoController = controller
self.view?.component?.controller()?.present(controller, in: .current)
}
}
}
private func commitSendStars(view: StoryItemSetContainerComponent.View, count: Int, delay: Bool) {
guard let component = view.component else {
return
}
guard case .liveStream = component.slice.item.storyItem.media else {
return
}
guard let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return
}
guard let call = itemView.mediaStreamCall else {
return
}
if let current = self.currentSendStarsUndoController {
self.currentSendStarsUndoController = nil
current.dismiss()
}
call.sendStars(amount: Int64(count), delay: delay)
}
}