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

This commit is contained in:
Ilya Laktyushin 2025-10-29 17:21:34 +04:00
commit 5ac77b9f31
30 changed files with 1125 additions and 161 deletions

View file

@ -1 +1 @@
eb0ad702db3f1d1c0afe18fdddf9fd4ba4562edd550a0b414b087d26caac1bda
0e50d6a10bc89732f2e475cc35994ebf5085443bef2d88cf3dbcb5d913d2128a

View file

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

View file

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

View file

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

View file

@ -934,21 +934,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if isStream {
messageLifetime = Int32.max
if self.isStream {
var allowLiveChat = false
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data {
if let dev = data["dev"] as? Double, dev != 0.0 {
allowLiveChat = true
}
if data["ios_can_join_streams"] != nil {
allowLiveChat = true
}
}
if !allowLiveChat {
preconditionFailure()
}
}
}
self.messagesContext = accountContext.engine.messages.groupCallMessages(
@ -1689,7 +1674,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 +1693,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 +1774,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
isVideoEnabled: false,
unmutedVideoLimit: 0,
isStream: true,
isCreator: false
isCreator: false,
defaultSendAs: nil
)
} else {
activeCallInfo = nil
@ -2691,7 +2679,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(
@ -4060,15 +4049,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)
}
}

View file

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

View file

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

View file

@ -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) {
@ -3693,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]
@ -3700,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
@ -3715,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,
@ -3734,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
}
@ -3836,7 +3848,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) {
@ -3915,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,
@ -3944,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,
@ -4225,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,
@ -4269,6 +4284,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,
@ -4278,7 +4303,7 @@ public final class GroupCallMessagesContext {
entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())
),
allowPaidStars: paidStars,
sendAs: nil
sendAs: sendAs
)) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in
guard let self else {
return
@ -4323,6 +4348,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,
@ -4332,7 +4364,7 @@ public final class GroupCallMessagesContext {
entities: []
),
allowPaidStars: pendingSendStars.amount,
sendAs: nil
sendAs: sendAs
)) |> deliverOn(self.queue)).startStrict(next: { [weak self] updates in
guard let self else {
return
@ -4349,7 +4381,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
@ -4386,7 +4418,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
}
@ -4397,7 +4429,7 @@ public final class GroupCallMessagesContext {
totalAmount = pendingSendStarsValue.amount + amount
self.pendingSendStars = (
fromId: fromId,
fromPeer: fromPeer,
messageId: pendingSendStarsValue.messageId,
amount: totalAmount
)
@ -4408,7 +4440,7 @@ public final class GroupCallMessagesContext {
arc4random_buf(&randomId, 8)
self.pendingSendStars = (
fromId: fromId,
fromPeer: fromPeer,
messageId: randomId,
amount: amount
)
@ -4426,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,
@ -4439,7 +4472,8 @@ public final class GroupCallMessagesContext {
state.messages.append(Message(
id: Message.Id(space: .local, id: pendingSendStarsValue.messageId),
stableId: stableId,
author: fromPeer.flatMap(EnginePeer.init),
isIncoming: false,
author: EnginePeer(fromPeer),
text: "",
entities: [],
date: currentTime,
@ -4453,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,
@ -4466,7 +4501,8 @@ public final class GroupCallMessagesContext {
state.pinnedMessages.append(Message(
id: Message.Id(space: .local, id: pendingSendStarsValue.messageId),
stableId: stableId,
author: fromPeer.flatMap(EnginePeer.init),
isIncoming: false,
author: EnginePeer(fromPeer),
text: "",
entities: [],
date: currentTime,

View file

@ -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<PeerId>()
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<Api.channels.SendAsPeers?, NoError> 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<PeerId>()
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)) }
}
}
}
}
}

View file

@ -520,23 +520,27 @@ public enum Stories {
case timestamp
case expirationTimestamp
case isCloseFriends = "clf"
case isLiveItem = "liv"
}
public let id: Int32
public let timestamp: Int32
public let expirationTimestamp: Int32
public let isCloseFriends: Bool
public let isLiveItem: Bool
public init(
id: Int32,
timestamp: Int32,
expirationTimestamp: Int32,
isCloseFriends: Bool
isCloseFriends: Bool,
isLiveItem: Bool
) {
self.id = id
self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp
self.isCloseFriends = isCloseFriends
self.isLiveItem = isLiveItem
}
public init(from decoder: Decoder) throws {
@ -546,6 +550,7 @@ public enum Stories {
self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
self.expirationTimestamp = try container.decode(Int32.self, forKey: .expirationTimestamp)
self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false
self.isLiveItem = try container.decodeIfPresent(Bool.self, forKey: .isLiveItem) ?? false
}
public func encode(to encoder: Encoder) throws {
@ -555,6 +560,7 @@ public enum Stories {
try container.encode(self.timestamp, forKey: .timestamp)
try container.encode(self.expirationTimestamp, forKey: .expirationTimestamp)
try container.encode(self.isCloseFriends, forKey: .isCloseFriends)
try container.encode(self.isLiveItem, forKey: .isLiveItem)
}
public static func ==(lhs: Placeholder, rhs: Placeholder) -> Bool {
@ -570,6 +576,9 @@ public enum Stories {
if lhs.isCloseFriends != rhs.isCloseFriends {
return false
}
if lhs.isLiveItem != rhs.isLiveItem {
return false
}
return true
}
}
@ -628,8 +637,8 @@ public enum Stories {
switch self {
case let .item(item):
return item.media is TelegramMediaLiveStream
case .placeholder:
return false
case let .placeholder(placeholder):
return placeholder.isLiveItem
}
}
@ -2326,7 +2335,8 @@ extension Stories.StoredItem {
}
case let .storyItemSkipped(flags, id, date, expireDate):
let isCloseFriends = (flags & (1 << 8)) != 0
self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends))
let isLiveItem = (flags & (1 << 9)) != 0
self = .placeholder(Stories.Placeholder(id: id, timestamp: date, expirationTimestamp: expireDate, isCloseFriends: isCloseFriends, isLiveItem: isLiveItem))
case .storyItemDeleted:
return nil
}

View file

@ -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<Never, UpdatePeerAllowedReactionsError> {
return _internal_updatePeerReactionSettings(account: account, peerId: peerId, reactionSettings: reactionSettings)
}

View file

@ -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": [],

View file

@ -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",
],
)

View file

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

View file

@ -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: [],
@ -2799,7 +2803,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
}
}
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal<InitialData?, NoError> {
public static func initialData(context: AccountContext, peerId: EnginePeer.Id, myPeer: EnginePeer? = nil, reactSubject: ReactSubject, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal<InitialData?, NoError> {
let balance: Signal<StarsAmount?, NoError>
if let starsContext = context.starsContext {
balance = starsContext.state
@ -2871,7 +2875,8 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer {
defaultPrivacyPeer
)
|> map { peerAndTopPeerMap, balance, channelsForPublicReaction, defaultPrivacyPeer -> InitialData? in
let (peer, myPeer, topPeerMap) = peerAndTopPeerMap
let (peer, myPeerValue, topPeerMap) = peerAndTopPeerMap
let myPeer = myPeer ?? myPeerValue
guard let peer, let myPeer else {
return nil
}

View file

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

View file

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

View file

@ -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<Empty>()
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<Empty>, 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<Empty>, 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<Empty>?
private var badge: ComponentView<Empty>?
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<Empty>
var textTransition = transition
if let current = self.text {
text = current
let badge: ComponentView<Empty>
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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,320 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
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 = 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
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()
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)))
})!
}
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 {
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
self.allowsEdgeAntialiasing = true
}
override init(layer: Any) {
self.amplitude = 0.0
self.period = 0.0
self.phaseOffset = 0.0
self.baseX = 0.0
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 + 0.2 {
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) {
let id = self.nextId
self.nextId += 1
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.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
}
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()
}
})
})
}
private func updatePhysics() {
let timestamp = CACurrentMediaTime()
let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp))
self.previousPhysicsTimestamp = timestamp
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
}
}
func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) {
transition.setFrame(layer: self.itemLayerContainer, frame: CGRect(origin: sourcePoint, size: CGSize()))
}
}

View file

@ -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)
@ -458,6 +476,8 @@ final class StoryContentLiveChatComponent: Component {
private let listState = AsyncListComponent.ExternalState()
private let list = ComponentView<Empty>()
private let listShadowView: UIView
private var reactionStreamView: LiveChatReactionStreamView?
private var component: StoryContentLiveChatComponent?
private weak var state: EmptyComponentState?
@ -808,20 +828,25 @@ 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 }) {
if let component = self.component, let previousMessagesState = self.messagesState {
if !self.isChatExpanded {
var hasNewMessages = false
for message in state.messages {
if message.isIncoming && !previousMessagesState.messages.contains(where: { $0.id == message.id }) {
hasNewMessages = true
break
if message.isIncoming, let paidStars = message.paidStars, let author = message.author {
self.reactionStreamView?.add(peer: author, count: Int(paidStars))
}
}
}
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
@ -973,6 +998,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
}
}

View file

@ -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,21 @@ public final class StoryItemSetContainerComponent: Component {
sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0)
}
}
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)
}
)
}
}
}
@ -3244,7 +3260,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)

View file

@ -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: [SendAsPeer])?
var currentSendAsPeer: SendAsPeer?
var isSelectingSendAsPeer: Bool = false
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<ChatEntityKeyboardInputNode.InputData, NoError>) {
self.context = context
func setup(component: StoryItemSetContainerComponent, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>) {
self.context = component.context
self.inputPanelExternalState = inputPanelExternalState
self.view = view
@ -208,6 +214,56 @@ 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: [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)
}
self.sendAsData = (
isPremium: isPremium,
availablePeers: availablePeers
)
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.currentSendAsPeer != nil {
self.currentSendAsPeer = nil
if !view.isUpdatingComponent {
view.state?.updated(transition: .spring(duration: 0.4))
}
}
}
})
}
}
func toggleInputMode() {
@ -281,7 +337,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,
@ -698,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)
@ -3952,13 +4008,14 @@ 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 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 +4027,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 +4071,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)
@ -4143,7 +4205,91 @@ final class StoryItemSetContainerSendMessage {
return
}
call.sendStars(amount: Int64(count), delay: delay)
call.sendStars(fromId: self.currentSendAsPeer?.peer.id, amount: Int64(count), delay: delay)
}
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
}
let focusedItem = component.slice.item
guard let peerId = focusedItem.peerId else {
return
}
let isPremium = sendAsData.isPremium
var items: [ContextMenuItem] = []
items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false))
items.append(.custom(ChatSendAsPeerListContextItem(
context: component.context,
chatPeerId: peerId,
peers: sendAsData.availablePeers,
selectedPeerId: currentSendAsPeer.peer.id,
isPremium: isPremium,
action: { [weak self, weak view] peer in
guard let self, let view else {
return
}
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 {
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
}
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))
}
}

View file

@ -123,6 +123,7 @@ import ChatMediaInputStickerGridItem
import AdsInfoScreen
import PostSuggestionsSettingsScreen
import ChatSendStarsScreen
import ChatSendAsContextMenu
extension ChatControllerImpl {
func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, 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()

View file

@ -8,6 +8,7 @@
- (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2;
- (float)next;
+ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2;
@end

View file

@ -61,4 +61,34 @@ static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, c
return old_seed;
}
+ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 {
uint32_t seed = ((uint32_t)seed0) * 1099087573U;
uint32_t seedb = ((uint32_t)seed1) * 1099087573U;
uint32_t seedc = ((uint32_t)seed2) * 1099087573U;
// Round 1: Randomise seed
uint32_t z1 = tausStep(seed,13,19,12,429496729U);
uint32_t z2 = tausStep(seed,2,25,4,4294967288U);
uint32_t z3 = tausStep(seed,3,11,17,429496280U);
uint32_t z4 = (1664525*seed + 1013904223U);
// Round 2: Randomise seed again using second seed
uint32_t r1 = (z1^z2^z3^z4^seedb);
z1 = tausStep(r1,13,19,12,429496729U);
z2 = tausStep(r1,2,25,4,4294967288U);
z3 = tausStep(r1,3,11,17,429496280U);
z4 = (1664525*r1 + 1013904223U);
// Round 3: Randomise seed again using third seed
r1 = (z1^z2^z3^z4^seedc);
z1 = tausStep(r1,13,19,12,429496729U);
z2 = tausStep(r1,2,25,4,4294967288U);
z3 = tausStep(r1,3,11,17,429496280U);
z4 = (1664525*r1 + 1013904223U);
return (z1^z2^z3^z4) * 2.3283064365387e-10f;
}
@end