mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
5ac77b9f31
30 changed files with 1125 additions and 161 deletions
|
|
@ -1 +1 @@
|
|||
eb0ad702db3f1d1c0afe18fdddf9fd4ba4562edd550a0b414b087d26caac1bda
|
||||
0e50d6a10bc89732f2e475cc35994ebf5085443bef2d88cf3dbcb5d913d2128a
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
- (instancetype _Nonnull)initWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2;
|
||||
|
||||
- (float)next;
|
||||
+ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
|||
|
|
@ -61,4 +61,34 @@ static uint32_t tausStep(const uint32_t z, const int32_t s1, const int32_t s2, c
|
|||
return old_seed;
|
||||
}
|
||||
|
||||
+ (float)randomWithSeed0:(NSUInteger)seed0 seed1:(NSUInteger)seed1 seed2:(NSUInteger)seed2 {
|
||||
uint32_t seed = ((uint32_t)seed0) * 1099087573U;
|
||||
uint32_t seedb = ((uint32_t)seed1) * 1099087573U;
|
||||
uint32_t seedc = ((uint32_t)seed2) * 1099087573U;
|
||||
|
||||
// Round 1: Randomise seed
|
||||
uint32_t z1 = tausStep(seed,13,19,12,429496729U);
|
||||
uint32_t z2 = tausStep(seed,2,25,4,4294967288U);
|
||||
uint32_t z3 = tausStep(seed,3,11,17,429496280U);
|
||||
uint32_t z4 = (1664525*seed + 1013904223U);
|
||||
|
||||
// Round 2: Randomise seed again using second seed
|
||||
uint32_t r1 = (z1^z2^z3^z4^seedb);
|
||||
|
||||
z1 = tausStep(r1,13,19,12,429496729U);
|
||||
z2 = tausStep(r1,2,25,4,4294967288U);
|
||||
z3 = tausStep(r1,3,11,17,429496280U);
|
||||
z4 = (1664525*r1 + 1013904223U);
|
||||
|
||||
// Round 3: Randomise seed again using third seed
|
||||
r1 = (z1^z2^z3^z4^seedc);
|
||||
|
||||
z1 = tausStep(r1,13,19,12,429496729U);
|
||||
z2 = tausStep(r1,2,25,4,4294967288U);
|
||||
z3 = tausStep(r1,3,11,17,429496280U);
|
||||
z4 = (1664525*r1 + 1013904223U);
|
||||
|
||||
return (z1^z2^z3^z4) * 2.3283064365387e-10f;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue