Various improvements

This commit is contained in:
Isaac 2025-11-04 14:22:43 +04:00
parent 3ab4850306
commit ff32f34405
13 changed files with 173 additions and 49 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,

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

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,14 +112,16 @@ 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?, isAdmin: Bool) {
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
@ -132,11 +134,13 @@ final class StoryItemContentComponent: Component {
var areMessagesEnabled: Bool
var minMessagePrice: Int64?
var isAdmin: Bool
var isUnifiedStream: Bool
init(areMessagesEnabled: Bool, minMessagePrice: Int64?, isAdmin: Bool) {
init(areMessagesEnabled: Bool, minMessagePrice: Int64?, isAdmin: Bool, isUnifiedStream: Bool) {
self.areMessagesEnabled = areMessagesEnabled
self.minMessagePrice = minMessagePrice
self.isAdmin = isAdmin
self.isUnifiedStream = isUnifiedStream
}
}
@ -220,6 +224,7 @@ final class StoryItemContentComponent: Component {
return LiveChatState(
isExpanded: currentInfo.isChatExpanded,
isEmpty: self.liveChatExternal.isEmpty,
hasUnseenMessages: self.liveChatExternal.hasUnseenMessages,
areMessagesEnabled: mediaStreamCallState?.areMessagesEnabled ?? false,
minMessagePrice: mediaStreamCallState?.minMessagePrice,
@ -937,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(
@ -1082,7 +1089,8 @@ final class StoryItemContentComponent: Component {
let mappedState = MediaStreamCallState(
areMessagesEnabled: state.messagesAreEnabled,
minMessagePrice: state.sendPaidMessageStars,
isAdmin: state.canManageCall
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

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