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/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", "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.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.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.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", "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/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", "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.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_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.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "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.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/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "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.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"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/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", "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.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "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) { 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() { if let blurFilter = CALayer.blur() {
blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius") blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius")
layer.filters = [blurFilter] layer.filters = [blurFilter]

View File

@ -303,6 +303,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }
dict[-674602536] = { return Api.GroupCall.parse_groupCall($0) } dict[-674602536] = { return Api.GroupCall.parse_groupCall($0) }
dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($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[445316222] = { return Api.GroupCallMessage.parse_groupCallMessage($0) }
dict[708691884] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) } dict[708691884] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) }
dict[1735736008] = { return Api.GroupCallParticipantVideo.parse_groupCallParticipantVideo($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[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) }
dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) }
dict[-1636664659] = { return Api.phone.GroupCall.parse_groupCall($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[-790330702] = { return Api.phone.GroupCallStreamChannels.parse_groupCallStreamChannels($0) }
dict[767505458] = { return Api.phone.GroupCallStreamRtmpUrl.parse_groupCallStreamRtmpUrl($0) } dict[767505458] = { return Api.phone.GroupCallStreamRtmpUrl.parse_groupCallStreamRtmpUrl($0) }
dict[-193506890] = { return Api.phone.GroupParticipants.parse_groupParticipants($0) } dict[-193506890] = { return Api.phone.GroupParticipants.parse_groupParticipants($0) }
@ -1820,6 +1822,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.GroupCall: case let _1 as Api.GroupCall:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.GroupCallDonor:
_1.serialize(buffer, boxed)
case let _1 as Api.GroupCallMessage: case let _1 as Api.GroupCallMessage:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.GroupCallParticipant: case let _1 as Api.GroupCallParticipant:
@ -2640,6 +2644,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCall: case let _1 as Api.phone.GroupCall:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStars:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStreamChannels: case let _1 as Api.phone.GroupCallStreamChannels:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.phone.GroupCallStreamRtmpUrl: 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 { public extension Api.phone {
enum GroupCallStreamChannels: TypeConstructorDescription { enum GroupCallStreamChannels: TypeConstructorDescription {
case groupCallStreamChannels(channels: [Api.GroupCallStreamChannel]) 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 { public extension Api.smsjobs {
enum EligibilityToJoin: TypeConstructorDescription { enum EligibilityToJoin: TypeConstructorDescription {
case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) 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 { public extension Api.functions.phone {
static func getGroupCallStreamChannels(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.GroupCallStreamChannels>) { static func getGroupCallStreamChannels(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.GroupCallStreamChannels>) {
let buffer = Buffer() 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 { public extension Api {
enum GroupCallMessage: TypeConstructorDescription { enum GroupCallMessage: TypeConstructorDescription {
case groupCallMessage(flags: Int32, id: Int32, fromId: Api.Peer, date: Int32, message: Api.TextWithEntities, paidMessageStars: Int64?) 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 { public extension Api {
enum GroupCallParticipantVideo: TypeConstructorDescription { enum GroupCallParticipantVideo: TypeConstructorDescription {
case groupCallParticipantVideo(flags: Int32, endpoint: String, sourceGroups: [Api.GroupCallParticipantVideoSourceGroup], audioSource: Int32?) 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 { public extension Api {
enum InputBusinessChatLink: TypeConstructorDescription { enum InputBusinessChatLink: TypeConstructorDescription {
case inputBusinessChatLink(flags: Int32, message: String, entities: [Api.MessageEntity]?, title: String?) 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> { public var messagesState: Signal<GroupCallMessagesContext.State, NoError> {
return self.messagesStatePromise.get() 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 { 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 struct State: Equatable {
public var messages: [Message] public var messages: [Message]
public var pinnedMessages: [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.messages = messages
self.pinnedMessages = pinnedMessages self.pinnedMessages = pinnedMessages
self.topStars = topStars
self.totalStars = totalStars
self.pendingMyStars = pendingMyStars
} }
} }
@ -3789,12 +3830,19 @@ public final class GroupCallMessagesContext {
let stateValue = ValuePromise<State>() let stateValue = ValuePromise<State>()
var updatesDisposable: Disposable? var updatesDisposable: Disposable?
var didInitializeTopStars: Bool = false
var pollTopStarsDisposable: Disposable?
let sendMessageDisposables = DisposableSet() let sendMessageDisposables = DisposableSet()
var processedIds = Set<Int64>() var processedIds = Set<Int64>()
private var messageLifeTimer: SwiftSignalKit.Timer? 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) { init(queue: Queue, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) {
self.queue = queue self.queue = queue
self.account = account self.account = account
@ -3804,9 +3852,10 @@ public final class GroupCallMessagesContext {
self.messageLifetime = messageLifetime self.messageLifetime = messageLifetime
self.isLiveStream = isLiveStream self.isLiveStream = isLiveStream
self.state = State(messages: [], pinnedMessages: []) self.state = State(messages: [], pinnedMessages: [], topStars: [], totalStars: 0, pendingMyStars: 0)
self.stateValue.set(self.state) self.stateValue.set(self.state)
let accountPeerId = account.peerId
self.updatesDisposable = (account.stateManager.groupCallMessageUpdates self.updatesDisposable = (account.stateManager.groupCallMessageUpdates
|> deliverOn(self.queue)).startStrict(next: { [weak self] updates in |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in
guard let self else { guard let self else {
@ -3913,13 +3962,20 @@ public final class GroupCallMessagesContext {
} }
existingIds.insert(message.id) existingIds.insert(message.id)
state.messages.append(message) state.messages.append(message)
if self.isLiveStream && message.paidStars != nil { if self.isLiveStream, let paidStars = message.paidStars {
if message.date + message.lifetime >= currentTime { if message.date + message.lifetime >= currentTime {
state.pinnedMessages.append(message) 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.state = state
self.didInitializeTopStars = true
}) })
} }
}) })
@ -3929,12 +3985,76 @@ public final class GroupCallMessagesContext {
}, queue: self.queue) }, queue: self.queue)
self.messageLifeTimer = timer self.messageLifeTimer = timer
timer.start() timer.start()
self.pollTopStars()
} }
deinit { deinit {
self.updatesDisposable?.dispose() self.updatesDisposable?.dispose()
self.sendMessageDisposables.dispose() self.sendMessageDisposables.dispose()
self.messageLifeTimer?.invalidate() 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() { 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?) { func send(fromId: EnginePeer.Id, randomId requestedRandomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId) return transaction.getPeer(fromId)
@ -4004,19 +4214,15 @@ public final class GroupCallMessagesContext {
) )
state.messages.append(message) state.messages.append(message)
if self.isLiveStream { if self.isLiveStream {
if paidStars != nil { if let paidStars {
state.pinnedMessages.append(message) state.pinnedMessages.append(message)
if let fromPeer {
Impl.addStateStars(state: &state, peerId: fromPeer.id, isMy: true, amount: paidStars)
}
} }
} }
self.state = state self.state = state
#if DEBUG
var paidStars = paidStars
if "".isEmpty {
paidStars = nil
}
#endif
self.processedIds.insert(randomId) self.processedIds.insert(randomId)
if let e2eContext = self.e2eContext, let messageData = serializeGroupCallMessage(randomId: randomId, text: text, entities: entities) { 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 flags |= 1 << 0
} }
self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage(
flags: 0, flags: flags,
call: self.reference.apiInputGroupCall, call: self.reference.apiInputGroupCall,
randomId: randomId, randomId: randomId,
message: .textWithEntities( 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? var updatedState: State?
if let index = self.state.messages.firstIndex(where: { $0.id == id }) { if let index = self.state.messages.firstIndex(where: { $0.id == id }) {
if updatedState == nil { if updatedState == nil {
@ -4097,6 +4482,12 @@ public final class GroupCallMessagesContext {
if let updatedState { if let updatedState {
self.state = 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 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) { private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return return
@ -578,7 +586,7 @@ private final class AdminUserActionsSheetComponent: Component {
if themeUpdated { if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 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.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor 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 let optionsItem: (OptionsSection) -> AnyComponentWithIdentity<Empty> = { section in
@ -912,6 +923,13 @@ private final class AdminUserActionsSheetComponent: Component {
titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) 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( let titleSize = self.title.update(
@ -965,6 +983,7 @@ private final class AdminUserActionsSheetComponent: Component {
transition: optionsSectionTransition, transition: optionsSectionTransition,
component: AnyComponent(ListSectionComponent( component: AnyComponent(ListSectionComponent(
theme: environment.theme, theme: environment.theme,
style: .glass,
header: AnyComponent(MultilineTextComponent( header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader, string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader,
@ -1042,6 +1061,7 @@ private final class AdminUserActionsSheetComponent: Component {
} }
if case let .channel(channel) = component.chatPeer, channel.isMonoForum { if case let .channel(channel) = component.chatPeer, channel.isMonoForum {
} else if case .liveStream = component.mode {
} else { } else {
var allConfigItems: [(ConfigItem, Bool)] = [] var allConfigItems: [(ConfigItem, Bool)] = []
if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty {
@ -1362,9 +1382,11 @@ private final class AdminUserActionsSheetComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(ButtonComponent( component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background( background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor, color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor, 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( content: AnyComponentWithIdentity(
id: AnyHashable(0), id: AnyHashable(0),
@ -1389,11 +1411,13 @@ private final class AdminUserActionsSheetComponent: Component {
completion(self.calculateMonoforumResult()) completion(self.calculateMonoforumResult())
case let .chat(_, _, completion): case let .chat(_, _, completion):
completion(self.calculateChatResult()) completion(self.calculateChatResult())
case let .liveStream(_, _, completion):
completion(self.calculateLiveStreamResult())
} }
} }
)), )),
environment: {}, 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 bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) 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 class AdminUserActionsSheet: ViewControllerComponentContainer {
public enum Mode { public enum Mode {
case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void) case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void)
case liveStream(messageCount: Int, deleteAllMessageCount: Int?, completion: (LiveStreamResult) -> Void)
case monoforum(completion: (MonoforumResult) -> 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 final class MonoforumResult {
public let ban: Bool public let ban: Bool
public let reportSpam: Bool public let reportSpam: Bool
@ -1493,9 +1530,9 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer {
private var isDismissed: Bool = false 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 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.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal

View File

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

View File

@ -18,8 +18,11 @@ import TelegramNotices
import GlassBackgroundComponent import GlassBackgroundComponent
import ComponentFlow import ComponentFlow
import ComponentDisplayAdapters import ComponentDisplayAdapters
import GlassControls
import BundleIconComponent
import MultilineTextComponent
private enum SubscriberAction: Equatable { private enum SubscriberAction: Equatable, Hashable {
case join case join
case joinGroup case joinGroup
case applyToJoin case applyToJoin
@ -143,7 +146,10 @@ private func actionForPeer(context: AccountContext, peer: Peer, interfaceState:
private let badgeFont = Font.regular(14.0) private let badgeFont = Font.regular(14.0)
public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { 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 button: HighlightableButton
private let buttonTitle: ImmediateTextNode private let buttonTitle: ImmediateTextNode
private let buttonTintTitle: ImmediateTextNode private let buttonTintTitle: ImmediateTextNode
@ -158,7 +164,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let suggestedPostButtonBackgroundView: GlassBackgroundView private let suggestedPostButtonBackgroundView: GlassBackgroundView
private let suggestedPostButton: HighlightableButton private let suggestedPostButton: HighlightableButton
private let suggestedPostButtonIconView: UIImageView private let suggestedPostButtonIconView: UIImageView*/
private var action: SubscriberAction? 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)? private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)?
public override init() { public override init() {
self.button = HighlightableButton() /*self.button = HighlightableButton()
self.buttonBackgroundView = GlassBackgroundView() self.buttonBackgroundView = GlassBackgroundView()
self.buttonBackgroundView.isUserInteractionEnabled = false self.buttonBackgroundView.isUserInteractionEnabled = false
self.buttonTitle = ImmediateTextNode() self.buttonTitle = ImmediateTextNode()
@ -203,18 +209,20 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView() self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView()
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView) self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView)
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton) self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton)
self.suggestedPostButtonBackgroundView.isHidden = true self.suggestedPostButtonBackgroundView.isHidden = true*/
super.init() super.init()
self.view.addSubview(self.buttonBackgroundView) /*self.view.addSubview(self.buttonBackgroundView)
self.view.addSubview(self.helpButtonBackgroundView) self.view.addSubview(self.helpButtonBackgroundView)
self.view.addSubview(self.giftButtonBackgroundView) self.view.addSubview(self.giftButtonBackgroundView)
self.view.addSubview(self.suggestedPostButtonBackgroundView) self.view.addSubview(self.suggestedPostButtonBackgroundView)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside) self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside)
self.giftButton.addTarget(self, action: #selector(self.giftPressed), 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 { deinit {
@ -330,11 +338,17 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
} }
#endif*/ #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() let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, { Queue.mainQueue().after(0.4, { [weak giftItemView] in
let absoluteFrame = self.giftButton.convert(self.giftButton.bounds, to: parentController.view) 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 location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -357,11 +371,14 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
) )
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil) 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() let _ = ApplicationSpecificNotice.incrementChannelSuggestTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, { Queue.mainQueue().after(0.4, { [weak suggestPostItemView] in
let absoluteFrame = self.suggestedPostButton.convert(self.suggestedPostButton.bounds, to: parentController.view) 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 location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -394,7 +411,133 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let isFirstTime = self.layoutData == nil let isFirstTime = self.layoutData == nil
self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics) 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 let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState 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.suggestedPostButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size)))
} }
transition.updateFrame(view: self.suggestedPostButton, frame: 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 return panelHeight
} }

View File

@ -257,13 +257,16 @@ private final class BadgeComponent: Component {
private final class PeerBadgeComponent: Component { private final class PeerBadgeComponent: Component {
let theme: PresentationTheme let theme: PresentationTheme
let title: String let title: String
let color: UIColor
init( init(
theme: PresentationTheme, theme: PresentationTheme,
title: String title: String,
color: UIColor
) { ) {
self.theme = theme self.theme = theme
self.title = title self.title = title
self.color = color
} }
static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool {
@ -273,6 +276,9 @@ private final class PeerBadgeComponent: Component {
if lhs.title != rhs.title { if lhs.title != rhs.title {
return false return false
} }
if lhs.color != rhs.color {
return false
}
return true 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) 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.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) let backgroundFrame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.frame = backgroundFrame self.backgroundLayer.frame = backgroundFrame
@ -370,19 +376,22 @@ private final class PeerComponent: Component {
let strings: PresentationStrings let strings: PresentationStrings
let peer: EnginePeer? let peer: EnginePeer?
let count: String let count: String
let color: UIColor
init( init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
peer: EnginePeer?, peer: EnginePeer?,
count: String count: String,
color: UIColor
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.peer = peer self.peer = peer
self.count = count self.count = count
self.color = color
} }
static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool {
@ -401,6 +410,9 @@ private final class PeerComponent: Component {
if lhs.count != rhs.count { if lhs.count != rhs.count {
return false return false
} }
if lhs.color != rhs.color {
return false
}
return true return true
} }
@ -445,7 +457,8 @@ private final class PeerComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent(PeerBadgeComponent( component: AnyComponent(PeerBadgeComponent(
theme: component.theme, theme: component.theme,
title: component.count title: component.count,
color: component.color
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 200.0, height: 200.0) containerSize: CGSize(width: 200.0, height: 200.0)
@ -2015,71 +2028,75 @@ private final class ChatSendStarsScreenComponent: Component {
if !reactData.topPeers.isEmpty { if !reactData.topPeers.isEmpty {
contentHeight += 3.0 contentHeight += 3.0
let topPeersLeftSeparator: SimpleLayer if case .message = reactData.reactSubject {
if let current = self.topPeersLeftSeparator { let topPeersLeftSeparator: SimpleLayer
topPeersLeftSeparator = current if let current = self.topPeersLeftSeparator {
} else { topPeersLeftSeparator = current
topPeersLeftSeparator = SimpleLayer() } else {
self.topPeersLeftSeparator = topPeersLeftSeparator topPeersLeftSeparator = SimpleLayer()
self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) 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)
} }
transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame)
}
let separatorY = topPeersBackgroundFrame.midY let topPeersRightSeparator: SimpleLayer
let separatorSpacing: CGFloat = 10.0 if let current = self.topPeersRightSeparator {
transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) topPeersRightSeparator = current
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))) } 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
}
var mappedTopPeers = reactData.topPeers var mappedTopPeers = reactData.topPeers
if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) {
@ -2142,6 +2159,12 @@ private final class ChatSendStarsScreenComponent: Component {
let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator) 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( let itemSize = itemView.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(PlainButtonComponent( component: AnyComponent(PlainButtonComponent(
@ -2150,7 +2173,8 @@ private final class ChatSendStarsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
peer: topPeer.peer, peer: topPeer.peer,
count: itemCountString count: itemCountString,
color: peerColor
)), )),
effectAlignment: .center, effectAlignment: .center,
action: { [weak self] in action: { [weak self] in
@ -2239,7 +2263,7 @@ private final class ChatSendStarsScreenComponent: Component {
itemComponentView.alpha = 0.0 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 { if animateItem {
itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center)
@ -2255,7 +2279,7 @@ private final class ChatSendStarsScreenComponent: Component {
itemX += itemSize.width + itemSpacing itemX += itemSize.width + itemSpacing
} }
contentHeight += 164.0 contentHeight += 104.0
} }
if !reactData.topPeers.isEmpty { if !reactData.topPeers.isEmpty {
@ -3197,8 +3221,8 @@ private final class SliderStarsView: UIView {
self.setupEmitter() self.setupEmitter()
} }
self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate") self.emitterLayer.setValue(20.0 + Float(value * 200.0), forKeyPath: "emitterCells.emitter.birthRate")
self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity") self.emitterLayer.setValue(15.0 + value * 250.0, forKeyPath: "emitterCells.emitter.velocity")
self.emitterLayer.frame = CGRect(origin: .zero, size: size) self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) 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 { public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate {
private enum AudioRecordingRemoveAnimationState {
case recordingToAttachButton
case previewToAttachButton
}
public let textPlaceholderNode: ImmediateTextNodeWithEntities public let textPlaceholderNode: ImmediateTextNodeWithEntities
private let glassBackgroundContainer: GlassBackgroundContainerView private let glassBackgroundContainer: GlassBackgroundContainerView
@ -271,7 +276,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
private var searchActivityIndicator: ActivityIndicator? private var searchActivityIndicator: ActivityIndicator?
public var audioRecordingInfoContainerNode: ASDisplayNode? public var audioRecordingInfoContainerNode: ASDisplayNode?
public var audioRecordingDotView: UIImageView? public var audioRecordingDotView: UIImageView?
public var audioRecordingDotNodeDismissed = false private var audioRecordingRemoveAnimationState: AudioRecordingRemoveAnimationState?
public var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? public var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
public var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? public var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
@ -803,6 +808,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if sendMedia { if sendMedia {
interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce)) interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce))
} else { } else {
strongSelf.audioRecordingRemoveAnimationState = .recordingToAttachButton
interfaceInteraction.finishMediaRecording(.dismiss) interfaceInteraction.finishMediaRecording(.dismiss)
} }
} else { } else {
@ -2256,6 +2262,46 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
audioRecordingItemsAlpha = 0.0 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 { if let mediaRecordingState {
audioRecordingItemsAlpha = 0.0 audioRecordingItemsAlpha = 0.0
@ -2290,9 +2336,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
animateCancelSlideIn = transition.isAnimated animateCancelSlideIn = transition.isAnimated
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
self?.viewOnce = false guard let self else {
self?.interfaceInteraction?.finishMediaRecording(.dismiss) return
self?.tooltipController?.dismiss() }
self.viewOnce = false
self.audioRecordingRemoveAnimationState = .recordingToAttachButton
self.interfaceInteraction?.finishMediaRecording(.dismiss)
self.tooltipController?.dismiss()
}) })
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
self.textInputContainerBackgroundView.contentView.addSubview(audioRecordingCancelIndicator) self.textInputContainerBackgroundView.contentView.addSubview(audioRecordingCancelIndicator)
@ -2462,13 +2512,58 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let audioRecordingDotView = self.audioRecordingDotView { if let audioRecordingDotView = self.audioRecordingDotView {
self.audioRecordingDotView = nil self.audioRecordingDotView = nil
var dotFrame = audioRecordingDotView.bounds.size.centered(around: audioRecordingDotView.center) if let audioRecordingRemoveAnimationState = self.audioRecordingRemoveAnimationState, case .recordingToAttachButton = audioRecordingRemoveAnimationState {
dotFrame.origin.x = hideOffset.x + leftInset + textFieldInsets.left + 16.0 self.audioRecordingRemoveAnimationState = nil
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) let sourceFrame = audioRecordingDotView.convert(audioRecordingDotView.bounds, to: self.attachmentButtonBackground.contentView)
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()
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) 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)) 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.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: sendAsButtonFrame.size))
transition.updateFrame(node: self.sendAsAvatarReferenceNode, 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) 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) 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 mediaActionButtonsFrame.origin.x = width + 8.0
} }
transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame) transition.updateFrame(node: self.mediaActionButtons, frame: mediaActionButtonsFrame)
@ -4859,6 +4957,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
@objc func attachmentButtonPressed() { @objc func attachmentButtonPressed() {
if let presentationInterfaceState = self.presentationInterfaceState, presentationInterfaceState.interfaceState.mediaDraftState != nil { if let presentationInterfaceState = self.presentationInterfaceState, presentationInterfaceState.interfaceState.mediaDraftState != nil {
self.viewOnce = false self.viewOnce = false
self.audioRecordingRemoveAnimationState = .previewToAttachButton
self.interfaceInteraction?.deleteRecordedMedia() self.interfaceInteraction?.deleteRecordedMedia()
} else { } else {
self.displayAttachmentMenu() 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 case legacy
} }
public enum BackgroundColor {
case base
case modal
}
public let theme: PresentationTheme public let theme: PresentationTheme
public let style: Style public let style: Style
public let background: Background public let background: Background
public let backgroundColor: BackgroundColor
public let header: AnyComponent<Empty>? public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>? public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>] public let items: [AnyComponentWithIdentity<Empty>]
@ -345,6 +351,7 @@ public final class ListSectionComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
style: Style = .legacy, style: Style = .legacy,
background: Background = .all, background: Background = .all,
backgroundColor: BackgroundColor = .base,
header: AnyComponent<Empty>?, header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?, footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>], items: [AnyComponentWithIdentity<Empty>],
@ -355,6 +362,7 @@ public final class ListSectionComponent: Component {
self.theme = theme self.theme = theme
self.style = style self.style = style
self.background = background self.background = background
self.backgroundColor = backgroundColor
self.header = header self.header = header
self.footer = footer self.footer = footer
self.items = items self.items = items
@ -373,6 +381,9 @@ public final class ListSectionComponent: Component {
if lhs.background != rhs.background { if lhs.background != rhs.background {
return false return false
} }
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.header != rhs.header { if lhs.header != rhs.header {
return false 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 externalState: ExternalState
public let context: AccountContext public let context: AccountContext
public let theme: PresentationTheme public let theme: PresentationTheme
@ -244,6 +254,7 @@ public final class MessageInputPanelComponent: Component {
public let liveChatState: LiveChatState? public let liveChatState: LiveChatState?
public let toggleLiveChatExpanded: (() -> Void)? public let toggleLiveChatExpanded: (() -> Void)?
public let sendStarsAction: ((UIView, Bool) -> Void)? public let sendStarsAction: ((UIView, Bool) -> Void)?
public let starStars: StarStats?
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -307,7 +318,8 @@ public final class MessageInputPanelComponent: Component {
chatLocation: ChatLocation?, chatLocation: ChatLocation?,
liveChatState: LiveChatState? = nil, liveChatState: LiveChatState? = nil,
toggleLiveChatExpanded: (() -> Void)? = nil, toggleLiveChatExpanded: (() -> Void)? = nil,
sendStarsAction: ((UIView, Bool) -> Void)? = nil sendStarsAction: ((UIView, Bool) -> Void)? = nil,
starStars: StarStats? = nil
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -371,6 +383,7 @@ public final class MessageInputPanelComponent: Component {
self.liveChatState = liveChatState self.liveChatState = liveChatState
self.toggleLiveChatExpanded = toggleLiveChatExpanded self.toggleLiveChatExpanded = toggleLiveChatExpanded
self.sendStarsAction = sendStarsAction self.sendStarsAction = sendStarsAction
self.starStars = starStars
} }
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@ -503,6 +516,9 @@ public final class MessageInputPanelComponent: Component {
if lhs.liveChatState != rhs.liveChatState { if lhs.liveChatState != rhs.liveChatState {
return false return false
} }
if lhs.starStars != rhs.starStars {
return false
}
return true return true
} }
@ -967,7 +983,7 @@ public final class MessageInputPanelComponent: Component {
} }
component.toggleLiveChatExpanded?() 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 { guard let self, let component = self.component else {
return return
} }

View File

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

View File

@ -1945,7 +1945,7 @@ private final class StoryContainerScreenComponent: Component {
size: availableSize, size: availableSize,
metrics: environment.metrics, metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics, 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), safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
additionalInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(),
statusBarHeight: nil, statusBarHeight: nil,

View File

@ -17,6 +17,7 @@ import MultilineTextComponent
import ContextUI import ContextUI
import StarsParticleEffect import StarsParticleEffect
import StoryLiveChatMessageComponent import StoryLiveChatMessageComponent
import AdminUserActionsSheet
private final class PinnedBarMessageComponent: Component { private final class PinnedBarMessageComponent: Component {
let context: AccountContext let context: AccountContext
@ -374,6 +375,7 @@ final class StoryContentLiveChatComponent: Component {
let call: PresentationGroupCall let call: PresentationGroupCall
let storyPeerId: EnginePeer.Id let storyPeerId: EnginePeer.Id
let insets: UIEdgeInsets let insets: UIEdgeInsets
let controller: () -> ViewController?
init( init(
external: External, external: External,
@ -382,7 +384,8 @@ final class StoryContentLiveChatComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
call: PresentationGroupCall, call: PresentationGroupCall,
storyPeerId: EnginePeer.Id, storyPeerId: EnginePeer.Id,
insets: UIEdgeInsets insets: UIEdgeInsets,
controller: @escaping () -> ViewController?
) { ) {
self.external = external self.external = external
self.context = context self.context = context
@ -391,6 +394,7 @@ final class StoryContentLiveChatComponent: Component {
self.call = call self.call = call
self.storyPeerId = storyPeerId self.storyPeerId = storyPeerId
self.insets = insets self.insets = insets
self.controller = controller
} }
static func ==(lhs: StoryContentLiveChatComponent, rhs: StoryContentLiveChatComponent) -> Bool { static func ==(lhs: StoryContentLiveChatComponent, rhs: StoryContentLiveChatComponent) -> Bool {
@ -437,6 +441,7 @@ final class StoryContentLiveChatComponent: Component {
private var stateDisposable: Disposable? private var stateDisposable: Disposable?
private var currentListIsEmpty: Bool = true private var currentListIsEmpty: Bool = true
private var isMessageContextMenuOpen: Bool = false
public var isChatEmpty: Bool { public var isChatEmpty: Bool {
guard let messagesState = self.messagesState else { guard let messagesState = self.messagesState else {
@ -446,6 +451,17 @@ final class StoryContentLiveChatComponent: Component {
} }
private(set) var isChatExpanded: Bool = false 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) { override init(frame: CGRect) {
self.listContainer = UIView() self.listContainer = UIView()
@ -506,7 +522,7 @@ final class StoryContentLiveChatComponent: Component {
self.addSubview(self.listShadowView) self.addSubview(self.listShadowView)
self.addSubview(self.listContainer) self.addSubview(self.listContainer)
//self.isChatExpanded = true self.isChatExpanded = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -540,6 +556,60 @@ final class StoryContentLiveChatComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4)) 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) { private func openMessageContextMenu(id: GroupCallMessagesContext.Message.Id, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) {
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
guard let self else { guard let self else {
@ -572,7 +642,52 @@ final class StoryContentLiveChatComponent: Component {
}))) })))
let state = await (component.call.state |> take(1)).get() 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 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 { guard let self else {
return return
@ -583,7 +698,11 @@ final class StoryContentLiveChatComponent: Component {
return return
} }
if let call = component.call as? PresentationGroupCallImpl { 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 { guard let self else {
return return
} }
if let listView = self.list.view { self.isMessageContextMenuOpen = false
let transition: ComponentTransition = .easeInOut(duration: 0.2) if !self.isUpdating {
transition.setAlpha(view: listView, alpha: 1.0) self.state?.updated(transition: .easeInOut(duration: 0.2), isLocal: true)
} }
} }
if let listView = self.list.view {
let transition: ComponentTransition = .easeInOut(duration: 0.2) self.isMessageContextMenuOpen = true
transition.setAlpha(view: listView, alpha: 0.25) 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.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)) 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)) 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 preferHighQuality: Bool
let isEmbeddedInCamera: Bool let isEmbeddedInCamera: Bool
let activateReaction: (UIView, MessageReaction.Reaction) -> Void 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.context = context
self.strings = strings self.strings = strings
self.peer = peer self.peer = peer
@ -55,6 +56,7 @@ final class StoryItemContentComponent: Component {
self.preferHighQuality = preferHighQuality self.preferHighQuality = preferHighQuality
self.isEmbeddedInCamera = isEmbeddedInCamera self.isEmbeddedInCamera = isEmbeddedInCamera
self.activateReaction = activateReaction self.activateReaction = activateReaction
self.controller = controller
} }
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool { 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() { public func toggleLiveChatExpanded() {
guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else { guard let liveChatView = self.liveChat?.view as? StoryContentLiveChatComponent.View else {
return return
@ -852,7 +861,13 @@ final class StoryItemContentComponent: Component {
theme: environment.theme, theme: environment.theme,
call: mediaStreamCall, call: mediaStreamCall,
storyPeerId: component.peer.id, 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: {}, environment: {},
containerSize: availableSize containerSize: availableSize

View File

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

View File

@ -52,6 +52,7 @@ import StoryQualityUpgradeSheetScreen
import AudioWaveform import AudioWaveform
import ChatMessagePaymentAlertController import ChatMessagePaymentAlertController
import ChatSendStarsScreen import ChatSendStarsScreen
import AnimatedTextComponent
private var ObjCKey_DeinitWatcher: Int? private var ObjCKey_DeinitWatcher: Int?
@ -102,6 +103,7 @@ final class StoryItemSetContainerSendMessage {
var currentSpeechHolder: SpeechSynthesizerHolder? var currentSpeechHolder: SpeechSynthesizerHolder?
var currentLiveStreamMessageStars: StarsAmount? var currentLiveStreamMessageStars: StarsAmount?
weak var currentSendStarsUndoController: UndoOverlayController?
private(set) var isMediaRecordingLocked: Bool = false private(set) var isMediaRecordingLocked: Bool = false
var wasRecordingDismissed: Bool = false var wasRecordingDismissed: Bool = false
@ -422,7 +424,7 @@ final class StoryItemSetContainerSendMessage {
return return
} }
self.currentLiveStreamMessageStars = nil self.currentLiveStreamMessageStars = nil
view.state?.updated(transition: .spring(duration: 0.3)) view.state?.updated(transition: .spring(duration: 0.4))
}))) })))
} }
} else { } else {
@ -677,7 +679,7 @@ final class StoryItemSetContainerSendMessage {
self.currentInputMode = .text self.currentInputMode = .text
self.currentLiveStreamMessageStars = nil 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 let controller = component.controller() as? StoryContainerScreen
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
@ -759,7 +761,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) { if hasFirstResponder(view) {
view.endEditing(true) view.endEditing(true)
} else { } 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)) controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
} }
@ -826,7 +828,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) { if hasFirstResponder(view) {
view.endEditing(true) view.endEditing(true)
} else { } 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)) controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
}) })
@ -886,7 +888,7 @@ final class StoryItemSetContainerSendMessage {
if hasFirstResponder(view) { if hasFirstResponder(view) {
view.endEditing(true) view.endEditing(true)
} else { } 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)) controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
}) })
@ -3890,11 +3892,26 @@ final class StoryItemSetContainerSendMessage {
return 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( let initialData = await ChatSendStarsScreen.initialData(
context: component.context, context: component.context,
peerId: peerId, peerId: peerId,
reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id), reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id),
topPeers: [], topPeers: topPeers,
completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in
guard let view, let component = view.component else { guard let view, let component = view.component else {
return return
@ -3916,7 +3933,168 @@ final class StoryItemSetContainerSendMessage {
return 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)
} }
} }