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

This commit is contained in:
Ilya Laktyushin 2025-11-04 14:48:05 +04:00
commit 0284687897
18 changed files with 227 additions and 76 deletions

View file

@ -233,6 +233,7 @@ public struct PresentationGroupCallState: Equatable {
public var isVideoEnabled: Bool
public var isVideoWatchersLimitReached: Bool
public var isMyVideoActive: Bool
public var isUnifiedStream: Bool
public init(
myPeerId: EnginePeer.Id,
@ -251,7 +252,8 @@ public struct PresentationGroupCallState: Equatable {
subscribedToScheduled: Bool,
isVideoEnabled: Bool,
isVideoWatchersLimitReached: Bool,
isMyVideoActive: Bool
isMyVideoActive: Bool,
isUnifiedStream: Bool
) {
self.myPeerId = myPeerId
self.networkState = networkState
@ -270,6 +272,7 @@ public struct PresentationGroupCallState: Equatable {
self.isVideoEnabled = isVideoEnabled
self.isVideoWatchersLimitReached = isVideoWatchersLimitReached
self.isMyVideoActive = isMyVideoActive
self.isUnifiedStream = isUnifiedStream
}
}

View file

@ -38,7 +38,8 @@ private extension PresentationGroupCallState {
subscribedToScheduled: subscribedToScheduled,
isVideoEnabled: false,
isVideoWatchersLimitReached: false,
isMyVideoActive: false
isMyVideoActive: false,
isUnifiedStream: false
)
}
}
@ -2664,6 +2665,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.stateValue.scheduleTimestamp = state.scheduleTimestamp
self.stateValue.isVideoEnabled = state.isVideoEnabled && otherParticipantsWithVideo < state.unmutedVideoLimit
self.stateValue.isVideoWatchersLimitReached = videoWatchingParticipants >= configuration.videoParticipantsMaxCount
self.stateValue.isUnifiedStream = state.isStream
self.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo(
id: callInfo.id,
@ -4050,15 +4052,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
}
public func sendMessage(fromId: PeerId?, randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
public func sendMessage(fromId: PeerId?, isAdmin: Bool, randomId: Int64? = nil, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
if let messagesContext = self.messagesContext {
messagesContext.send(fromId: fromId ?? self.joinAsPeerId, randomId: randomId, text: text, entities: entities, paidStars: paidStars)
messagesContext.send(fromId: fromId ?? self.joinAsPeerId, isAdmin: isAdmin, randomId: randomId, text: text, entities: entities, paidStars: paidStars)
}
}
public func sendStars(fromId: PeerId?, amount: Int64, delay: Bool) {
public func sendStars(fromId: PeerId?, isAdmin: Bool, amount: Int64, delay: Bool) {
if let messagesContext = self.messagesContext {
messagesContext.sendStars(fromId: fromId ?? self.joinAsPeerId, amount: amount, delay: delay)
messagesContext.sendStars(fromId: fromId ?? self.joinAsPeerId, isAdmin: isAdmin, amount: amount, delay: delay)
}
}

View file

@ -1218,7 +1218,8 @@ final class VideoChatScreenComponent: Component {
subscribedToScheduled: false,
isVideoEnabled: true,
isVideoWatchersLimitReached: false,
isMyVideoActive: false
isMyVideoActive: false,
isUnifiedStream: false
)
return .single((callState, invitedPeers.compactMap({ peer -> VideoChatScreenComponent.InvitedPeer? in
@ -1416,7 +1417,7 @@ final class VideoChatScreenComponent: Component {
return
}
let entities = generateTextEntities(text.string, enabledTypes: [.mention, .hashtag], currentEntities: generateChatInputTextEntities(text))
call.sendMessage(fromId: nil, randomId: randomId, text: text.string, entities: entities, paidStars: nil)
call.sendMessage(fromId: nil, isAdmin: false, randomId: randomId, text: text.string, entities: entities, paidStars: nil)
}
inputPanelView.clearSendMessageInput(updateState: true)
@ -3836,7 +3837,7 @@ final class VideoChatScreenComponent: Component {
guard case let .group(groupCall) = self.currentCall, let call = groupCall as? PresentationGroupCallImpl else {
return
}
call.sendMessage(fromId: nil, text: text, entities: entities, paidStars: nil)
call.sendMessage(fromId: nil, isAdmin: false, text: text, entities: entities, paidStars: nil)
})
}

View file

@ -118,7 +118,7 @@ enum AccountStateMutationOperation {
case UpdateGroupCallParticipants(id: Int64, accessHash: Int64, participants: [Api.GroupCallParticipant], version: Int32)
case UpdateGroupCall(peerId: PeerId?, call: Api.GroupCall)
case UpdateGroupCallChainBlocks(id: Int64, accessHash: Int64, subChainId: Int32, blocks: [Data], nextOffset: Int32)
case UpdateGroupCallMessage(id: Int64, authorId: PeerId, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?)
case UpdateGroupCallMessage(id: Int64, authorId: PeerId, isFromAdmin: Bool, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?)
case UpdateGroupCallOpaqueMessage(id: Int64, authorId: PeerId, data: Data)
case UpdateAutoremoveTimeout(peer: Api.Peer, value: CachedPeerAutoremoveTimeout.Value?)
case UpdateAttachMenuBots
@ -413,8 +413,8 @@ struct AccountMutableState {
self.addOperation(.UpdateGroupCallChainBlocks(id: id, accessHash: accessHash, subChainId: subChainId, blocks: blocks, nextOffset: nextOffset))
}
mutating func updateGroupCallMessage(id: Int64, authorId: PeerId, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) {
self.addOperation(.UpdateGroupCallMessage(id: id, authorId: authorId, messageId: messageId, text: text, date: date, paidMessageStars: paidMessageStars))
mutating func updateGroupCallMessage(id: Int64, authorId: PeerId, isFromAdmin: Bool, messageId: Int32, text: Api.TextWithEntities, date: Int32, paidMessageStars: Int64?) {
self.addOperation(.UpdateGroupCallMessage(id: id, authorId: authorId, isFromAdmin: isFromAdmin, messageId: messageId, text: text, date: date, paidMessageStars: paidMessageStars))
}
mutating func updateGroupCallOpaqueMessage(id: Int64, authorId: PeerId, data: Data) {

View file

@ -1686,8 +1686,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
case let .updateGroupCallMessage(call, message):
if case let .inputGroupCall(id, _) = call {
switch message {
case let .groupCallMessage(_, messageId, fromId, date, message, paidMessageStars):
updatedState.updateGroupCallMessage(id: id, authorId: fromId.peerId, messageId: messageId, text: message, date: date, paidMessageStars: paidMessageStars)
case let .groupCallMessage(flags, messageId, fromId, date, message, paidMessageStars):
updatedState.updateGroupCallMessage(id: id, authorId: fromId.peerId, isFromAdmin: (flags & (1 << 1)) != 0, messageId: messageId, text: message, date: date, paidMessageStars: paidMessageStars)
}
}
case let .updateGroupCallEncryptedMessage(call, fromId, encryptedMessage):
@ -4895,10 +4895,10 @@ func replayFinalState(
callId,
.state(update: GroupCallParticipantsContext.Update.StateUpdate(participants: participants, version: version))
))
case let .UpdateGroupCallMessage(callId, authorId, messageId, text, date, paidMessageStars):
case let .UpdateGroupCallMessage(callId, authorId, isFromAdmin, messageId, text, date, paidMessageStars):
switch text {
case let .textWithEntities(text, entities):
groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newPlaintextMessage(authorId: authorId, messageId: messageId, text: text, entities: messageTextEntitiesFromApiEntities(entities), timestamp: date, paidMessageStars: paidMessageStars)))
groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newPlaintextMessage(authorId: authorId, isFromAdmin: isFromAdmin, messageId: messageId, text: text, entities: messageTextEntitiesFromApiEntities(entities), timestamp: date, paidMessageStars: paidMessageStars)))
}
case let .UpdateGroupCallOpaqueMessage(callId, authorId, data):
groupCallMessageUpdates.append(GroupCallMessageUpdate(callId: callId, update: .newOpaqueMessage(authorId: authorId, data: data)))

View file

@ -3462,7 +3462,7 @@ func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) ->
struct GroupCallMessageUpdate {
enum Update {
case newPlaintextMessage(authorId: PeerId, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)
case newPlaintextMessage(authorId: PeerId, isFromAdmin: Bool, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)
case newOpaqueMessage(authorId: PeerId, data: Data)
}
@ -3703,17 +3703,19 @@ public final class GroupCallMessagesContext {
public let stableId: Int
public let isIncoming: Bool
public let author: EnginePeer?
public let isFromAdmin: Bool
public let text: String
public let entities: [MessageTextEntity]
public let date: Int32
public let lifetime: Int32
public let paidStars: Int64?
public init(id: Id, stableId: Int, isIncoming: Bool, author: EnginePeer?, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) {
public init(id: Id, stableId: Int, isIncoming: Bool, author: EnginePeer?, isFromAdmin: Bool, text: String, entities: [MessageTextEntity], date: Int32, lifetime: Int32, paidStars: Int64?) {
self.id = id
self.stableId = stableId
self.isIncoming = isIncoming
self.author = author
self.isFromAdmin = isFromAdmin
self.text = text
self.entities = entities
self.date = date
@ -3727,6 +3729,7 @@ public final class GroupCallMessagesContext {
stableId: self.stableId,
isIncoming: self.isIncoming,
author: self.author,
isFromAdmin: self.isFromAdmin,
text: self.text,
entities: self.entities,
date: self.date,
@ -3751,6 +3754,9 @@ public final class GroupCallMessagesContext {
if lhs.author != rhs.author {
return false
}
if lhs.isFromAdmin != rhs.isFromAdmin {
return false
}
if lhs.text != rhs.text {
return false
}
@ -3874,15 +3880,15 @@ public final class GroupCallMessagesContext {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var addedMessages: [(authorId: PeerId, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)] = []
var addedMessages: [(authorId: PeerId, isFromAdmin: Bool, messageId: Int32, text: String, entities: [MessageTextEntity], timestamp: Int32, paidMessageStars: Int64?)] = []
var addedOpaqueMessages: [(authorId: PeerId, data: Data)] = []
for update in updates {
if update.callId != self.callId {
continue
}
switch update.update {
case let .newPlaintextMessage(authorId, messageId, text, entities, timestamp, paidMessageStars):
addedMessages.append((authorId, messageId, text, entities, timestamp, paidMessageStars))
case let .newPlaintextMessage(authorId, isFromAdmin, messageId, text, entities, timestamp, paidMessageStars):
addedMessages.append((authorId, isFromAdmin, messageId, text, entities, timestamp, paidMessageStars))
case let .newOpaqueMessage(authorId, data):
if authorId != self.account.peerId {
addedOpaqueMessages.append((authorId, data))
@ -3933,6 +3939,7 @@ public final class GroupCallMessagesContext {
stableId: allocatedStableIds[messages.count],
isIncoming: addedOpaqueMessage.authorId != accountPeerId,
author: transaction.getPeer(addedOpaqueMessage.authorId).flatMap(EnginePeer.init),
isFromAdmin: false,
text: text,
entities: entities,
date: currentTime,
@ -3963,6 +3970,7 @@ public final class GroupCallMessagesContext {
stableId: allocatedStableIds[messages.count],
isIncoming: addedMessage.authorId != accountPeerId,
author: transaction.getPeer(addedMessage.authorId).flatMap(EnginePeer.init),
isFromAdmin: addedMessage.isFromAdmin,
text: addedMessage.text,
entities: addedMessage.entities,
date: addedMessage.timestamp,
@ -4212,7 +4220,7 @@ public final class GroupCallMessagesContext {
}
}
func send(fromId: EnginePeer.Id, randomId requestedRandomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
func send(fromId: EnginePeer.Id, isAdmin: Bool, randomId requestedRandomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId)
}
@ -4245,6 +4253,7 @@ public final class GroupCallMessagesContext {
stableId: stableId,
isIncoming: false,
author: fromPeer.flatMap(EnginePeer.init),
isFromAdmin: isAdmin,
text: text,
entities: entities,
date: currentTime,
@ -4417,7 +4426,7 @@ public final class GroupCallMessagesContext {
}
}
func sendStars(fromId: EnginePeer.Id, amount: Int64, delay: Bool) {
func sendStars(fromId: EnginePeer.Id, isAdmin: Bool, amount: Int64, delay: Bool) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId)
}
@ -4464,6 +4473,7 @@ public final class GroupCallMessagesContext {
stableId: message.stableId,
isIncoming: false,
author: message.author,
isFromAdmin: isAdmin,
text: message.text,
entities: message.entities,
date: currentTime,
@ -4478,6 +4488,7 @@ public final class GroupCallMessagesContext {
stableId: stableId,
isIncoming: false,
author: EnginePeer(fromPeer),
isFromAdmin: isAdmin,
text: "",
entities: [],
date: currentTime,
@ -4493,6 +4504,7 @@ public final class GroupCallMessagesContext {
stableId: message.stableId,
isIncoming: message.isIncoming,
author: message.author,
isFromAdmin: isAdmin,
text: message.text,
entities: message.entities,
date: currentTime,
@ -4507,6 +4519,7 @@ public final class GroupCallMessagesContext {
stableId: stableId,
isIncoming: false,
author: EnginePeer(fromPeer),
isFromAdmin: isAdmin,
text: "",
entities: [],
date: currentTime,
@ -4614,15 +4627,15 @@ public final class GroupCallMessagesContext {
})
}
public func send(fromId: EnginePeer.Id, randomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
public func send(fromId: EnginePeer.Id, isAdmin: Bool, randomId: Int64?, text: String, entities: [MessageTextEntity], paidStars: Int64?) {
self.impl.with { impl in
impl.send(fromId: fromId, randomId: randomId, text: text, entities: entities, paidStars: paidStars)
impl.send(fromId: fromId, isAdmin: isAdmin, randomId: randomId, text: text, entities: entities, paidStars: paidStars)
}
}
public func sendStars(fromId: EnginePeer.Id, amount: Int64, delay: Bool) {
public func sendStars(fromId: EnginePeer.Id, isAdmin: Bool, amount: Int64, delay: Bool) {
self.impl.with { impl in
impl.sendStars(fromId: fromId, amount: amount, delay: delay)
impl.sendStars(fromId: fromId, isAdmin: isAdmin, amount: amount, delay: delay)
}
}

View file

@ -2017,6 +2017,7 @@ private final class ChatSendStarsScreenComponent: Component {
stableId: 0,
isIncoming: false,
author: liveStreamMessage.myPeer,
isFromAdmin: false,
text: liveStreamMessage.text.string,
entities: entities,
date: 0,
@ -2032,6 +2033,7 @@ private final class ChatSendStarsScreenComponent: Component {
stableId: 0,
isIncoming: false,
author: reactData.myPeer,
isFromAdmin: false,
text: "",
entities: [],
date: 0,

View file

@ -145,6 +145,7 @@ public final class ChatTextInputPanelComponent: Component {
let rightAction: RightAction?
let sendAsConfiguration: SendAsConfiguration?
let placeholder: String
let isEnabled: Bool
let paidMessagePrice: StarsAmount?
let sendColor: UIColor?
let isSendDisabled: Bool
@ -166,6 +167,7 @@ public final class ChatTextInputPanelComponent: Component {
rightAction: RightAction?,
sendAsConfiguration: SendAsConfiguration?,
placeholder: String,
isEnabled: Bool,
paidMessagePrice: StarsAmount?,
sendColor: UIColor?,
isSendDisabled: Bool,
@ -186,6 +188,7 @@ public final class ChatTextInputPanelComponent: Component {
self.rightAction = rightAction
self.sendAsConfiguration = sendAsConfiguration
self.placeholder = placeholder
self.isEnabled = isEnabled
self.paidMessagePrice = paidMessagePrice
self.sendColor = sendColor
self.isSendDisabled = isSendDisabled
@ -228,6 +231,9 @@ public final class ChatTextInputPanelComponent: Component {
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.paidMessagePrice != rhs.paidMessagePrice {
return false
}
@ -829,6 +835,7 @@ public final class ChatTextInputPanelComponent: Component {
}
panelNode.customPlaceholder = component.placeholder
panelNode.customIsDisabled = !component.isEnabled
if let leftAction = component.leftAction {
switch leftAction.kind {

View file

@ -264,6 +264,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
public let attachmentButtonBackground: GlassBackgroundView
public let attachmentButtonIcon: GlassBackgroundView.ContentImageView
private var commentsButtonIcon: RasterizedCompositionMonochromeLayer?
private var commentsButtonCenterIcon: UIImageView?
private var commentsButtonContentsLayer: RasterizedCompositionImageLayer?
private var commentsButtonDotLayer: RasterizedCompositionImageLayer?
private var attachmentButtonUnseenIcon: UIImageView?
@ -393,6 +394,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
public var customPlaceholder: String?
public var customIsDisabled: Bool = false
public var customLeftAction: LeftAction?
public var customRightAction: RightAction?
public var customSendColor: UIColor?
@ -1495,6 +1497,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
}
if self.customIsDisabled {
sendingTextDisabled = true
}
self.sendingTextDisabled = sendingTextDisabled
self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled
@ -1941,7 +1946,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let customLeftAction = self.customLeftAction {
switch customLeftAction {
case let .toggleExpanded(_, isExpanded, hasUnseen):
let _ = isExpanded
let commentsButtonIcon: RasterizedCompositionMonochromeLayer
if let current = self.commentsButtonIcon {
commentsButtonIcon = current
@ -1951,6 +1955,17 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.attachmentButtonBackground.contentView.layer.addSublayer(commentsButtonIcon)
}
let commentsButtonCenterIcon: UIImageView
if let current = self.commentsButtonCenterIcon {
commentsButtonCenterIcon = current
} else {
commentsButtonCenterIcon = UIImageView()
self.commentsButtonCenterIcon = commentsButtonCenterIcon
self.attachmentButtonBackground.contentView.addSubview(commentsButtonCenterIcon)
commentsButtonCenterIcon.image = UIImage(bundleImageName: "Chat/Input/Text/CommensCross")?.withRenderingMode(.alwaysTemplate)
}
commentsButtonCenterIcon.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
let commentsButtonContentsLayer: RasterizedCompositionImageLayer
if let current = self.commentsButtonContentsLayer {
commentsButtonContentsLayer = current
@ -1975,6 +1990,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
commentsButtonIcon.position = iconFrame.center
commentsButtonIcon.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
commentsButtonCenterIcon.center = iconFrame.center
if let image = commentsButtonCenterIcon.image {
commentsButtonCenterIcon.bounds = image.size.centered(in: iconFrame)
}
transition.updateTransformRotation(view: commentsButtonCenterIcon, angle: !isExpanded ? (CGFloat.pi * 3.0 / 4.0) : 0.0)
commentsButtonIcon.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
commentsButtonIcon.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
@ -2036,6 +2057,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
attachmentButtonUnseenIcon?.removeFromSuperview()
})
}
if let commentsButtonCenterIcon = self.commentsButtonCenterIcon {
self.commentsButtonCenterIcon = nil
transition.updateTransformScale(layer: commentsButtonCenterIcon.layer, scale: 0.001)
transition.updateAlpha(layer: commentsButtonCenterIcon.layer, alpha: 0.0, completion: { [weak commentsButtonCenterIcon] _ in
commentsButtonCenterIcon?.removeFromSuperview()
})
}
}
var textFieldMinHeight: CGFloat = 33.0

View file

@ -174,11 +174,15 @@ public final class MessageInputPanelComponent: Component {
}
public struct LiveChatState: Equatable {
public var isEnabled: Bool
public var isExpanded: Bool
public var isEmpty: Bool
public var hasUnseenMessages: Bool
public init(isExpanded: Bool, hasUnseenMessages: Bool) {
public init(isEnabled: Bool, isExpanded: Bool, isEmpty: Bool, hasUnseenMessages: Bool) {
self.isEnabled = isEnabled
self.isExpanded = isExpanded
self.isEmpty = isEmpty
self.hasUnseenMessages = hasUnseenMessages
}
}
@ -929,35 +933,37 @@ public final class MessageInputPanelComponent: Component {
self.currentInputMode = inputMode
var inlineActions: [ChatTextInputPanelComponent.InlineAction] = []
if component.paidMessageAction != nil && self.textInputPanelExternalState.textInputState.inputText.length == 0 {
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .paidMessage,
action: { [weak self] in
guard let self else {
return
if component.liveChatState?.isEnabled == true {
if component.paidMessageAction != nil && self.textInputPanelExternalState.textInputState.inputText.length == 0 {
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .paidMessage,
action: { [weak self] in
guard let self else {
return
}
self.component?.paidMessageAction?()
}
self.component?.paidMessageAction?()
))
} else if let inputMode {
let mappedInputMode: ChatTextInputPanelComponent.InputMode
switch inputMode {
case .text:
mappedInputMode = .text
case .emoji:
mappedInputMode = .emoji
case .stickers:
mappedInputMode = .stickers
}
))
} else if let inputMode {
let mappedInputMode: ChatTextInputPanelComponent.InputMode
switch inputMode {
case .text:
mappedInputMode = .text
case .emoji:
mappedInputMode = .emoji
case .stickers:
mappedInputMode = .stickers
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .inputMode(mappedInputMode),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.inputModeAction?()
}
))
}
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .inputMode(mappedInputMode),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.inputModeAction?()
}
))
}
let placeholder: String
@ -1026,11 +1032,18 @@ public final class MessageInputPanelComponent: Component {
strings: component.strings,
chatPeerId: component.chatLocation?.peerId ?? component.context.account.peerId,
inlineActions: inlineActions,
leftAction: ChatTextInputPanelComponent.LeftAction(kind: .toggleExpanded(isVisible: component.liveChatState != nil, isExpanded: component.liveChatState?.isExpanded ?? true, hasUnseen: component.liveChatState?.hasUnseenMessages ?? false), action: { [weak self] in
leftAction: ChatTextInputPanelComponent.LeftAction(kind: .toggleExpanded(isVisible: component.liveChatState?.isEnabled == true, isExpanded: component.liveChatState?.isExpanded ?? true, hasUnseen: component.liveChatState?.hasUnseenMessages ?? false), action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.toggleLiveChatExpanded?()
guard let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View else {
return
}
if let liveChatState = component.liveChatState, liveChatState.isEmpty {
inputPanelView.activateInput()
} else {
component.toggleLiveChatExpanded?()
}
}),
rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.starStars?.totalStars ?? 0), isFilled: component.starStars?.hasOutgoingStars ?? false), action: { [weak self] sourceView in
guard let self, let component = self.component else {
@ -1043,8 +1056,10 @@ public final class MessageInputPanelComponent: Component {
}
component.sendStarsAction?(sourceView, true)
}),
sendAsConfiguration: sendAsConfiguration,
placeholder: placeholder,
sendAsConfiguration: component.liveChatState?.isEnabled == true ? sendAsConfiguration : nil,
//TODO:localize
placeholder: component.liveChatState?.isEnabled == true ? placeholder : "Comments are disabled",
isEnabled: component.liveChatState?.isEnabled == true,
paidMessagePrice: component.sendPaidMessageStars,
sendColor: component.sendPaidMessageStars.flatMap { value in
let params = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 }))

View file

@ -21,6 +21,7 @@ import AdminUserActionsSheet
final class StoryContentLiveChatComponent: Component {
final class External {
fileprivate(set) var isEmpty: Bool = false
fileprivate(set) var hasUnseenMessages: Bool = false
init() {
@ -268,6 +269,35 @@ final class StoryContentLiveChatComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
private func displayDeleteMessageConfirmation(id: GroupCallMessagesContext.Message.Id) {
guard let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: component.strings.Chat_DeleteMessagesConfirmation(1), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component, let call = component.call as? PresentationGroupCallImpl else {
return
}
call.deleteMessage(id: id, reportSpam: false)
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
component.controller()?.present(actionSheet, in: .window(.root))
}
private func displayDeleteMessageAndBan(id: GroupCallMessagesContext.Message.Id) {
Task { @MainActor [weak self] in
guard let self, let component = self.component else {
@ -281,6 +311,10 @@ final class StoryContentLiveChatComponent: Component {
guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else {
return
}
if message.isFromAdmin {
self.displayDeleteMessageConfirmation(id: id)
return
}
guard let author = message.author else {
return
}
@ -311,10 +345,23 @@ final class StoryContentLiveChatComponent: Component {
messageCount: 1,
deleteAllMessageCount: totalCount,
completion: { [weak self] result in
guard let self else {
guard let self, let component = self.component, let call = component.call as? PresentationGroupCallImpl else {
return
}
let _ = self
if result.deleteAll {
call.deleteAllMessages(authorId: author.id, reportSpam: result.reportSpam)
} else {
call.deleteMessage(id: id, reportSpam: result.reportSpam)
}
if result.ban {
if component.storyPeerId == component.context.account.peerId {
let _ = component.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: author.id, isBlocked: true).startStandalone()
} else {
let _ = component.context.engine.peers.updateChannelMemberBannedRights(peerId: component.storyPeerId, memberId: author.id, rights: TelegramChatBannedRights(flags: .banReadMessages, untilDate: Int32.max)).startStandalone()
}
}
}
),
customTheme: defaultDarkColorPresentationTheme
@ -414,15 +461,13 @@ final class StoryContentLiveChatComponent: Component {
}
c?.dismiss(completion: { [weak self] in
guard let self, let component = self.component else {
guard let self else {
return
}
if let call = component.call as? PresentationGroupCallImpl {
if isAdmin && !isMyMessage {
self.displayDeleteMessageAndBan(id: id)
} else {
call.deleteMessage(id: id, reportSpam: false)
}
if isAdmin && !isMyMessage {
self.displayDeleteMessageAndBan(id: id)
} else {
self.displayDeleteMessageConfirmation(id: id)
}
})
})))
@ -500,6 +545,7 @@ final class StoryContentLiveChatComponent: Component {
self.reactionStreamView?.add(peer: peer, count: Int(state.pendingMyStars - previousMessagesState.pendingMyStars))
}
}
component.external.isEmpty = state.messages.isEmpty
self.messagesState = state
if !self.isUpdating {

View file

@ -112,27 +112,35 @@ final class StoryItemContentComponent: Component {
struct LiveChatState {
var isExpanded: Bool
var isEmpty: Bool
var hasUnseenMessages: Bool
var areMessagesEnabled: Bool
var minMessagePrice: Int64?
var starStats: StarStats?
var isAdmin: Bool
init(isExpanded: Bool, hasUnseenMessages: Bool, areMessagesEnabled: Bool, minMessagePrice: Int64?, starStats: StarStats?) {
init(isExpanded: Bool, isEmpty: Bool, hasUnseenMessages: Bool, areMessagesEnabled: Bool, minMessagePrice: Int64?, starStats: StarStats?, isAdmin: Bool) {
self.isExpanded = isExpanded
self.isEmpty = isEmpty
self.hasUnseenMessages = hasUnseenMessages
self.areMessagesEnabled = areMessagesEnabled
self.minMessagePrice = minMessagePrice
self.starStats = starStats
self.isAdmin = isAdmin
}
}
private struct MediaStreamCallState: Equatable {
var areMessagesEnabled: Bool
var minMessagePrice: Int64?
var isAdmin: Bool
var isUnifiedStream: Bool
init(areMessagesEnabled: Bool, minMessagePrice: Int64?) {
init(areMessagesEnabled: Bool, minMessagePrice: Int64?, isAdmin: Bool, isUnifiedStream: Bool) {
self.areMessagesEnabled = areMessagesEnabled
self.minMessagePrice = minMessagePrice
self.isAdmin = isAdmin
self.isUnifiedStream = isUnifiedStream
}
}
@ -216,10 +224,12 @@ final class StoryItemContentComponent: Component {
return LiveChatState(
isExpanded: currentInfo.isChatExpanded,
isEmpty: self.liveChatExternal.isEmpty,
hasUnseenMessages: self.liveChatExternal.hasUnseenMessages,
areMessagesEnabled: mediaStreamCallState?.areMessagesEnabled ?? false,
minMessagePrice: mediaStreamCallState?.minMessagePrice,
starStats: starStats
starStats: starStats,
isAdmin: mediaStreamCallState?.isAdmin ?? false
)
}
@ -932,7 +942,9 @@ final class StoryItemContentComponent: Component {
if case .rtc = liveStream.kind, component.isEmbeddedInCamera {
} else {
var videoEndpointId: String?
if let mediaStreamCallVideoState = self.mediaStreamCallVideoState {
if let mediaStreamCallState = self.mediaStreamCallState, mediaStreamCallState.isUnifiedStream {
videoEndpointId = "unified"
} else if let mediaStreamCallVideoState = self.mediaStreamCallVideoState {
videoEndpointId = mediaStreamCallVideoState.videoEndpointId
}
let _ = mediaStream.update(
@ -1076,7 +1088,9 @@ final class StoryItemContentComponent: Component {
let mappedState = MediaStreamCallState(
areMessagesEnabled: state.messagesAreEnabled,
minMessagePrice: state.sendPaidMessageStars
minMessagePrice: state.sendPaidMessageStars,
isAdmin: state.canManageCall,
isUnifiedStream: state.isUnifiedStream
)
if self.mediaStreamCallState != mappedState {
self.mediaStreamCallState = mappedState

View file

@ -2968,7 +2968,9 @@ public final class StoryItemSetContainerComponent: Component {
if let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
if let liveChatStateValue = visibleItemView.liveChatState {
liveChatState = MessageInputPanelComponent.LiveChatState(
isEnabled: liveChatStateValue.areMessagesEnabled,
isExpanded: liveChatStateValue.isExpanded,
isEmpty: liveChatStateValue.isEmpty,
hasUnseenMessages: liveChatStateValue.hasUnseenMessages
)
starStats = liveChatStateValue.starStats.flatMap { starStats in

View file

@ -683,6 +683,7 @@ final class StoryItemSetContainerSendMessage {
if let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View {
var sendPaidMessageStars = self.currentLiveStreamMessageStars
var isAdmin = false
if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View {
if let liveChatStateValue = visibleItemView.liveChatState {
if let minMessagePrice = liveChatStateValue.minMessagePrice {
@ -694,6 +695,7 @@ final class StoryItemSetContainerSendMessage {
sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0)
}
}
isAdmin = liveChatStateValue.isAdmin
}
}
@ -756,7 +758,7 @@ final class StoryItemSetContainerSendMessage {
let entities = generateChatInputTextEntities(text)
call.sendMessage(fromId: self.currentSendAsPeer?.peer.id, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value)
call.sendMessage(fromId: self.currentSendAsPeer?.peer.id, isAdmin: isAdmin, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value)
component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id))
inputPanelView.clearSendMessageInput(updateState: true)
@ -4207,14 +4209,18 @@ final class StoryItemSetContainerSendMessage {
guard case .liveStream = component.slice.item.storyItem.media else {
return
}
var isAdmin = false
guard let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View else {
return
}
guard let call = itemView.mediaStreamCall else {
return
}
if let liveChatStateValue = itemView.liveChatState {
isAdmin = liveChatStateValue.isAdmin
}
call.sendStars(fromId: self.currentSendAsPeer?.peer.id, amount: Int64(count), delay: delay)
call.sendStars(fromId: self.currentSendAsPeer?.peer.id, isAdmin: isAdmin, amount: Int64(count), delay: delay)
}
func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) {

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "comments2.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "hidecomments.pdf",
"filename" : "comments1.pdf",
"idiom" : "universal"
}
],