From aa637d96d74c0869c8aa81d1bc87048aa8a12c79 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 28 Oct 2025 12:41:50 +0400 Subject: [PATCH 1/6] Stories --- .../ContextUI/Sources/ContextController.swift | 3 +- .../ContextControllerActionsStackNode.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api15.swift | 14 +- submodules/TelegramApi/Sources/Api39.swift | 23 ++- submodules/TelegramApi/Sources/Api7.swift | 20 +- .../Sources/AccountGroupCallContextImpl.swift | 3 +- .../Sources/PresentationGroupCall.swift | 12 +- .../State/AccountStateManagementUtils.swift | 2 +- .../SyncCore/SyncCore_Namespaces.swift | 1 + .../TelegramEngine/Calls/GroupCalls.swift | 51 +++-- .../TelegramEngine/Messages/SendAsPeers.swift | 111 ++++++++++- .../Peers/TelegramEnginePeers.swift | 4 + submodules/TelegramUI/BUILD | 1 + .../Chat/ChatSendAsContextMenu/BUILD | 29 +++ .../ChatSendAsPeerListContextItem.swift | 10 +- .../ChatSendAsPeerTitleContextItem.swift | 0 .../Sources/ChatTextInputPanelComponent.swift | 52 ++++- .../Sources/MessageInputPanelComponent.swift | 51 ++++- .../Stories/StoryContainerScreen/BUILD | 1 + .../StoryContentLiveChatComponent.swift | 20 +- .../StoryItemSetContainerComponent.swift | 8 +- ...StoryItemSetContainerViewSendMessage.swift | 184 +++++++++++++++++- .../Chat/ChatControllerLoadDisplayNode.swift | 8 +- 24 files changed, 554 insertions(+), 60 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD rename submodules/TelegramUI/{ => Components/Chat/ChatSendAsContextMenu}/Sources/ChatSendAsPeerListContextItem.swift (94%) rename submodules/TelegramUI/{ => Components/Chat/ChatSendAsContextMenu}/Sources/ChatSendAsPeerTitleContextItem.swift (100%) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index a1344158eb..dd6506d277 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -684,11 +684,12 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega self.contentReady.set(.single(true)) let transitionInfo = source.transitionInfo() - if let transitionInfo = transitionInfo { + if let transitionInfo { let referenceView = transitionInfo.referenceView self.contentContainerNode.contentNode = .reference(view: referenceView) self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace self.customPosition = transitionInfo.customPosition + var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) projectedFrame.origin.x += transitionInfo.insets.left projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 728e56f64e..dbf823676a 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -749,7 +749,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C getController: self.getController, actionSelected: { result in switch result { - case .dismissWithoutContent: + case .default, .dismissWithoutContent: self.requestDismiss(result) default: break diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 66a8c7f560..aaff5b68aa 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -301,7 +301,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } dict[-565420653] = { return Api.GeoPointAddress.parse_geoPointAddress($0) } dict[-29248689] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } - dict[-674602536] = { return Api.GroupCall.parse_groupCall($0) } + dict[-273500649] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } dict[-297595771] = { return Api.GroupCallDonor.parse_groupCallDonor($0) } dict[445316222] = { return Api.GroupCallMessage.parse_groupCallMessage($0) } @@ -588,7 +588,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) } - dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[1223234306] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) } dict[-1465661799] = { return Api.MessageAction.parse_messageActionGiftTon($0) } dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index d79ae1726d..53200a29ce 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -1044,7 +1044,7 @@ public extension Api { case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) - case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) + case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, days: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case messageActionGiftTon(flags: Int32, currency: String, amount: Int64, cryptoCurrency: String, cryptoAmount: Int64, transactionId: String?) case messageActionGiveawayLaunch(flags: Int32, stars: Int64?) @@ -1235,14 +1235,14 @@ public extension Api { if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)} break - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): + case .messageActionGiftPremium(let flags, let currency, let amount, let days, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(1818391802) + buffer.appendInt32(1223234306) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) - serializeInt32(months, buffer: buffer, boxed: false) + serializeInt32(days, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} @@ -1627,8 +1627,8 @@ public extension Api { return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): - return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) + case .messageActionGiftPremium(let flags, let currency, let amount, let days, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("days", days as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)]) case .messageActionGiftTon(let flags, let currency, let amount, let cryptoCurrency, let cryptoAmount, let transactionId): @@ -1991,7 +1991,7 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7) + return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, days: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 5212a5e7b9..1874a75d3b 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -10854,6 +10854,22 @@ public extension Api.functions.phone { }) } } +public extension Api.functions.phone { + static func saveDefaultSendAs(call: Api.InputGroupCall, sendAs: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1097313745) + call.serialize(buffer, true) + sendAs.serialize(buffer, true) + return (FunctionDescription(name: "phone.saveDefaultSendAs", parameters: [("call", String(describing: call)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.phone { static func sendConferenceCallBroadcast(call: Api.InputGroupCall, block: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -10887,15 +10903,16 @@ public extension Api.functions.phone { } } public extension Api.functions.phone { - static func sendGroupCallMessage(flags: Int32, call: Api.InputGroupCall, randomId: Int64, message: Api.TextWithEntities, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendGroupCallMessage(flags: Int32, call: Api.InputGroupCall, randomId: Int64, message: Api.TextWithEntities, allowPaidStars: Int64?, sendAs: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(445465039) + buffer.appendInt32(-1311697904) serializeInt32(flags, buffer: buffer, boxed: false) call.serialize(buffer, true) serializeInt64(randomId, buffer: buffer, boxed: false) message.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "phone.sendGroupCallMessage", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("randomId", String(describing: randomId)), ("message", String(describing: message)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 1) != 0 {sendAs!.serialize(buffer, true)} + return (FunctionDescription(name: "phone.sendGroupCallMessage", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("randomId", String(describing: randomId)), ("message", String(describing: message)), ("allowPaidStars", String(describing: allowPaidStars)), ("sendAs", String(describing: sendAs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index d78b77465d..1ad2e7de78 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -1222,14 +1222,14 @@ public extension Api { } public extension Api { enum GroupCall: TypeConstructorDescription { - case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, title: String?, streamDcId: Int32?, recordStartDate: Int32?, scheduleDate: Int32?, unmutedVideoCount: Int32?, unmutedVideoLimit: Int32, version: Int32, inviteLink: String?, sendPaidMessagesStars: Int64?) + case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, title: String?, streamDcId: Int32?, recordStartDate: Int32?, scheduleDate: Int32?, unmutedVideoCount: Int32?, unmutedVideoLimit: Int32, version: Int32, inviteLink: String?, sendPaidMessagesStars: Int64?, defaultSendAs: Api.Peer?) case groupCallDiscarded(id: Int64, accessHash: Int64, duration: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars): + case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars, let defaultSendAs): if boxed { - buffer.appendInt32(-674602536) + buffer.appendInt32(-273500649) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -1244,6 +1244,7 @@ public extension Api { serializeInt32(version, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 16) != 0 {serializeString(inviteLink!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 20) != 0 {serializeInt64(sendPaidMessagesStars!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 21) != 0 {defaultSendAs!.serialize(buffer, true)} break case .groupCallDiscarded(let id, let accessHash, let duration): if boxed { @@ -1258,8 +1259,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars): - return ("groupCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("participantsCount", participantsCount as Any), ("title", title as Any), ("streamDcId", streamDcId as Any), ("recordStartDate", recordStartDate as Any), ("scheduleDate", scheduleDate as Any), ("unmutedVideoCount", unmutedVideoCount as Any), ("unmutedVideoLimit", unmutedVideoLimit as Any), ("version", version as Any), ("inviteLink", inviteLink as Any), ("sendPaidMessagesStars", sendPaidMessagesStars as Any)]) + case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version, let inviteLink, let sendPaidMessagesStars, let defaultSendAs): + return ("groupCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("participantsCount", participantsCount as Any), ("title", title as Any), ("streamDcId", streamDcId as Any), ("recordStartDate", recordStartDate as Any), ("scheduleDate", scheduleDate as Any), ("unmutedVideoCount", unmutedVideoCount as Any), ("unmutedVideoLimit", unmutedVideoLimit as Any), ("version", version as Any), ("inviteLink", inviteLink as Any), ("sendPaidMessagesStars", sendPaidMessagesStars as Any), ("defaultSendAs", defaultSendAs as Any)]) case .groupCallDiscarded(let id, let accessHash, let duration): return ("groupCallDiscarded", [("id", id as Any), ("accessHash", accessHash as Any), ("duration", duration as Any)]) } @@ -1292,6 +1293,10 @@ public extension Api { if Int(_1!) & Int(1 << 16) != 0 {_12 = parseString(reader) } var _13: Int64? if Int(_1!) & Int(1 << 20) != 0 {_13 = reader.readInt64() } + var _14: Api.Peer? + if Int(_1!) & Int(1 << 21) != 0 {if let signature = reader.readInt32() { + _14 = Api.parse(reader, signature: signature) as? Api.Peer + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1305,8 +1310,9 @@ public extension Api { let _c11 = _11 != nil let _c12 = (Int(_1!) & Int(1 << 16) == 0) || _12 != nil let _c13 = (Int(_1!) & Int(1 << 20) == 0) || _13 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { - return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!, inviteLink: _12, sendPaidMessagesStars: _13) + let _c14 = (Int(_1!) & Int(1 << 21) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!, inviteLink: _12, sendPaidMessagesStars: _13, defaultSendAs: _14) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift index bbf2e28b75..5b348c93da 100644 --- a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift +++ b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift @@ -100,7 +100,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: state.isStream, - isCreator: state.isCreator + isCreator: state.isCreator, + defaultSendAs: state.defaultSendAs ), topParticipants: topParticipants, participantCount: state.totalCount, diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 8860268561..760f476a6d 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1689,7 +1689,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: callInfo.isVideoEnabled, unmutedVideoLimit: callInfo.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )), audioSessionControl: self.audioSessionControl) } else { self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( @@ -1707,7 +1708,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )))) self.summaryParticipantsState.set(.single(SummaryParticipantsState( @@ -1787,7 +1789,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: false, unmutedVideoLimit: 0, isStream: true, - isCreator: false + isCreator: false, + defaultSendAs: nil ) } else { activeCallInfo = nil @@ -2691,7 +2694,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit, isStream: callInfo.isStream, - isCreator: callInfo.isCreator + isCreator: callInfo.isCreator, + defaultSendAs: callInfo.defaultSendAs )))) self.summaryParticipantsState.set(.single(SummaryParticipantsState( diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 04e469776d..ceec57cc75 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4906,7 +4906,7 @@ func replayFinalState( } switch call { - case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _, _, sendPaidMessagesStars): + case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _, _, sendPaidMessagesStars, _): let isMin = (flags & (1 << 19)) != 0 let isMuted = (flags & (1 << 1)) != 0 let canChange = (flags & (1 << 2)) != 0 diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d9284622d8..c81fbbf369 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -147,6 +147,7 @@ public struct Namespaces { public static let cachedProfileGiftsCollections: Int8 = 48 public static let cachedProfileSavedMusic: Int8 = 49 public static let cachedChatThemes: Int8 = 50 + public static let cachedLiveStorySendAsPeers: Int8 = 51 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 06bd09fe43..234cebeac0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -99,6 +99,7 @@ public struct GroupCallInfo: Equatable { public var unmutedVideoLimit: Int public var isStream: Bool public var isCreator: Bool + public var defaultSendAs: EnginePeer.Id? public init( id: Int64, @@ -115,7 +116,8 @@ public struct GroupCallInfo: Equatable { isVideoEnabled: Bool, unmutedVideoLimit: Int, isStream: Bool, - isCreator: Bool + isCreator: Bool, + defaultSendAs: EnginePeer.Id? ) { self.id = id self.accessHash = accessHash @@ -132,6 +134,7 @@ public struct GroupCallInfo: Equatable { self.unmutedVideoLimit = unmutedVideoLimit self.isStream = isStream self.isCreator = isCreator + self.defaultSendAs = defaultSendAs } } @@ -143,7 +146,7 @@ public struct GroupCallSummary: Equatable { extension GroupCallInfo { init?(_ call: Api.GroupCall) { switch call { - case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars): + case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, defaultSendAs): self.init( id: id, accessHash: accessHash, @@ -159,7 +162,8 @@ extension GroupCallInfo { isVideoEnabled: (flags & (1 << 9)) != 0, unmutedVideoLimit: Int(unmutedVideoLimit), isStream: (flags & (1 << 12)) != 0, - isCreator: (flags & (1 << 15)) != 0 + isCreator: (flags & (1 << 15)) != 0, + defaultSendAs: defaultSendAs?.peerId ) case .groupCallDiscarded: return nil @@ -773,7 +777,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, maybeParsedCall = GroupCallInfo(call) switch call { - case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars): + case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _, _, sendPaidMessagesStars, defaultSendAs): let isMin = (flags & (1 << 19)) != 0 let isMuted = (flags & (1 << 1)) != 0 let canChange = (flags & (1 << 2)) != 0 @@ -787,6 +791,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, state.scheduleTimestamp = scheduleDate state.isVideoEnabled = isMin ? state.isVideoEnabled : isVideoEnabled state.unmutedVideoLimit = Int(unmutedVideoLimit) + state.defaultSendAs = defaultSendAs?.peerId default: break } @@ -1429,6 +1434,7 @@ public final class GroupCallParticipantsContext { public var unmutedVideoLimit: Int public var isStream: Bool public var sendPaidMessagesStars: Int64? + public var defaultSendAs: PeerId? public var version: Int32 public mutating func mergeActivity(from other: State, myPeerId: PeerId?, previousMyPeerId: PeerId?, mergeActivityTimestamps: Bool) { @@ -3836,7 +3842,7 @@ public final class GroupCallMessagesContext { private var messageLifeTimer: SwiftSignalKit.Timer? - private var pendingSendStars: (fromId: PeerId, messageId: Int64, amount: Int64)? + private var pendingSendStars: (fromPeer: Peer, messageId: Int64, amount: Int64)? private var pendingSendStarsTimer: SwiftSignalKit.Timer? init(queue: Queue, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { @@ -4269,6 +4275,16 @@ public final class GroupCallMessagesContext { if paidStars != nil { flags |= 1 << 0 } + var sendAs: Api.InputPeer? + if fromId != self.account.peerId { + guard let fromPeer else { + return + } + sendAs = apiInputPeer(fromPeer) + } + if sendAs != nil { + flags |= 1 << 1 + } self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( flags: flags, call: self.reference.apiInputGroupCall, @@ -4277,7 +4293,8 @@ public final class GroupCallMessagesContext { text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()) ), - allowPaidStars: paidStars + allowPaidStars: paidStars, + sendAs: sendAs )) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in guard let self else { return @@ -4322,6 +4339,13 @@ public final class GroupCallMessagesContext { var flags: Int32 = 0 flags |= 1 << 0 + var sendAs: Api.InputPeer? + if pendingSendStars.fromPeer.id != self.account.peerId { + sendAs = apiInputPeer(pendingSendStars.fromPeer) + } + if sendAs != nil { + flags |= 1 << 1 + } self.sendMessageDisposables.add((self.account.network.request(Api.functions.phone.sendGroupCallMessage( flags: flags, call: self.reference.apiInputGroupCall, @@ -4330,7 +4354,8 @@ public final class GroupCallMessagesContext { text: "", entities: [] ), - allowPaidStars: pendingSendStars.amount + allowPaidStars: pendingSendStars.amount, + sendAs: sendAs )) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in guard let self else { return @@ -4347,7 +4372,7 @@ public final class GroupCallMessagesContext { 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) + Impl.addStateStars(state: &state, peerId: pendingSendStars.fromPeer.id, isMy: true, amount: pendingSendStars.amount) state.pendingMyStars = 0 self.state = state break @@ -4384,7 +4409,7 @@ public final class GroupCallMessagesContext { return transaction.getPeer(fromId) } |> deliverOn(self.queue)).startStandalone(next: { [weak self] fromPeer in - guard let self else { + guard let self, let fromPeer else { return } @@ -4395,7 +4420,7 @@ public final class GroupCallMessagesContext { totalAmount = pendingSendStarsValue.amount + amount self.pendingSendStars = ( - fromId: fromId, + fromPeer: fromPeer, messageId: pendingSendStarsValue.messageId, amount: totalAmount ) @@ -4406,7 +4431,7 @@ public final class GroupCallMessagesContext { arc4random_buf(&randomId, 8) self.pendingSendStars = ( - fromId: fromId, + fromPeer: fromPeer, messageId: randomId, amount: amount ) @@ -4437,7 +4462,7 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, - author: fromPeer.flatMap(EnginePeer.init), + author: EnginePeer(fromPeer), text: "", entities: [], date: currentTime, @@ -4464,7 +4489,7 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, - author: fromPeer.flatMap(EnginePeer.init), + author: EnginePeer(fromPeer), text: "", entities: [], date: currentTime, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 63405e9f01..0f3320a497 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -96,7 +96,6 @@ func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId) } } - func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { return postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) @@ -216,3 +215,113 @@ func _internal_updatePeerSendAsPeer(account: Account, peerId: PeerId, sendAs: Pe } } } + +func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return account.postbox.transaction { transaction -> ([SendAsPeer], Int32)? in + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key))?.get(CachedSendAsPeers.self) + if let cached = cached { + var peers: [SendAsPeer] = [] + for peerId in cached.peerIds { + if let peer = transaction.getPeer(peerId) { + var subscribers: Int32? + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + subscribers = cachedData.participantsSummary.memberCount + } + peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) + } + } + return (peers, cached.timestamp) + } else { + return nil + } + } + |> mapToSignal { cachedPeersAndTimestamp -> Signal<[SendAsPeer], NoError> in + let initialSignal: Signal<[SendAsPeer], NoError> + if let (cachedPeers, _) = cachedPeersAndTimestamp { + initialSignal = .single(cachedPeers) + } else { + initialSignal = .complete() + } + return initialSignal + |> then(_internal_liveStorySendAsAvailablePeers(account: account, peerId: peerId) + |> mapToSignal { peers -> Signal<[SendAsPeer], NoError> in + return account.postbox.transaction { transaction -> [SendAsPeer] in + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var premiumRequiredPeerIds = Set() + for peer in peers { + if peer.isPremiumRequired { + premiumRequiredPeerIds.insert(peer.peer.id) + } + } + if let entry = CodableEntry(CachedSendAsPeers(peerIds: peers.map { $0.peer.id }, premiumRequiredPeerIds: premiumRequiredPeerIds, timestamp: currentTimestamp)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedLiveStorySendAsPeers, key: key), entry: entry) + } + return peers + } + }) + } +} + +func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal<[SendAsPeer], NoError> in + guard let peer else { + return .single([]) + } + guard let inputPeer = apiInputPeer(peer) else { + return .single([]) + } + + return account.network.request(Api.functions.channels.getSendAs(flags: 1 << 1, peer: inputPeer)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[SendAsPeer], NoError> in + guard let result = result else { + return .single([]) + } + switch result { + case let .sendAsPeers(sendAsPeers, chats, _): + return account.postbox.transaction { transaction -> [SendAsPeer] in + var subscribers: [PeerId: Int32] = [:] + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: []) + + var premiumRequiredPeerIds = Set() + for sendAsPeer in sendAsPeers { + if case let .sendAsPeer(flags, peer) = sendAsPeer, (flags & (1 << 0)) != 0 { + premiumRequiredPeerIds.insert(peer.peerId) + } + } + for chat in chats { + if let groupOrChannel = parsedPeers.get(chat.peerId) { + switch chat { + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _, _, _, _): + if let participantsCount = participantsCount { + subscribers[groupOrChannel.id] = participantsCount + } + case let .chat(_, _, _, _, participantsCount, _, _, _, _, _): + subscribers[groupOrChannel.id] = participantsCount + default: + break + } + } + } + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + + var peers: [Peer] = [] + for chat in chats { + if let peer = transaction.getPeer(chat.peerId) { + peers.append(peer) + } + } + return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } + } + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 064db9b593..2c246af27d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1175,6 +1175,10 @@ public extension TelegramEngine { return _internal_updatePeerSendAsPeer(account: self.account, peerId: peerId, sendAs: sendAs) } + public func liveStorySendAsAvailablePeers(peerId: PeerId) -> Signal<[SendAsPeer], NoError> { + return _internal_cachedLiveStorySendAsAvailablePeers(account: self.account, peerId: peerId) + } + public func updatePeerReactionSettings(peerId: PeerId, reactionSettings: PeerReactionSettings) -> Signal { return _internal_updatePeerReactionSettings(account: account, peerId: peerId, reactionSettings: reactionSettings) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ec8fff1b06..601bf2830e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -495,6 +495,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/AttachmentFileController", "//submodules/TelegramUI/Components/Contacts/NewContactScreen", + "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD new file mode 100644 index 0000000000..1ade4c0550 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatSendAsContextMenu", + module_name = "ChatSendAsContextMenu", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/TelegramStringFormatting", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift similarity index 94% rename from submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift rename to submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index 0dc33acc28..a6a877ccea 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -13,24 +13,26 @@ import AvatarNode import AccountContext import UndoUI -final class ChatSendAsPeerListContextItem: ContextMenuCustomItem { +public final class ChatSendAsPeerListContextItem: ContextMenuCustomItem { let context: AccountContext let chatPeerId: PeerId let peers: [SendAsPeer] let selectedPeerId: PeerId? let isPremium: Bool + let action: (EnginePeer) -> Void let presentToast: (EnginePeer) -> Void - init(context: AccountContext, chatPeerId: PeerId, peers: [SendAsPeer], selectedPeerId: PeerId?, isPremium: Bool, presentToast: @escaping (EnginePeer) -> Void) { + public init(context: AccountContext, chatPeerId: PeerId, peers: [SendAsPeer], selectedPeerId: PeerId?, isPremium: Bool, action: @escaping (EnginePeer) -> Void, presentToast: @escaping (EnginePeer) -> Void) { self.context = context self.chatPeerId = chatPeerId self.peers = peers self.selectedPeerId = selectedPeerId self.isPremium = isPremium + self.action = action self.presentToast = presentToast } - func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return ChatSendAsPeerListContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -115,7 +117,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen } if peer.peer.id != item.selectedPeerId { - let _ = item.context.engine.peers.updatePeerSendAsPeer(peerId: item.chatPeerId, sendAs: peer.peer.id).startStandalone() + item.action(EnginePeer(peer.peer)) } }) let actionNode = ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: {}, requestUpdateAction: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerTitleContextItem.swift similarity index 100% rename from submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift rename to submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerTitleContextItem.swift diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index a17ba719e3..fc624f6476 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -103,6 +103,38 @@ public final class ChatTextInputPanelComponent: Component { } } + public final class SendAsConfiguration: Equatable { + public let currentPeer: EnginePeer + public let subscriberCount: Int? + public let isPremiumLocked: Bool + public let isSelecting: Bool + public let action: (UIView, ContextGesture?) -> Void + + public init(currentPeer: EnginePeer, subscriberCount: Int?, isPremiumLocked: Bool, isSelecting: Bool, action: @escaping (UIView, ContextGesture?) -> Void) { + self.currentPeer = currentPeer + self.subscriberCount = subscriberCount + self.isPremiumLocked = isPremiumLocked + self.isSelecting = isSelecting + self.action = action + } + + public static func ==(lhs: SendAsConfiguration, rhs: SendAsConfiguration) -> Bool { + if lhs.currentPeer != rhs.currentPeer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } + if lhs.isSelecting != rhs.isSelecting { + return false + } + return true + } + } + let externalState: ExternalState let context: AccountContext let theme: PresentationTheme @@ -111,6 +143,7 @@ public final class ChatTextInputPanelComponent: Component { let inlineActions: [InlineAction] let leftAction: LeftAction? let rightAction: RightAction? + let sendAsConfiguration: SendAsConfiguration? let placeholder: String let paidMessagePrice: StarsAmount? let sendColor: UIColor? @@ -131,6 +164,7 @@ public final class ChatTextInputPanelComponent: Component { inlineActions: [InlineAction], leftAction: LeftAction?, rightAction: RightAction?, + sendAsConfiguration: SendAsConfiguration?, placeholder: String, paidMessagePrice: StarsAmount?, sendColor: UIColor?, @@ -150,6 +184,7 @@ public final class ChatTextInputPanelComponent: Component { self.inlineActions = inlineActions self.leftAction = leftAction self.rightAction = rightAction + self.sendAsConfiguration = sendAsConfiguration self.placeholder = placeholder self.paidMessagePrice = paidMessagePrice self.sendColor = sendColor @@ -187,6 +222,9 @@ public final class ChatTextInputPanelComponent: Component { if lhs.rightAction != rhs.rightAction { return false } + if lhs.sendAsConfiguration != rhs.sendAsConfiguration { + return false + } if lhs.placeholder != rhs.placeholder { return false } @@ -582,7 +620,11 @@ public final class ChatTextInputPanelComponent: Component { }, openInviteRequests: { }, - openSendAsPeer: { _, _ in + openSendAsPeer: { [weak self] sourceNode, gesture in + guard let self, let component = self.component, let sendAsConfiguration = component.sendAsConfiguration else { + return + } + sendAsConfiguration.action(sourceNode.view, gesture) }, presentChatRequestAdminInfo: { }, @@ -722,6 +764,14 @@ public final class ChatTextInputPanelComponent: Component { } presentationInterfaceState = presentationInterfaceState.updatedSendPaidMessageStars(component.paidMessagePrice) + if let sendAsConfiguration = component.sendAsConfiguration { + presentationInterfaceState = presentationInterfaceState.updatedSendAsPeers([SendAsPeer( + peer: sendAsConfiguration.currentPeer._asPeer(), + subscribers: sendAsConfiguration.subscriberCount.flatMap(Int32.init(clamping:)), + isPremiumRequired: sendAsConfiguration.isPremiumLocked + )]).updatedShowSendAsPeers(sendAsConfiguration.isSelecting).updatedCurrentSendAsPeerId(sendAsConfiguration.currentPeer.id) + } + let panelNode: ChatTextInputPanelNode if let current = self.panelNode { panelNode = current diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 7b491e959d..ec7b716d98 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -192,6 +192,38 @@ public final class MessageInputPanelComponent: Component { } } + public final class SendAsConfiguration: Equatable { + public let currentPeer: EnginePeer + public let subscriberCount: Int? + public let isPremiumLocked: Bool + public let isSelecting: Bool + public let action: (UIView, ContextGesture?) -> Void + + public init(currentPeer: EnginePeer, subscriberCount: Int?, isPremiumLocked: Bool, isSelecting: Bool, action: @escaping (UIView, ContextGesture?) -> Void) { + self.currentPeer = currentPeer + self.subscriberCount = subscriberCount + self.isPremiumLocked = isPremiumLocked + self.isSelecting = isSelecting + self.action = action + } + + public static func ==(lhs: SendAsConfiguration, rhs: SendAsConfiguration) -> Bool { + if lhs.currentPeer != rhs.currentPeer { + return false + } + if lhs.subscriberCount != rhs.subscriberCount { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } + if lhs.isSelecting != rhs.isSelecting { + return false + } + return true + } + } + public let externalState: ExternalState public let context: AccountContext public let theme: PresentationTheme @@ -255,6 +287,7 @@ public final class MessageInputPanelComponent: Component { public let toggleLiveChatExpanded: (() -> Void)? public let sendStarsAction: ((UIView, Bool) -> Void)? public let starStars: StarStats? + public let sendAsConfiguration: SendAsConfiguration? public init( externalState: ExternalState, @@ -319,7 +352,8 @@ public final class MessageInputPanelComponent: Component { liveChatState: LiveChatState? = nil, toggleLiveChatExpanded: (() -> Void)? = nil, sendStarsAction: ((UIView, Bool) -> Void)? = nil, - starStars: StarStats? = nil + starStars: StarStats? = nil, + sendAsConfiguration: SendAsConfiguration? = nil ) { self.externalState = externalState self.context = context @@ -384,6 +418,7 @@ public final class MessageInputPanelComponent: Component { self.toggleLiveChatExpanded = toggleLiveChatExpanded self.sendStarsAction = sendStarsAction self.starStars = starStars + self.sendAsConfiguration = sendAsConfiguration } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -519,6 +554,9 @@ public final class MessageInputPanelComponent: Component { if lhs.starStars != rhs.starStars { return false } + if lhs.sendAsConfiguration != rhs.sendAsConfiguration { + return false + } return true } @@ -968,6 +1006,16 @@ public final class MessageInputPanelComponent: Component { } } + let sendAsConfiguration = component.sendAsConfiguration.flatMap { value in + return ChatTextInputPanelComponent.SendAsConfiguration( + currentPeer: value.currentPeer, + subscriberCount: value.subscriberCount, + isPremiumLocked: value.isPremiumLocked, + isSelecting: value.isSelecting, + action: value.action + ) + } + let inputPanelSize = inputPanel.update( transition: transition, component: AnyComponent(ChatTextInputPanelComponent( @@ -994,6 +1042,7 @@ public final class MessageInputPanelComponent: Component { } component.sendStarsAction?(sourceView, true) }), + sendAsConfiguration: sendAsConfiguration, placeholder: placeholder, paidMessagePrice: component.sendPaidMessageStars, sendColor: component.sendPaidMessageStars.flatMap { value in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 7333c3e16d..826101a1ff 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -110,6 +110,7 @@ swift_library( "//submodules/TelegramUI/Components/StarsParticleEffect", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AdminUserActionsSheet", + "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index 1218816e63..ff37e7c9f0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -302,6 +302,7 @@ private final class PinnedBarComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state @@ -325,8 +326,21 @@ private final class PinnedBarComponent: Component { let listInsets = UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right) let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + + var listTransition = transition + var animateIn = false + if let previousComponent { + if previousComponent.messages.isEmpty { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + } else { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + let _ = self.list.update( - transition: transition, + transition: listTransition, component: AnyComponent(AsyncListComponent( externalState: self.listState, items: listItems, @@ -343,6 +357,10 @@ private final class PinnedBarComponent: Component { } transition.setPosition(view: listView, position: CGRect(origin: CGPoint(), size: listFrame.size).center) transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) + + if animateIn { + transition.animateAlpha(view: listView, from: 0.0, to: 1.0) + } } transition.setFrame(view: self.listContainer, frame: listFrame) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index aed4b736a1..295eed8349 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2628,7 +2628,7 @@ public final class StoryItemSetContainerComponent: Component { let isFirstTime = self.component == nil if self.component == nil { - self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) + self.sendMessageContext.setup(component: component, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) /*#if DEBUG class Target: NSObject { @@ -2963,6 +2963,7 @@ public final class StoryItemSetContainerComponent: Component { var liveChatState: MessageInputPanelComponent.LiveChatState? var starStats: MessageInputPanelComponent.StarStats? + var sendAsConfiguration: MessageInputPanelComponent.SendAsConfiguration? var sendPaidMessageStars = isLiveStream ? self.sendMessageContext.currentLiveStreamMessageStars : component.slice.additionalPeerData.sendPaidMessageStars if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let liveChatStateValue = visibleItemView.liveChatState { @@ -2985,6 +2986,8 @@ public final class StoryItemSetContainerComponent: Component { sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0) } } + + sendAsConfiguration = self.sendMessageContext.currentSendAsConfiguration } } @@ -3244,7 +3247,8 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.performSendStars(view: self, buttonView: sourceView, count: 1, isFromExpandedView: false) } } : nil, - starStars: starStats + starStars: starStats, + sendAsConfiguration: sendAsConfiguration )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 0ced021abd..9bc37ea475 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -37,7 +37,6 @@ import TextFieldComponent import StickerPackPreviewUI import OpenInExternalAppUI import SafariServices -//import MediaPasteboardUI import WebPBinding import ContextUI import ChatScheduleTimeController @@ -53,6 +52,7 @@ import AudioWaveform import ChatMessagePaymentAlertController import ChatSendStarsScreen import AnimatedTextComponent +import ChatSendAsContextMenu private var ObjCKey_DeinitWatcher: Int? @@ -105,6 +105,11 @@ final class StoryItemSetContainerSendMessage { var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? + var sendAsData: (isPremium: Bool, availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)])? + var currentSendAsConfiguration: MessageInputPanelComponent.SendAsConfiguration? + var sendAsContextPeerId: EnginePeer.Id? + var sendAsDisposable: Disposable? + private(set) var isMediaRecordingLocked: Bool = false var wasRecordingDismissed: Bool = false @@ -118,10 +123,11 @@ final class StoryItemSetContainerSendMessage { self.resolvePeerByNameDisposable.dispose() self.inputMediaNodeDataDisposable?.dispose() self.currentTooltipUpdateTimer?.invalidate() + self.sendAsDisposable?.dispose() } - func setup(context: AccountContext, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { - self.context = context + func setup(component: StoryItemSetContainerComponent, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { + self.context = component.context self.inputPanelExternalState = inputPanelExternalState self.view = view @@ -208,6 +214,73 @@ final class StoryItemSetContainerSendMessage { } ) self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme + + if let peerId = component.slice.item.peerId { + self.sendAsDisposable = combineLatest( + queue: Queue.mainQueue(), + component.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + ), + component.context.engine.peers.liveStorySendAsAvailablePeers(peerId: peerId) + ).startStrict(next: { [weak self, weak view] accountPeer, peers in + guard let self, let view, let accountPeer else { + return + } + + let isPremium = accountPeer.isPremium + + var availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)] = [] + availablePeers.append(( + peer: accountPeer, + subscriberCount: nil, + isPremiumRequired: false + )) + for peer in peers { + if peer.peer.id == accountPeer.id { + continue + } + availablePeers.append(( + peer: EnginePeer(peer.peer), + subscriberCount: peer.subscribers.flatMap(Int.init), + isPremiumRequired: peer.isPremiumRequired + )) + } + + self.sendAsData = ( + isPremium: isPremium, + availablePeers: availablePeers + ) + + //TODO:localize + if "".isEmpty { + let sendAsConfiguration = MessageInputPanelComponent.SendAsConfiguration( + currentPeer: accountPeer, + subscriberCount: nil, + isPremiumLocked: false, + isSelecting: false, + action: { [weak self, weak view] sourceView, gesture in + guard let self, let view else { + return + } + self.openSendAsSelection(view: view, sourceView: sourceView, gesture: gesture) + } + ) + if self.currentSendAsConfiguration != sendAsConfiguration { + self.currentSendAsConfiguration = sendAsConfiguration + if !view.isUpdatingComponent { + view.state?.updated(transition: .spring(duration: 0.4)) + } + } + } else { + if self.currentSendAsConfiguration != nil { + self.currentSendAsConfiguration = nil + if !view.isUpdatingComponent { + view.state?.updated(transition: .spring(duration: 0.4)) + } + } + } + }) + } } func toggleInputMode() { @@ -281,7 +354,7 @@ final class StoryItemSetContainerSendMessage { self.inputMediaNode = inputMediaNode } - let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, @@ -3954,11 +4027,11 @@ final class StoryItemSetContainerSendMessage { peerId: peerId, reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount)), topPeers: topPeers, - completion: { [weak view] amount, privacy, isBecomingTop, transitionOut in - guard let view, let component = view.component else { + completion: { [weak self, weak view] amount, _, _, _ in + guard let self, let view else { return } - let _ = component.context.engine.messages.sendStoryStars(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, count: Int(amount)).startStandalone() + self.performSendStars(view: view, buttonView: nil, count: Int(amount), isFromExpandedView: true) }).get() if let initialData { controller.push(ChatSendStarsScreen( @@ -3970,12 +4043,17 @@ final class StoryItemSetContainerSendMessage { } } - func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView, count: Int, isFromExpandedView: Bool) { + func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView?, count: Int, isFromExpandedView: Bool) { guard let component = view.component else { return } if isFromExpandedView { + if let currentSendStarsUndoController = self.currentSendStarsUndoController { + self.currentSendStarsUndoController = nil + currentSendStarsUndoController.dismiss() + } + self.commitSendStars(view: view, count: count, delay: false) } else { Task { @MainActor [weak view] in @@ -4009,7 +4087,7 @@ final class StoryItemSetContainerSendMessage { } } - if let reactionItem { + if let reactionItem, let buttonView { let targetFrame = buttonView.convert(buttonView.bounds, to: view) let targetView = UIView(frame: targetFrame) @@ -4145,6 +4223,94 @@ final class StoryItemSetContainerSendMessage { call.sendStars(amount: Int64(count), delay: delay) } + + private func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { + guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsConfiguration = self.currentSendAsConfiguration, let controller = component.controller() else { + return + } + + let focusedItem = component.slice.item + guard let peerId = focusedItem.peerId else { + return + } + let isPremium = sendAsData.isPremium + + let mappedPeers = sendAsData.availablePeers.map { peer in + return SendAsPeer( + peer: peer.peer._asPeer(), + subscribers: peer.subscriberCount.flatMap(Int32.init(clamping:)), + isPremiumRequired: peer.isPremiumRequired + ) + } + + var items: [ContextMenuItem] = [] + items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) + items.append(.custom(ChatSendAsPeerListContextItem( + context: component.context, + chatPeerId: peerId, + peers: mappedPeers, + selectedPeerId: currentSendAsConfiguration.currentPeer.id, + isPremium: isPremium, + action: { [weak self] peer in + guard let self else { + return + } + let _ = self + }, + presentToast: { [weak view] peer in + guard let view, let component = view.component, let controller = component.controller() else { + return + } + HapticFeedback().impact() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + controller.present(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak view] action in + guard let view, let component = view.component, let controller = component.controller() else { + return true + } + if case .undo = action { + view.endEditing(true) + + let introScreen = PremiumIntroScreen(context: component.context, source: .settings) + controller.push(introScreen) + } + return true + }), in: .current) + }), + false + )) + + final class ReferenceSource: ContextReferenceContentSource { + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets + + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + self.controller = controller + self.sourceView = sourceView + self.insets = insets + self.contentInsets = contentInsets + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets, actionsPosition: .top) + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let contextController = ContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) + contextController.dismissed = { [weak self, weak view] in + guard let self, let view else { + return + } + let _ = self + view.state?.updated(transition: .spring(duration: 0.4)) + } + controller.presentInGlobalOverlay(contextController) + + view.state?.updated(transition: .spring(duration: 0.4)) + } } public class StoryProgressPauseContext { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index be52c49e1d..b90870ac87 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -123,6 +123,7 @@ import ChatMediaInputStickerGridItem import AdsInfoScreen import PostSuggestionsSettingsScreen import ChatSendStarsScreen +import ChatSendAsContextMenu extension ChatControllerImpl { func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((ContainedViewLayoutTransition?) -> Void) -> Void) { @@ -3896,7 +3897,12 @@ extension ChatControllerImpl { var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) - items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, presentToast: { [weak self] peer in + items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, action: { [weak self] peer in + guard let self else { + return + } + let _ = self.context.engine.peers.updatePeerSendAsPeer(peerId: peerId, sendAs: peer.id).startStandalone() + }, presentToast: { [weak self] peer in if let strongSelf = self { HapticFeedback().impact() From aa59524a0917bb59fc638449673a4d0c17741113 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 28 Oct 2025 15:44:44 +0400 Subject: [PATCH 2/6] Stories --- .../Sources/PresentationGroupCall.swift | 8 +- .../Sources/VideoChatScreen.swift | 4 +- .../TelegramEngine/Calls/GroupCalls.swift | 15 +- .../Sources/ChatSendStarsScreen.swift | 56 ++--- .../Chat/ChatTextInputPanelNode/BUILD | 1 + .../Sources/StarReactionButtonComponent.swift | 217 ++++++++++++++---- .../Sources/ActionPanelComponent.swift | 8 +- .../Stories/StoryContainerScreen/BUILD | 1 + .../Sources/LiveChatReactionStreamView.swift | 197 ++++++++++++++++ .../StoryContentLiveChatComponent.swift | 40 +++- .../StoryItemSetContainerComponent.swift | 15 +- ...StoryItemSetContainerViewSendMessage.swift | 75 +++--- 12 files changed, 491 insertions(+), 146 deletions(-) create mode 100644 submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 760f476a6d..abd952c3f9 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -4060,15 +4060,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) } - public func sendMessage(randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) { + public func sendMessage(fromId: PeerId?, randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) { if let messagesContext = self.messagesContext { - messagesContext.send(fromId: self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars) + messagesContext.send(fromId: fromId ?? self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars) } } - public func sendStars(amount: Int64, delay: Bool) { + public func sendStars(fromId: PeerId?, amount: Int64, delay: Bool) { if let messagesContext = self.messagesContext { - messagesContext.sendStars(fromId: self.joinAsPeerId, amount: amount, delay: delay) + messagesContext.sendStars(fromId: fromId ?? self.joinAsPeerId, amount: amount, delay: delay) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 3d37f0610e..58bfb14a9e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1416,7 +1416,7 @@ final class VideoChatScreenComponent: Component { return } let entities = generateTextEntities(text.string, enabledTypes: [.mention, .hashtag], currentEntities: generateChatInputTextEntities(text)) - call.sendMessage(randomId: randomId, text: text.string, entities: entities, paidStars: nil) + call.sendMessage(fromId: nil, randomId: randomId, text: text.string, entities: entities, paidStars: nil) } inputPanelView.clearSendMessageInput(updateState: true) @@ -3836,7 +3836,7 @@ final class VideoChatScreenComponent: Component { guard case let .group(groupCall) = self.currentCall, let call = groupCall as? PresentationGroupCallImpl else { return } - call.sendMessage(text: text, entities: entities, paidStars: nil) + call.sendMessage(fromId: nil, text: text, entities: entities, paidStars: nil) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 234cebeac0..bf73c77d07 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -3699,6 +3699,7 @@ public final class GroupCallMessagesContext { public let id: Id public let stableId: Int + public let isIncoming: Bool public let author: EnginePeer? public let text: String public let entities: [MessageTextEntity] @@ -3706,9 +3707,10 @@ public final class GroupCallMessagesContext { public let lifetime: Int32 public let paidStars: Int64? - public init(id: Id, stableId: Int, author: EnginePeer?, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) { + public init(id: Id, stableId: Int, isIncoming: Bool, author: EnginePeer?, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) { self.id = id self.stableId = stableId + self.isIncoming = isIncoming self.author = author self.text = text self.entities = entities @@ -3721,6 +3723,7 @@ public final class GroupCallMessagesContext { return Message( id: id, stableId: self.stableId, + isIncoming: self.isIncoming, author: self.author, text: self.text, entities: self.entities, @@ -3740,6 +3743,9 @@ public final class GroupCallMessagesContext { if lhs.stableId != rhs.stableId { return false } + if lhs.isIncoming != rhs.isIncoming { + return false + } if lhs.author != rhs.author { return false } @@ -3921,6 +3927,7 @@ public final class GroupCallMessagesContext { messages.append(Message( id: Message.Id(space: .remote, id: randomId), stableId: allocatedStableIds[messages.count], + isIncoming: addedOpaqueMessage.authorId != accountPeerId, author: transaction.getPeer(addedOpaqueMessage.authorId).flatMap(EnginePeer.init), text: text, entities: entities, @@ -3950,6 +3957,7 @@ public final class GroupCallMessagesContext { messages.append(Message( id: Message.Id(space: .remote, id: Int64(addedMessage.messageId)), stableId: allocatedStableIds[messages.count], + isIncoming: addedMessage.authorId != accountPeerId, author: transaction.getPeer(addedMessage.authorId).flatMap(EnginePeer.init), text: addedMessage.text, entities: addedMessage.entities, @@ -4231,6 +4239,7 @@ public final class GroupCallMessagesContext { let message = Message( id: Message.Id(space: .local, id: randomId), stableId: stableId, + isIncoming: false, author: fromPeer.flatMap(EnginePeer.init), text: text, entities: entities, @@ -4449,6 +4458,7 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: message.id, stableId: message.stableId, + isIncoming: false, author: message.author, text: message.text, entities: message.entities, @@ -4462,6 +4472,7 @@ public final class GroupCallMessagesContext { state.messages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, + isIncoming: false, author: EnginePeer(fromPeer), text: "", entities: [], @@ -4476,6 +4487,7 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: message.id, stableId: message.stableId, + isIncoming: message.isIncoming, author: message.author, text: message.text, entities: message.entities, @@ -4489,6 +4501,7 @@ public final class GroupCallMessagesContext { state.pinnedMessages.append(Message( id: Message.Id(space: .local, id: pendingSendStarsValue.messageId), stableId: stableId, + isIncoming: false, author: EnginePeer(fromPeer), text: "", entities: [], diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 9b569233aa..2aef0a98f6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1755,35 +1755,37 @@ private final class ChatSendStarsScreenComponent: Component { switch component.initialData.subjectInitialData { case let .react(reactData): - var sendAsPeers: [EnginePeer] = [reactData.myPeer] - sendAsPeers.append(contentsOf: self.channelsForPublicReaction) - - let currentMyPeer = self.currentMyPeer ?? reactData.myPeer - - let peerSelectorButtonSize = self.peerSelectorButton.update( - transition: transition, - component: AnyComponent(PeerSelectorBadgeComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: currentMyPeer, - action: { [weak self] sourceView in - guard let self else { - return + if case .message = reactData.reactSubject { + var sendAsPeers: [EnginePeer] = [reactData.myPeer] + sendAsPeers.append(contentsOf: self.channelsForPublicReaction) + + let currentMyPeer = self.currentMyPeer ?? reactData.myPeer + + let peerSelectorButtonSize = self.peerSelectorButton.update( + transition: transition, + component: AnyComponent(PeerSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentMyPeer, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) } - self.displayTargetSelectionMenu(sourceView: sourceView) + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - peerSelectorButtonSize.width, y: floor((78.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + if let peerSelectorButtonView = self.peerSelectorButton.view { + if peerSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(peerSelectorButtonView) } - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - peerSelectorButtonSize.width, y: floor((78.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) - if let peerSelectorButtonView = self.peerSelectorButton.view { - if peerSelectorButtonView.superview == nil { - self.navigationBarContainer.addSubview(peerSelectorButtonView) + transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) + peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } - transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) - peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } case .liveStreamMessage: break @@ -1967,6 +1969,7 @@ private final class ChatSendStarsScreenComponent: Component { id: 1 ), stableId: 0, + isIncoming: false, author: liveStreamMessage.myPeer, text: liveStreamMessage.text.string, entities: entities, @@ -1981,6 +1984,7 @@ private final class ChatSendStarsScreenComponent: Component { id: 1 ), stableId: 0, + isIncoming: false, author: reactData.myPeer, text: "", entities: [], diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD index c2ce20425f..46eb4b449b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/BUILD @@ -65,6 +65,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + "//submodules/TelegramUI/Components/StarsParticleEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift index 594976a7d7..2e9ac46f63 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift @@ -5,6 +5,109 @@ import TelegramPresentationData import ComponentFlow import GlassBackgroundComponent import AnimatedTextComponent +import StarsParticleEffect + +final class StarReactionButtonBadgeComponent: Component { + let theme: PresentationTheme + let count: Int + let isFilled: Bool + + init( + theme: PresentationTheme, + count: Int, + isFilled: Bool + ) { + self.theme = theme + self.count = count + self.isFilled = isFilled + } + + static func ==(lhs: StarReactionButtonBadgeComponent, rhs: StarReactionButtonBadgeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.isFilled != rhs.isFilled { + return false + } + return true + } + + final class View: UIView { + private let backgroundView: GlassBackgroundView + private let text = ComponentView() + + private var component: StarReactionButtonBadgeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StarReactionButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let height: CGFloat = 15.0 + let sideInset: CGFloat = 4.0 + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(10.0), + color: component.theme.chat.inputPanel.panelControlColor, + items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countString(Int64(component.count))))], + noDelay: true + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: textSize.width + sideInset * 2.0, height: height) + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + + let backgroundTintColor: GlassBackgroundView.TintColor + if component.isFilled { + backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D)) + } else { + backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + } + + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) + + if let textView = self.text.view { + let textFrame = textSize.centered(in: CGRect(origin: CGPoint(), size: size)) + + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(textView) + } + transition.setFrame(view: textView, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class StarReactionButtonComponent: Component { let theme: PresentationTheme @@ -45,8 +148,11 @@ final class StarReactionButtonComponent: Component { final class View: UIView { private let backgroundView: GlassBackgroundView + private let backgroundEffectLayer: StarsParticleEffectLayer + private let backgroundMaskView: UIView + private let backgroundBadgeMask: UIImageView private let iconView: UIImageView - private var text: ComponentView? + private var badge: ComponentView? private var longTapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -55,6 +161,20 @@ final class StarReactionButtonComponent: Component { override init(frame: CGRect) { self.backgroundView = GlassBackgroundView() + self.backgroundMaskView = UIView() + self.backgroundBadgeMask = UIImageView() + self.backgroundMaskView.addSubview(self.backgroundBadgeMask) + + self.backgroundEffectLayer = StarsParticleEffectLayer() + self.backgroundView.contentView.layer.addSublayer(self.backgroundEffectLayer) + + //self.backgroundView.mask = self.backgroundMaskView + + self.backgroundMaskView.backgroundColor = .white + if let filter = CALayer.luminanceToAlpha() { + self.backgroundMaskView.layer.filters = [filter] + } + self.iconView = UIImageView() super.init(frame: frame) @@ -96,48 +216,53 @@ final class StarReactionButtonComponent: Component { self.component = component self.state = state - let leftInset: CGFloat = 12.0 - let rightInset: CGFloat = 12.0 - let textSpacing: CGFloat = 2.0 - - var size = CGSize(width: 40.0, height: 40.0) - var textSize: CGSize? + let size = CGSize(width: 40.0, height: 40.0) if self.iconView.image == nil { self.iconView.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate) } if component.count != 0 { - let text: ComponentView - var textTransition = transition - if let current = self.text { - text = current + let badge: ComponentView + var badgeTransition = transition + if let current = self.badge { + badge = current } else { - textTransition = textTransition.withAnimation(.none) - text = ComponentView() - self.text = text + badgeTransition = badgeTransition.withAnimation(.none) + badge = ComponentView() + self.badge = badge } - let textSizeValue = text.update( - transition: textTransition, - component: AnyComponent(AnimatedTextComponent( - font: Font.regular(17.0), - color: component.theme.chat.inputPanel.panelControlColor, - items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.count, minDigits: 1))], - noDelay: true + let badgeSize = badge.update( + transition: badgeTransition, + component: AnyComponent(StarReactionButtonBadgeComponent( + theme: component.theme, + count: component.count, + isFilled: component.isFilled )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) - textSize = textSizeValue - if let image = self.iconView.image { - size.width = leftInset + image.size.width + textSpacing + textSizeValue.width + rightInset + if let badgeView = badge.view { + var badgeFrame = CGRect(origin: CGPoint(x: min(size.width + 6.0 - badgeSize.width, floorToScreenPixels(size.width - 4.0 - badgeSize.width * 0.5)), y: -3.0), size: badgeSize) + if badgeSize.width > size.width * 0.8 { + badgeFrame.origin.x = floor((size.width - badgeSize.width) * 0.5) + } + + if badgeView.superview == nil { + badgeView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(badgeView) + badgeView.frame = badgeFrame + transition.animateScale(view: badgeView, from: 0.001, to: 1.0) + transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0) + } + transition.setFrame(view: badgeView, frame: badgeFrame) } - } else if let text = self.text { - self.text = nil - if let textView = text.view { - transition.setScale(view: textView, scale: 0.001) - transition.setAlpha(view: textView, alpha: 0.0, completion: { [weak textView] _ in - textView?.removeFromSuperview() + } else if let badge = self.badge { + self.badge = nil + if let badgeView = badge.view { + transition.setScale(view: badgeView, scale: 0.001) + transition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in + badgeView?.removeFromSuperview() }) } } @@ -153,30 +278,24 @@ final class StarReactionButtonComponent: Component { self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.setFrame(layer: self.backgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + self.backgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.25), rate: 10.0, size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, transition: transition) + + let badgeDiameter: CGFloat = 15.0 + if self.backgroundBadgeMask.image == nil { + self.backgroundBadgeMask.image = generateStretchableFilledCircleImage(diameter: badgeDiameter + 1.0 * 2.0, color: .black) + } + let badgeWidth: CGFloat = 20.0 + let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeWidth, y: 0.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) + transition.setFrame(view: self.backgroundBadgeMask, frame: badgeFrame.insetBy(dx: -1.0, dy: -1.0)) self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor if let image = self.iconView.image { - let iconFrame: CGRect - if textSize == nil { - iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - } else { - iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((backgroundFrame.height - image.size.height) * 0.5)), size: image.size) - } + let iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.setFrame(view: self.iconView, frame: iconFrame) - - if let textView = self.text?.view, let textSize { - let textFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + textSpacing, y: floor((backgroundFrame.height - textSize.height) * 0.5)), size: textSize) - - if textView.superview == nil { - textView.isUserInteractionEnabled = false - self.backgroundView.contentView.addSubview(textView) - textView.frame = textFrame - transition.animateScale(view: textView, from: 0.001, to: 1.0) - transition.animateAlpha(view: textView, from: 0.0, to: 1.0) - } - transition.setFrame(view: textView, frame: textFrame) - } } return size diff --git a/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift index 1d211b19f8..def26465ca 100644 --- a/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift +++ b/submodules/TelegramUI/Components/StarsParticleEffect/Sources/ActionPanelComponent.swift @@ -23,7 +23,7 @@ public final class StarsParticleEffectLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - private func setup() { + private func setup(rate: CGFloat) { guard let currentColor = self.currentColor else { return } @@ -32,7 +32,7 @@ public final class StarsParticleEffectLayer: SimpleLayer { let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage - emitter.birthRate = 25.0 + emitter.birthRate = Float(rate) emitter.lifetime = 2.0 emitter.velocity = 12.0 emitter.velocityRange = 3 @@ -56,10 +56,10 @@ public final class StarsParticleEffectLayer: SimpleLayer { self.emitterLayer.emitterCells = [emitter] } - public func update(color: UIColor, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + public func update(color: UIColor, rate: CGFloat = 25.0, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { if self.emitterLayer.emitterCells == nil || self.currentColor != color { self.currentColor = color - self.setup() + self.setup(rate: rate) } self.emitterLayer.emitterShape = .circle self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 826101a1ff..5ce7a5256f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -111,6 +111,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/AdminUserActionsSheet", "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", + "//submodules/Components/HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift new file mode 100644 index 0000000000..97adc3d522 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -0,0 +1,197 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import AvatarNode +import AppBundle +import AccountContext +import HierarchyTrackingLayer + +private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: Int) async -> UIImage { + let avatarSize: CGFloat = 16.0 + let avatarInset: CGFloat = 2.0 + let avatarIconSpacing: CGFloat = 2.0 + let iconTextSpacing: CGFloat = 2.0 + let iconSize: CGFloat = 8.0 + let rightInset: CGFloat = 4.0 + + let text = NSAttributedString(string: "\(count)", font: Font.semibold(10.0), textColor: .white) + var textSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size + textSize.width = ceil(textSize.width) + textSize.height = ceil(textSize.height) + + let size = CGSize(width: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing + textSize.height + rightInset, height: avatarSize + avatarInset * 2.0) + return generateImage(size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0xFFB10D).cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) + context.fillPath() + + text.draw(at: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5))) + })! +} + +private actor LiveChatReactionItemTaskQueue { + private final class PeerTask { + let peer: EnginePeer + let count: Int + let completion: (UIImage) -> Void + + init(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) { + self.peer = peer + self.count = count + self.completion = completion + } + } + + private let engine: TelegramEngine + private var tasks: [PeerTask] = [] + + init(engine: TelegramEngine) { + self.engine = engine + } + + func add(peer: EnginePeer, count: Int, completion: @escaping (UIImage) -> Void) { + self.tasks.append(PeerTask(peer: peer, count: count, completion: completion)) + if self.tasks.count == 1 { + Task { + await processTasks() + } + } + } + + private func processTasks() async { + while !self.tasks.isEmpty { + let task = self.tasks.removeFirst() + let image = await makePeerBadgeImage(engine: self.engine, peer: task.peer, count: task.count) + task.completion(image) + } + } +} + +final class LiveChatReactionStreamView: UIView { + private final class ItemLayer: SimpleLayer { + init(image: UIImage) { + super.init() + + self.contents = image.cgImage + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private var nextId: Int = 0 + private var itemLayers: [Int: ItemLayer] = [:] + private let itemLayerContainer: SimpleLayer + private let hierarchyTracker: HierarchyTrackingLayer + private var previousTimestamp: Double = 0.0 + private var displayLink: SharedDisplayLinkDriver.Link? + private var previousPhysicsTimestamp: Double = 0.0 + + private let taskQueue: LiveChatReactionItemTaskQueue + + init(context: AccountContext) { + self.itemLayerContainer = SimpleLayer() + self.hierarchyTracker = HierarchyTrackingLayer() + self.taskQueue = LiveChatReactionItemTaskQueue(engine: context.engine) + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.itemLayerContainer) + + self.layer.addSublayer(self.hierarchyTracker) + self.hierarchyTracker.isInHierarchyUpdated = { [weak self] inHierarchy in + guard let self else { + return + } + if inHierarchy { + if self.displayLink == nil { + self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + guard let self else { + return + } + self.updatePhysics() + }) + } + } else { + self.displayLink = nil + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func add(peer: EnginePeer, count: Int) { + if !self.hierarchyTracker.isInHierarchy { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if timestamp < self.previousTimestamp + 1.0 / 30.0 { + return + } + self.previousTimestamp = timestamp + Task { + await self.taskQueue.add(peer: peer, count: count, completion: { [weak self] image in + Task { @MainActor in + guard let self else { + return + } + self.addRenderedItem(image: image) + } + }) + } + } + + private func addRenderedItem(image: UIImage) { + if "".isEmpty { + return + } + + let id = self.nextId + self.nextId += 1 + + let itemLayer = ItemLayer(image: image) + itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 10.0, y: -image.size.height * 0.5), size: image.size) + self.itemLayers[id] = itemLayer + self.itemLayerContainer.addSublayer(itemLayer) + + let transition = ComponentTransition(animation: .curve(duration: 2.0, curve: .linear)) + transition.setPosition(layer: itemLayer, position: CGPoint(x: itemLayer.position.x, y: -300.0)) + + itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let itemLayer = self.itemLayers[id] { + self.itemLayers.removeValue(forKey: id) + itemLayer.removeFromSuperlayer() + } + }) + } + + private func updatePhysics() { + let timestamp = CACurrentMediaTime() + let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) + self.previousPhysicsTimestamp = timestamp + + let _ = dt + } + + func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) { + transition.setFrame(layer: self.itemLayerContainer, frame: CGRect(origin: sourcePoint, size: CGSize())) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index ff37e7c9f0..f89f062c89 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -476,6 +476,8 @@ final class StoryContentLiveChatComponent: Component { private let listState = AsyncListComponent.ExternalState() private let list = ComponentView() private let listShadowView: UIView + + private var reactionStreamView: LiveChatReactionStreamView? private var component: StoryContentLiveChatComponent? private weak var state: EmptyComponentState? @@ -826,20 +828,26 @@ final class StoryContentLiveChatComponent: Component { updateTransition = .immediate } - if let component = self.component, let previousMessagesState = self.messagesState, !self.isChatExpanded { - var hasNewMessages = false - for message in state.messages { - //TODO:release - //if message.author?.id != component.context.account.peerId { - do { - if !previousMessagesState.messages.contains(where: { $0.id == message.id }) { - hasNewMessages = true - break + if let component = self.component, let previousMessagesState = self.messagesState { + if !self.isChatExpanded { + var hasNewMessages = false + for message in state.messages { + //TODO:release + //if message.author?.id != component.context.account.peerId { + do { + if !previousMessagesState.messages.contains(where: { $0.id == message.id }) { + hasNewMessages = true + break + } } } + if hasNewMessages { + component.external.hasUnseenMessages = true + } } - if hasNewMessages { - component.external.hasUnseenMessages = true + + if state.pendingMyStars > previousMessagesState.pendingMyStars, let message = state.messages.first(where: { $0.paidStars != nil && !$0.isIncoming }), let peer = message.author { + self.reactionStreamView?.add(peer: peer, count: Int(state.pendingMyStars - previousMessagesState.pendingMyStars)) } } self.messagesState = state @@ -991,6 +999,16 @@ final class StoryContentLiveChatComponent: Component { self.listShadowView.backgroundColor = UIColor(white: 0.0, alpha: 0.3) transition.setAlpha(view: self.listShadowView, alpha: self.isChatExpanded ? 1.0 : 0.0) + let reactionStreamView: LiveChatReactionStreamView + if let current = self.reactionStreamView { + reactionStreamView = current + } else { + reactionStreamView = LiveChatReactionStreamView(context: component.context) + self.reactionStreamView = reactionStreamView + self.addSubview(reactionStreamView) + } + reactionStreamView.update(size: availableSize, sourcePoint: CGPoint(x: availableSize.width, y: availableSize.height), transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 295eed8349..05b2c612b3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2987,7 +2987,20 @@ public final class StoryItemSetContainerComponent: Component { } } - sendAsConfiguration = self.sendMessageContext.currentSendAsConfiguration + sendAsConfiguration = self.sendMessageContext.currentSendAsPeer.flatMap { value in + return MessageInputPanelComponent.SendAsConfiguration( + currentPeer: EnginePeer(value.peer), + subscriberCount: value.subscribers.flatMap(Int.init), + isPremiumLocked: value.isPremiumRequired, + isSelecting: self.sendMessageContext.isSelectingSendAsPeer, + action: { [weak self] sourceView, gesture in + guard let self else { + return + } + self.sendMessageContext.openSendAsSelection(view: self, sourceView: sourceView, gesture: gesture) + } + ) + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 9bc37ea475..f903b6fd96 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -105,9 +105,9 @@ final class StoryItemSetContainerSendMessage { var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? - var sendAsData: (isPremium: Bool, availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)])? - var currentSendAsConfiguration: MessageInputPanelComponent.SendAsConfiguration? - var sendAsContextPeerId: EnginePeer.Id? + var sendAsData: (isPremium: Bool, availablePeers: [SendAsPeer])? + var currentSendAsPeer: SendAsPeer? + var isSelectingSendAsPeer: Bool = false var sendAsDisposable: Disposable? private(set) var isMediaRecordingLocked: Bool = false @@ -229,21 +229,17 @@ final class StoryItemSetContainerSendMessage { let isPremium = accountPeer.isPremium - var availablePeers: [(peer: EnginePeer, subscriberCount: Int?, isPremiumRequired: Bool)] = [] - availablePeers.append(( - peer: accountPeer, - subscriberCount: nil, + var availablePeers: [SendAsPeer] = [] + availablePeers.append(SendAsPeer( + peer: accountPeer._asPeer(), + subscribers: nil, isPremiumRequired: false )) for peer in peers { if peer.peer.id == accountPeer.id { continue } - availablePeers.append(( - peer: EnginePeer(peer.peer), - subscriberCount: peer.subscribers.flatMap(Int.init), - isPremiumRequired: peer.isPremiumRequired - )) + availablePeers.append(peer) } self.sendAsData = ( @@ -251,29 +247,16 @@ final class StoryItemSetContainerSendMessage { availablePeers: availablePeers ) - //TODO:localize - if "".isEmpty { - let sendAsConfiguration = MessageInputPanelComponent.SendAsConfiguration( - currentPeer: accountPeer, - subscriberCount: nil, - isPremiumLocked: false, - isSelecting: false, - action: { [weak self, weak view] sourceView, gesture in - guard let self, let view else { - return - } - self.openSendAsSelection(view: view, sourceView: sourceView, gesture: gesture) - } - ) - if self.currentSendAsConfiguration != sendAsConfiguration { - self.currentSendAsConfiguration = sendAsConfiguration + if availablePeers.count > 1 { + if self.currentSendAsPeer == nil { + self.currentSendAsPeer = availablePeers.first if !view.isUpdatingComponent { view.state?.updated(transition: .spring(duration: 0.4)) } } } else { - if self.currentSendAsConfiguration != nil { - self.currentSendAsConfiguration = nil + if self.currentSendAsPeer != nil { + self.currentSendAsPeer = nil if !view.isUpdatingComponent { view.state?.updated(transition: .spring(duration: 0.4)) } @@ -771,7 +754,7 @@ final class StoryItemSetContainerSendMessage { let entities = generateChatInputTextEntities(text) - call.sendMessage(text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) + call.sendMessage(fromId: self.currentSendAsPeer?.peer.id, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) @@ -4221,11 +4204,11 @@ final class StoryItemSetContainerSendMessage { return } - call.sendStars(amount: Int64(count), delay: delay) + call.sendStars(fromId: self.currentSendAsPeer?.peer.id, amount: Int64(count), delay: delay) } - private func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { - guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsConfiguration = self.currentSendAsConfiguration, let controller = component.controller() else { + func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { + guard let component = view.component, let sendAsData = self.sendAsData, let currentSendAsPeer = self.currentSendAsPeer, let controller = component.controller() else { return } @@ -4235,27 +4218,22 @@ final class StoryItemSetContainerSendMessage { } let isPremium = sendAsData.isPremium - let mappedPeers = sendAsData.availablePeers.map { peer in - return SendAsPeer( - peer: peer.peer._asPeer(), - subscribers: peer.subscriberCount.flatMap(Int32.init(clamping:)), - isPremiumRequired: peer.isPremiumRequired - ) - } - var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem( context: component.context, chatPeerId: peerId, - peers: mappedPeers, - selectedPeerId: currentSendAsConfiguration.currentPeer.id, + peers: sendAsData.availablePeers, + selectedPeerId: currentSendAsPeer.peer.id, isPremium: isPremium, - action: { [weak self] peer in - guard let self else { + action: { [weak self, weak view] peer in + guard let self, let view else { return } - let _ = self + if let foundPeer = self.sendAsData?.availablePeers.first(where: { $0.peer.id == peer.id }) { + self.currentSendAsPeer = foundPeer + view.state?.updated(transition: .spring(duration: 0.4)) + } }, presentToast: { [weak view] peer in guard let view, let component = view.component, let controller = component.controller() else { @@ -4304,11 +4282,12 @@ final class StoryItemSetContainerSendMessage { guard let self, let view else { return } - let _ = self + self.isSelectingSendAsPeer = false view.state?.updated(transition: .spring(duration: 0.4)) } controller.presentInGlobalOverlay(contextController) + self.isSelectingSendAsPeer = true view.state?.updated(transition: .spring(duration: 0.4)) } } From 70702914b4eac152ff9026dd63058b019cf6451c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 28 Oct 2025 16:56:08 +0400 Subject: [PATCH 3/6] Stories --- .../Sources/PresentationGroupCall.swift | 15 -- .../TelegramEngine/Messages/Stories.swift | 18 +- .../Sources/ChatSendStarsScreen.swift | 5 +- .../Stories/StoryContainerScreen/BUILD | 1 + .../Sources/LiveChatReactionStreamView.swift | 163 ++++++++++++++++-- ...StoryItemSetContainerViewSendMessage.swift | 1 + .../LokiRng/PublicHeaders/LokiRng/LokiRng.h | 1 + submodules/Utils/LokiRng/Sources/LokiRng.mm | 30 ++++ 8 files changed, 202 insertions(+), 32 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index abd952c3f9..b7908c3952 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -934,21 +934,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if isStream { messageLifetime = Int32.max - - if self.isStream { - var allowLiveChat = false - if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data { - if let dev = data["dev"] as? Double, dev != 0.0 { - allowLiveChat = true - } - if data["ios_can_join_streams"] != nil { - allowLiveChat = true - } - } - if !allowLiveChat { - preconditionFailure() - } - } } self.messagesContext = accountContext.engine.messages.groupCallMessages( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 2d21de2fd2..1edcb22928 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -520,23 +520,27 @@ public enum Stories { case timestamp case expirationTimestamp case isCloseFriends = "clf" + case isLiveItem = "liv" } public let id: Int32 public let timestamp: Int32 public let expirationTimestamp: Int32 public let isCloseFriends: Bool + public let isLiveItem: Bool public init( id: Int32, timestamp: Int32, expirationTimestamp: Int32, - isCloseFriends: Bool + isCloseFriends: Bool, + isLiveItem: Bool ) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.isCloseFriends = isCloseFriends + self.isLiveItem = isLiveItem } public init(from decoder: Decoder) throws { @@ -546,6 +550,7 @@ public enum Stories { self.timestamp = try container.decode(Int32.self, forKey: .timestamp) self.expirationTimestamp = try container.decode(Int32.self, forKey: .expirationTimestamp) self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false + self.isLiveItem = try container.decodeIfPresent(Bool.self, forKey: .isLiveItem) ?? false } public func encode(to encoder: Encoder) throws { @@ -555,6 +560,7 @@ public enum Stories { try container.encode(self.timestamp, forKey: .timestamp) try container.encode(self.expirationTimestamp, forKey: .expirationTimestamp) try container.encode(self.isCloseFriends, forKey: .isCloseFriends) + try container.encode(self.isLiveItem, forKey: .isLiveItem) } public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool { @@ -570,6 +576,9 @@ public enum Stories { if lhs.isCloseFriends != rhs.isCloseFriends { return false } + if lhs.isLiveItem != rhs.isLiveItem { + return false + } return true } } @@ -628,8 +637,8 @@ public enum Stories { switch self { case let .item(item): return item.media is TelegramMediaLiveStream - case .placeholder: - return false + case let .placeholder(placeholder): + return placeholder.isLiveItem } } @@ -2318,7 +2327,8 @@ extension Stories.StoredItem { } case let .storyItemSkipped(flags, id, date, expireDate): let isCloseFriends = (flags & (1 << 8)) != 0 - self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends)) + let isLiveItem = (flags & (1 << 9)) != 0 + self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends, isLiveItem: isLiveItem)) case .storyItemDeleted: return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 2aef0a98f6..46661824be 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -2803,7 +2803,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, myPeer: EnginePeer? = nil, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -2875,7 +2875,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { defaultPrivacyPeer ) |> map { peerAndTopPeerMap, balance, channelsForPublicReaction, defaultPrivacyPeer -> InitialData? in - let (peer, myPeer, topPeerMap) = peerAndTopPeerMap + let (peer, myPeerValue, topPeerMap) = peerAndTopPeerMap + let myPeer = myPeer ?? myPeerValue guard let peer, let myPeer else { return nil } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 5ce7a5256f..8d049bc116 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -112,6 +112,7 @@ swift_library( "//submodules/TelegramUI/Components/AdminUserActionsSheet", "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Utils/LokiRng", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift index 97adc3d522..26168cf9bc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -7,14 +7,97 @@ import AvatarNode import AppBundle import AccountContext import HierarchyTrackingLayer +import LokiRng + +private let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], +] + +private func avatarViewLettersImage(size: CGSize, peerId: EnginePeer.Id, letters: [String], isStory: Bool) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 2.0) + let context = UIGraphicsGetCurrentContext() + + context?.beginPath() + if isStory { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height).insetBy(dx: 4.0, dy: 4.0)) + } else { + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + } + context?.clip() + + let colorIndex: Int + if peerId.namespace == .max { + colorIndex = 0 + } else { + colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value())) + } + + let colorsArray = gradientColors[colorIndex % gradientColors.count] + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context?.setBlendMode(.normal) + + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 8.0), NSAttributedString.Key.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) + let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + + context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context?.scaleBy(x: 1.0, y: -1.0) + context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) + if let context = context { + CTLineDraw(line, context) + } + context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + + if isStory { + context?.resetClip() + + let lineWidth: CGFloat = 2.0 + context?.setLineWidth(lineWidth) + context?.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height)).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + context?.replacePathWithStrokedPath() + context?.clip() + + let colors: [CGColor] = [ + UIColor(rgb: 0x34C76F).cgColor, + UIColor(rgb: 0x3DA1FD).cgColor + ] + var locations: [CGFloat] = [0.0, 1.0] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: Int) async -> UIImage { let avatarSize: CGFloat = 16.0 let avatarInset: CGFloat = 2.0 let avatarIconSpacing: CGFloat = 2.0 - let iconTextSpacing: CGFloat = 2.0 - let iconSize: CGFloat = 8.0 - let rightInset: CGFloat = 4.0 + let iconTextSpacing: CGFloat = 1.0 + let iconSize: CGFloat = 10.0 + let rightInset: CGFloat = 2.0 let text = NSAttributedString(string: "\(count)", font: Font.semibold(10.0), textColor: .white) var textSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size @@ -33,6 +116,15 @@ private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) context.fillPath() + if let image = avatarViewLettersImage(size: CGSize(width: avatarSize, height: avatarSize), peerId: peer.id, letters: peer.displayLetters, isStory: false) { + image.draw(in: CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: CGSize(width: avatarSize, height: avatarSize))) + } + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white) { + let iconFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing, y: floorToScreenPixels((size.height - iconSize) * 0.5) + 1.0), size: CGSize(width: iconSize, height: iconSize)) + image.draw(in: iconFrame) + } + text.draw(at: CGPoint(x: avatarInset + avatarSize + avatarIconSpacing + iconSize + iconTextSpacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5))) })! } @@ -81,6 +173,7 @@ final class LiveChatReactionStreamView: UIView { super.init() self.contents = image.cgImage + self.allowsEdgeAntialiasing = true } override init(layer: Any) { @@ -157,20 +250,16 @@ final class LiveChatReactionStreamView: UIView { } private func addRenderedItem(image: UIImage) { - if "".isEmpty { - return - } - let id = self.nextId self.nextId += 1 let itemLayer = ItemLayer(image: image) - itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 10.0, y: -image.size.height * 0.5), size: image.size) + itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 30.0, y: -image.size.height * 0.5), size: image.size).offsetBy(dx: 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0)) - 0.5, dy: 0.0) + itemLayer.transform = CATransform3DMakeRotation(CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 1, seed2: 0) - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) self.itemLayers[id] = itemLayer self.itemLayerContainer.addSublayer(itemLayer) - let transition = ComponentTransition(animation: .curve(duration: 2.0, curve: .linear)) - transition.setPosition(layer: itemLayer, position: CGPoint(x: itemLayer.position.x, y: -300.0)) + //itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, removeOnCompletion: false, completion: { [weak self] _ in guard let self else { @@ -188,7 +277,59 @@ final class LiveChatReactionStreamView: UIView { let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) self.previousPhysicsTimestamp = timestamp - let _ = dt + let cellSize: CGFloat = 16.0 + let forceScale: CGFloat = 60.0 + let falloffDistance: CGFloat = 24.0 + + for (id, itemLayer) in self.itemLayers { + let px = itemLayer.position.x + let py = itemLayer.position.y + + // Grid coordinates (no abs; keep sign, use floor) + let gx = Int(floor(px / cellSize)) + let gy = Int(floor(py / cellSize)) + + // Fractional position within the cell + let fx = (px / cellSize) - CGFloat(gx) + let fy = (py / cellSize) - CGFloat(gy) + + // Bilinear weights for the 4 corners + let w00 = (1 - fx) * (1 - fy) + let w10 = (fx) * (1 - fy) + let w01 = (1 - fx) * (fy) + let w11 = (fx) * (fy) + + func n(_ ix: Int, _ iy: Int) -> CGFloat { + // random in [0,1), shift to [-0.5, 0.5) + let r = LokiRng.random( + withSeed0: UInt(abs(ix)), + seed1: UInt(abs(iy)), + seed2: UInt(id) + ) + return CGFloat(r) - 0.5 + } + + let n00x = n(gx + 0, gy + 0) + let n10x = n(gx + 1, gy + 0) + let n01x = n(gx + 0, gy + 1) + let n11x = n(gx + 1, gy + 1) + + // Bilinear interpolation (smooth, limited cancellation) + var fxForce = w00*n00x + w10*n10x + w01*n01x + w11*n11x + + // Optional local radial falloff from the nearest lattice center + // (invert the original: strongest at center) + let cx = (CGFloat(gx) + 0.5) * cellSize + let cy = (CGFloat(gy) + 0.5) * cellSize + let d = hypot(px - cx, py - cy) + let t = max(0.0, 1.0 - d / falloffDistance) + let weight = t * t + fxForce *= weight + + // Apply force directly to position (or integrate velocity if you have it) + itemLayer.position.x += fxForce * forceScale * dt + itemLayer.position.y -= dt * 100.0 + } } func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index f903b6fd96..719bad7ac6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -4008,6 +4008,7 @@ final class StoryItemSetContainerSendMessage { let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, + myPeer: (self.currentSendAsPeer?.peer).flatMap(EnginePeer.init), reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount)), topPeers: topPeers, completion: { [weak self, weak view] amount, _, _, _ in diff --git a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h index e1c0abb7c3..73b623d0f9 100644 --- a/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h +++ b/submodules/Utils/LokiRng/PublicHeaders/LokiRng/LokiRng.h @@ -8,6 +8,7 @@ - (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; - (float)next; ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2; @end diff --git a/submodules/Utils/LokiRng/Sources/LokiRng.mm b/submodules/Utils/LokiRng/Sources/LokiRng.mm index 3f8240d361..957af6d795 100644 --- a/submodules/Utils/LokiRng/Sources/LokiRng.mm +++ b/submodules/Utils/LokiRng/Sources/LokiRng.mm @@ -61,4 +61,34 @@ static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, c return old_seed; } ++ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 { + uint32_t seed = ((uint32_t)seed0) * 1099087573U; + uint32_t seedb = ((uint32_t)seed1) * 1099087573U; + uint32_t seedc = ((uint32_t)seed2) * 1099087573U; + + // Round 1: Randomise seed + uint32_t z1 = tausStep(seed,13,19,12,429496729U); + uint32_t z2 = tausStep(seed,2,25,4,4294967288U); + uint32_t z3 = tausStep(seed,3,11,17,429496280U); + uint32_t z4 = (1664525*seed + 1013904223U); + + // Round 2: Randomise seed again using second seed + uint32_t r1 = (z1^z2^z3^z4^seedb); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + // Round 3: Randomise seed again using third seed + r1 = (z1^z2^z3^z4^seedc); + + z1 = tausStep(r1,13,19,12,429496729U); + z2 = tausStep(r1,2,25,4,4294967288U); + z3 = tausStep(r1,3,11,17,429496280U); + z4 = (1664525*r1 + 1013904223U); + + return (z1^z2^z3^z4) * 2.3283064365387e-10f; +} + @end From d4d032a47de75ebd0ea5825064bd96affcf1fc15 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 28 Oct 2025 17:48:36 +0400 Subject: [PATCH 4/6] Fix incoming stars --- .../Sources/StoryContentLiveChatComponent.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index f89f062c89..7fdddb25a8 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -832,12 +832,11 @@ final class StoryContentLiveChatComponent: Component { if !self.isChatExpanded { var hasNewMessages = false for message in state.messages { - //TODO:release - //if message.author?.id != component.context.account.peerId { - do { - if !previousMessagesState.messages.contains(where: { $0.id == message.id }) { - hasNewMessages = true - break + if message.isIncoming && !previousMessagesState.messages.contains(where: { $0.id == message.id }) { + hasNewMessages = true + + if message.isIncoming, let paidStars = message.paidStars, let author = message.author { + self.reactionStreamView?.add(peer: author, count: Int(paidStars)) } } } From 8dc3fcb2ece6cd1ec447afefc6d76bba1c53f1e1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 28 Oct 2025 18:26:25 +0400 Subject: [PATCH 5/6] Update animation --- .../Sources/GlassBackgroundComponent.swift | 1 - .../Sources/LiveChatReactionStreamView.swift | 110 ++++++++---------- 2 files changed, 46 insertions(+), 65 deletions(-) diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index c0d0710189..fb7e2f54cb 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -383,7 +383,6 @@ public class GlassBackgroundView: UIView { nativeView.layer.cornerRadius = cornerRadius nativeView.frame = CGRect(origin: CGPoint(), size: size) } else { - //nativeView.layer.cornerRadius = cornerRadius transition.setCornerRadius(layer: nativeView.layer, cornerRadius: cornerRadius) let nativeFrame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift index 26168cf9bc..bafe924698 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -169,7 +169,18 @@ private actor LiveChatReactionItemTaskQueue { final class LiveChatReactionStreamView: UIView { private final class ItemLayer: SimpleLayer { - init(image: UIImage) { + let amplitude: CGFloat + let period: CGFloat + let phaseOffset: CGFloat + let baseX: CGFloat + var timeValue: CGFloat = 0.0 + + init(image: UIImage, amplitude: CGFloat, period: CGFloat, phaseOffset: CGFloat, baseX: CGFloat) { + self.amplitude = amplitude + self.period = period + self.phaseOffset = phaseOffset + self.baseX = baseX + super.init() self.contents = image.cgImage @@ -177,6 +188,11 @@ final class LiveChatReactionStreamView: UIView { } override init(layer: Any) { + self.amplitude = 0.0 + self.period = 0.0 + self.phaseOffset = 0.0 + self.baseX = 0.0 + super.init(layer: layer) } @@ -233,7 +249,7 @@ final class LiveChatReactionStreamView: UIView { return } let timestamp = CFAbsoluteTimeGetCurrent() - if timestamp < self.previousTimestamp + 1.0 / 30.0 { + if timestamp < self.previousTimestamp + 0.2 { return } self.previousTimestamp = timestamp @@ -253,22 +269,35 @@ final class LiveChatReactionStreamView: UIView { let id = self.nextId self.nextId += 1 - let itemLayer = ItemLayer(image: image) - itemLayer.frame = CGRect(origin: CGPoint(x: -image.size.width - 30.0, y: -image.size.height * 0.5), size: image.size).offsetBy(dx: 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0)) - 0.5, dy: 0.0) - itemLayer.transform = CATransform3DMakeRotation(CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 1, seed2: 0) - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) + let random = LokiRng(seed0: UInt(id), seed1: 1, seed2: 0) + let itemX: CGFloat = -image.size.width - 8.0 + 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0) - 0.5) + let phaseOffset: CGFloat = CGFloat(random.next()) + let itemLayer = ItemLayer(image: image, amplitude: 0.0 + CGFloat(random.next()) * 6.0, period: 1.0 + CGFloat(random.next()) * 1.0, phaseOffset: phaseOffset, baseX: itemX) + itemLayer.frame = CGRect(origin: CGPoint(x: itemX, y: -image.size.height * 0.5), size: image.size) + itemLayer.transform = CATransform3DMakeRotation(CGFloat(random.next() - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) self.itemLayers[id] = itemLayer self.itemLayerContainer.addSublayer(itemLayer) - //itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) + itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) - itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: 2.0 - 0.18, removeOnCompletion: false, completion: { [weak self] _ in - guard let self else { + itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self, weak itemLayer] _ in + guard let itemLayer else { return } - if let itemLayer = self.itemLayers[id] { - self.itemLayers.removeValue(forKey: id) - itemLayer.removeFromSuperlayer() - } + + let delay: Double = 2.0 - 0.1 - 0.18 + + itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.18, delay: delay, removeOnCompletion: false) + itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: delay, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + if let itemLayer = self.itemLayers[id] { + self.itemLayers.removeValue(forKey: id) + itemLayer.removeFromSuperlayer() + } + }) }) } @@ -276,59 +305,12 @@ final class LiveChatReactionStreamView: UIView { let timestamp = CACurrentMediaTime() let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) self.previousPhysicsTimestamp = timestamp - - let cellSize: CGFloat = 16.0 - let forceScale: CGFloat = 60.0 - let falloffDistance: CGFloat = 24.0 - for (id, itemLayer) in self.itemLayers { - let px = itemLayer.position.x - let py = itemLayer.position.y - - // Grid coordinates (no abs; keep sign, use floor) - let gx = Int(floor(px / cellSize)) - let gy = Int(floor(py / cellSize)) - - // Fractional position within the cell - let fx = (px / cellSize) - CGFloat(gx) - let fy = (py / cellSize) - CGFloat(gy) - - // Bilinear weights for the 4 corners - let w00 = (1 - fx) * (1 - fy) - let w10 = (fx) * (1 - fy) - let w01 = (1 - fx) * (fy) - let w11 = (fx) * (fy) - - func n(_ ix: Int, _ iy: Int) -> CGFloat { - // random in [0,1), shift to [-0.5, 0.5) - let r = LokiRng.random( - withSeed0: UInt(abs(ix)), - seed1: UInt(abs(iy)), - seed2: UInt(id) - ) - return CGFloat(r) - 0.5 - } - - let n00x = n(gx + 0, gy + 0) - let n10x = n(gx + 1, gy + 0) - let n01x = n(gx + 0, gy + 1) - let n11x = n(gx + 1, gy + 1) - - // Bilinear interpolation (smooth, limited cancellation) - var fxForce = w00*n00x + w10*n10x + w01*n01x + w11*n11x - - // Optional local radial falloff from the nearest lattice center - // (invert the original: strongest at center) - let cx = (CGFloat(gx) + 0.5) * cellSize - let cy = (CGFloat(gy) + 0.5) * cellSize - let d = hypot(px - cx, py - cy) - let t = max(0.0, 1.0 - d / falloffDistance) - let weight = t * t - fxForce *= weight - - // Apply force directly to position (or integrate velocity if you have it) - itemLayer.position.x += fxForce * forceScale * dt - itemLayer.position.y -= dt * 100.0 + for (_, itemLayer) in self.itemLayers { + itemLayer.timeValue += dt + let itemPhase = (itemLayer.timeValue.truncatingRemainder(dividingBy: itemLayer.period) / itemLayer.period + itemLayer.phaseOffset).truncatingRemainder(dividingBy: 1.0) + let phaseFraction = sin(itemPhase * CGFloat.pi * 2.0) + itemLayer.position.x = itemLayer.baseX + phaseFraction * itemLayer.amplitude } } From 1bb50a3b36ff74a0220cbd2c4e1e17171340ae9a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 29 Oct 2025 16:46:49 +0400 Subject: [PATCH 6/6] Trigger build --- Random.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Random.txt b/Random.txt index ee6acb4db0..275fbd7cbd 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -eb0ad702db3f1d1c0afe18fdddf9fd4ba4562edd550a0b414b087d26caac1bda +0e50d6a10bc89732f2e475cc35994ebf5085443bef2d88cf3dbcb5d913d2128a