This commit is contained in:
Isaac 2025-11-11 18:07:34 +08:00
parent 99cb9d4832
commit 3d51d83e89
12 changed files with 305 additions and 171 deletions

View file

@ -525,7 +525,7 @@ public protocol PresentationGroupCall: AnyObject {
func disableScreencast()
func switchVideoCamera()
func updateDefaultParticipantsAreMuted(isMuted: Bool)
func updateMessagesEnabled(isEnabled: Bool)
func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?)
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
func setSuspendVideoChannelRequests(_ value: Bool)

View file

@ -1156,11 +1156,6 @@ public final class AvatarNode: ASDisplayNode {
public let contentNode: ContentNode
private var storyIndicator: ComponentView<Empty>?
private var contentMaskView: UIView?
private var storyIndicatorMaskView: UIView?
private var liveBadgeMaskView: UIImageView?
private var liveBadgeStoryIndicatorMaskView: UIImageView?
private var liveBadgeView: UIImageView?
public private(set) var storyPresentationParams: StoryPresentationParams?
private var loadingStatuses = Bag<Disposable>()
@ -1188,7 +1183,6 @@ public final class AvatarNode: ASDisplayNode {
}
public private(set) var storyStats: StoryStats?
public var displayLiveBadge: Bool = false
public var font: UIFont {
get {
@ -1504,124 +1498,6 @@ public final class AvatarNode: ASDisplayNode {
}
}
}
if self.displayLiveBadge, let storyStats = self.storyStats, storyStats.hasLiveItems {
let contentMaskView: UIView
let storyIndicatorMaskView: UIView
let liveBadgeMaskView: UIImageView
let liveBadgeStoryIndicatorMaskView: UIImageView
let liveBadgeView: UIImageView
var liveBadgeTransition = transition
if let current = self.contentMaskView {
contentMaskView = current
} else {
liveBadgeTransition = liveBadgeTransition.withAnimation(.none)
contentMaskView = UIView()
contentMaskView.backgroundColor = .white
if let filter = CALayer.luminanceToAlpha() {
contentMaskView.layer.filters = [filter]
}
self.contentMaskView = contentMaskView
self.contentNode.view.mask = contentMaskView
}
if let current = self.storyIndicatorMaskView {
storyIndicatorMaskView = current
} else {
storyIndicatorMaskView = UIView()
storyIndicatorMaskView.backgroundColor = .white
if let filter = CALayer.luminanceToAlpha() {
storyIndicatorMaskView.layer.filters = [filter]
}
self.storyIndicatorMaskView = storyIndicatorMaskView
self.storyIndicator?.view?.mask = storyIndicatorMaskView
}
if let current = self.liveBadgeView {
liveBadgeView = current
} else {
liveBadgeView = UIImageView()
//TODO:localize
let liveString = NSAttributedString(string: "LIVE", font: Font.semibold(10.0), textColor: .white)
let liveStringBounds = liveString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let liveBadgeSize = CGSize(width: ceil(liveStringBounds.width) + 4.0 * 2.0, height: ceil(liveStringBounds.height) + 2.0 * 2.0)
liveBadgeView.image = generateImage(liveBadgeSize, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0xFF2D55).cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath)
context.fillPath()
liveString.draw(at: CGPoint(x: floorToScreenPixels((size.width - liveStringBounds.width) * 0.5), y: floorToScreenPixels((size.height - liveStringBounds.height) * 0.5)))
})
self.view.addSubview(liveBadgeView)
self.liveBadgeView = liveBadgeView
}
if let current = self.liveBadgeMaskView {
liveBadgeMaskView = current
} else {
liveBadgeMaskView = UIImageView()
self.liveBadgeMaskView = liveBadgeMaskView
contentMaskView.addSubview(liveBadgeMaskView)
if let image = liveBadgeView.image {
liveBadgeMaskView.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .black)
}
}
if let current = self.liveBadgeStoryIndicatorMaskView {
liveBadgeStoryIndicatorMaskView = current
} else {
liveBadgeStoryIndicatorMaskView = UIImageView()
self.liveBadgeStoryIndicatorMaskView = liveBadgeStoryIndicatorMaskView
storyIndicatorMaskView.addSubview(liveBadgeStoryIndicatorMaskView)
if let image = liveBadgeView.image {
liveBadgeStoryIndicatorMaskView.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .black)
}
}
liveBadgeTransition.setFrame(view: contentMaskView, frame: CGRect(origin: CGPoint(), size: size))
liveBadgeTransition.setFrame(view: storyIndicatorMaskView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -6.0, dy: -6.0))
if let image = liveBadgeView.image {
let badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: size.height + 5.0 - image.size.height), size: image.size)
liveBadgeTransition.setFrame(view: liveBadgeView, frame: badgeFrame)
liveBadgeTransition.setFrame(view: liveBadgeMaskView, frame: self.contentNode.view.convert(badgeFrame.insetBy(dx: -2.0, dy: -2.0), from: self.view))
liveBadgeTransition.setFrame(view: liveBadgeStoryIndicatorMaskView, frame: badgeFrame.insetBy(dx: -2.0, dy: -2.0).offsetBy(dx: 1.0, dy: 0.0))
}
} else {
if let contentMaskView = self.contentMaskView {
self.contentMaskView = nil
contentMaskView.removeFromSuperview()
self.contentNode.view.mask = nil
}
if let storyIndicatorMaskView = self.storyIndicatorMaskView {
self.storyIndicatorMaskView = nil
storyIndicatorMaskView.removeFromSuperview()
self.storyIndicator?.view?.mask = nil
}
if let liveBadgeMaskView = self.liveBadgeMaskView {
self.liveBadgeMaskView = nil
liveBadgeMaskView.removeFromSuperview()
}
if let liveBadgeStoryIndicatorMaskView = self.liveBadgeStoryIndicatorMaskView {
self.liveBadgeStoryIndicatorMaskView = nil
liveBadgeStoryIndicatorMaskView.removeFromSuperview()
}
if let liveBadgeView = self.liveBadgeView {
self.liveBadgeView = nil
liveBadgeView.removeFromSuperview()
}
}
}
public func cancelLoading() {

View file

@ -1348,6 +1348,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let onlineNode: PeerOnlineMarkerNode
var avatarTimerBadge: AvatarBadgeView?
private var starView: StarView?
var avatarLiveBadge: (outline: UIImageView, foreground: UIImageView)?
let pinnedIconNode: ASImageNode
var secretIconNode: ASImageNode?
var verifiedIconView: ComponentHostView<Empty>?
@ -1601,7 +1602,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.avatarContainerNode = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.avatarNode.displayLiveBadge = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
@ -1802,6 +1802,92 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
), transition: .immediate)
self.avatarNode.isUserInteractionEnabled = storyState != nil
if let stats = storyState?.stats, stats.hasLiveItems {
if self.avatarLiveBadge == nil {
let avatarLiveBadge: (outline: UIImageView, foreground: UIImageView) = (UIImageView(), UIImageView())
self.avatarLiveBadge = avatarLiveBadge
self.avatarNode.view.addSubview(avatarLiveBadge.outline)
self.avatarNode.view.addSubview(avatarLiveBadge.foreground)
//TODO:localize
let liveString = NSAttributedString(string: "LIVE", font: Font.semibold(10.0), textColor: .white)
let liveStringBounds = liveString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let liveBadgeSize = CGSize(width: ceil(liveStringBounds.width) + 4.0 * 2.0, height: ceil(liveStringBounds.height) + 2.0 * 2.0)
avatarLiveBadge.foreground.image = generateImage(liveBadgeSize, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0xFF2D55).cgColor)
func roundedRectCgPath(roundRect rect: CGRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) -> CGPath {
let path = CGMutablePath()
let topLeft = rect.origin
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
if topLeftRadius != .zero {
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
if topRightRadius != .zero {
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
} else {
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
}
if bottomRightRadius != .zero {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
} else {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
}
if bottomLeftRadius != .zero {
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
} else {
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
}
if topLeftRadius != .zero {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
path.closeSubpath()
return path
}
let radius = size.height * 0.5
context.addPath(roundedRectCgPath(roundRect: CGRect(origin: CGPoint(), size: size), topLeftRadius: radius, topRightRadius: radius, bottomLeftRadius: radius, bottomRightRadius: radius))
context.fillPath()
liveString.draw(at: CGPoint(x: floorToScreenPixels((size.width - liveStringBounds.width) * 0.5), y: floorToScreenPixels((size.height - liveStringBounds.height) * 0.5)))
})
if let image = avatarLiveBadge.foreground.image {
avatarLiveBadge.outline.image = generateStretchableFilledCircleImage(diameter: image.size.height + 2.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
}
} else {
if let avatarLiveBadge = self.avatarLiveBadge {
self.avatarLiveBadge = nil
avatarLiveBadge.outline.removeFromSuperview()
avatarLiveBadge.foreground.removeFromSuperview()
}
}
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
@ -2027,6 +2113,21 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition)
}
}
if let item = self.item {
if let avatarLiveBadge = self.avatarLiveBadge {
let effectiveBackgroundColor: UIColor
if item.isPinned {
effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor
} else {
effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor
}
let highlightAlpha = self.highlightedBackgroundNode.supernode == nil ? 0.0 : self.highlightedBackgroundNode.alpha
let outlineColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor.mixedWith(effectiveBackgroundColor, alpha: 1.0 - highlightAlpha)
transition.updateTintColor(view: avatarLiveBadge.outline, color: outlineColor)
}
}
}
override public func tapped() {
@ -4019,6 +4120,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame)
if let avatarLiveBadge = strongSelf.avatarLiveBadge, let iconImage = avatarLiveBadge.foreground.image, let outlineImage = avatarLiveBadge.outline.image {
let outlineInset = (outlineImage.size.height - iconImage.size.height) * 0.5
let liveBadgeFrame = CGRect(origin: CGPoint(x: floor((avatarFrame.width - iconImage.size.width) * 0.5), y: avatarFrame.height + 5.0 - iconImage.size.height), size: iconImage.size)
transition.updateFrame(view: avatarLiveBadge.foreground, frame: liveBadgeFrame)
transition.updateFrame(view: avatarLiveBadge.outline, frame: liveBadgeFrame.insetBy(dx: -outlineInset, dy: -outlineInset))
}
let onlineInlineNavigationFraction: CGFloat = item.interaction.inlineNavigationLocation?.progress ?? 0.0
transition.updateAlpha(node: strongSelf.onlineNode, alpha: 1.0 - onlineInlineNavigationFraction)
transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001)

View file

@ -3988,8 +3988,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted)
}
public func updateMessagesEnabled(isEnabled: Bool) {
self.participantsContext?.updateMessagesEnabled(isEnabled: isEnabled)
public func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?) {
self.participantsContext?.updateMessagesEnabled(isEnabled: isEnabled, sendPaidMessageStars: sendPaidMessageStars)
}
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {

View file

@ -163,7 +163,7 @@ extension VideoChatCall {
func setMessagesEnabled(isEnabled: Bool) {
switch self {
case let .group(group):
group.updateMessagesEnabled(isEnabled: isEnabled)
group.updateMessagesEnabled(isEnabled: isEnabled, sendPaidMessageStars: nil)
case .conferenceSource:
break
}

View file

@ -2643,13 +2643,18 @@ public final class GroupCallParticipantsContext {
}))
}
public func updateMessagesEnabled(isEnabled: Bool) {
if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled {
public func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?) {
if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled && self.stateValue.state.messagesAreEnabled.sendPaidMessagesStars == sendPaidMessageStars {
return
}
self.stateValue.state.messagesAreEnabled.isEnabled = isEnabled
self.stateValue.state.messagesAreEnabled.sendPaidMessagesStars = sendPaidMessageStars
self.updateMessagesEnabledDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse, sendPaidMessagesStars: nil))
var flags: Int32 = 1 << 2
if sendPaidMessageStars != nil {
flags |= 1 << 3
}
self.updateMessagesEnabledDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse, sendPaidMessagesStars: sendPaidMessageStars))
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let strongSelf = self else {
return

View file

@ -349,7 +349,14 @@ final class LiveStreamSettingsScreenComponent: Component {
contentHeight += sectionSpacing
}
if screenState.sendAsPeerId?.namespace != Namespaces.Peer.CloudChannel {
var displayPrivacy = true
if screenState.sendAsPeerId?.namespace == Namespaces.Peer.CloudChannel {
displayPrivacy = false
} else if screenState.call != nil && screenState.isEdit {
displayPrivacy = false
}
if displayPrivacy {
var privacySectionItems: [AnyComponentWithIdentity<Empty>] = []
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
@ -659,7 +666,7 @@ final class LiveStreamSettingsScreenComponent: Component {
})
}
if !screenState.isEdit {
if !screenState.isEdit || (screenState.call != nil && screenState.isEdit) {
let externalStreamSectionItems = [AnyComponentWithIdentity(id: 0, component: AnyComponent(
ListActionItemComponent(
theme: theme,
@ -703,6 +710,7 @@ final class LiveStreamSettingsScreenComponent: Component {
}
//TODO:localize
var settingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
settingsSectionItems.append(AnyComponentWithIdentity(id: "comments", component: AnyComponent(ListActionItemComponent(
theme: theme,
@ -725,26 +733,29 @@ final class LiveStreamSettingsScreenComponent: Component {
action: nil
))))
settingsSectionItems.append(AnyComponentWithIdentity(id: "screenshots", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Screenshots",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
if !(screenState.call != nil && screenState.isEdit) {
//TODO:localize
settingsSectionItems.append(AnyComponentWithIdentity(id: "screenshots", component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Screenshots",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
maximumNumberOfLines: 1
)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: !screenState.isForwardingDisabled, action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.stateContext.isForwardingDisabled = !component.stateContext.isForwardingDisabled
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: !screenState.isForwardingDisabled, action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.stateContext.isForwardingDisabled = !component.stateContext.isForwardingDisabled
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
}
let settingsSectionSize = self.settingsSection.update(
transition: transition,
@ -1025,7 +1036,7 @@ final class LiveStreamSettingsScreenComponent: Component {
public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
public enum Mode {
case create(sendAsPeerId: EnginePeer.Id?, isCustomTarget: Bool, privacy: EngineStoryPrivacy, allowComments: Bool, isForwardingDisabled: Bool, pin: Bool, paidMessageStars: Int64)
case edit(PresentationGroupCall)
case edit(call: PresentationGroupCall, displayPrivacy: Bool)
}
public struct Result {
@ -1088,6 +1099,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
}
final class State {
var call: PresentationGroupCall?
var isEdit: Bool
var maxPaidMessageStars: Int64
var sendAsPeerId: EnginePeer.Id?
@ -1105,6 +1117,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
var grayListPeers: [EnginePeer]
init(
call: PresentationGroupCall?,
isEdit: Bool,
maxPaidMessageStars: Int64,
sendAsPeerId: EnginePeer.Id?,
@ -1121,6 +1134,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
closeFriendsPeers: [EnginePeer],
grayListPeers: [EnginePeer]
) {
self.call = call
self.isEdit = isEdit
self.maxPaidMessageStars = maxPaidMessageStars
self.sendAsPeerId = sendAsPeerId
@ -1140,6 +1154,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
}
public final class StateContext {
let mode: LiveStreamSettingsScreen.Mode
let blockedPeersContext: BlockedPeersContext?
var stateValue: State?
@ -1211,6 +1226,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
adminedChannels: Signal<[EnginePeer], NoError>,
blockedPeersContext: BlockedPeersContext?
) {
self.mode = mode
self.blockedPeersContext = blockedPeersContext
let grayListPeers: Signal<[EnginePeer], NoError>
@ -1337,6 +1353,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
savedSelectedPeers[.contacts] = contactsPeers
savedSelectedPeers[.nobody] = selectedPeers
let call: PresentationGroupCall?
let isEdit: Bool
let maxPaidMessageStars: Int64 = 10000
let sendAsPeerId: EnginePeer.Id?
@ -1348,6 +1365,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
let paidMessageStars: Int64
if let current = self.stateValue {
call = current.call
isEdit = current.isEdit
sendAsPeerId = current.sendAsPeerId
isCustomTarget = current.isCustomTarget
@ -1359,6 +1377,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
} else {
switch mode {
case let .create(sendAsPeerIdValue, isCustomTargetValue, privacyValue, allowCommentsValue, isForwardingDisabledValue, pinValue, paidMessageStarsValue):
call = nil
isEdit = false
sendAsPeerId = sendAsPeerIdValue
isCustomTarget = isCustomTargetValue
@ -1367,8 +1386,8 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
isForwardingDisabled = isForwardingDisabledValue
pin = pinValue
paidMessageStars = paidMessageStarsValue
case let .edit(call):
let _ = call
case let .edit(callValue, _):
call = callValue
isEdit = true
sendAsPeerId = nil
isCustomTarget = false
@ -1381,6 +1400,7 @@ public class LiveStreamSettingsScreen: ViewControllerComponentContainer {
}
let state = State(
call: call,
isEdit: isEdit,
maxPaidMessageStars: maxPaidMessageStars,
sendAsPeerId: sendAsPeerId,

View file

@ -4249,23 +4249,13 @@ public final class StoryItemSetContainerComponent: Component {
var currentLeftInfoItem: InfoItem?
if focusedItem != nil {
let leftInfoComponent = AnyComponent(StoryAvatarInfoComponent(context: component.context, peer: component.slice.effectivePeer, isLiveStream: isLiveStream))
if let leftInfoItem = self.leftInfoItem, leftInfoItem.component == leftInfoComponent {
if let leftInfoItem = self.leftInfoItem {
currentLeftInfoItem = leftInfoItem
} else {
currentLeftInfoItem = InfoItem(component: leftInfoComponent)
}
}
if let leftInfoItem = self.leftInfoItem, currentLeftInfoItem?.component != leftInfoItem.component {
self.leftInfoItem = nil
if let view = leftInfoItem.view.view {
view.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
}
var currentCenterInfoItem: InfoItem?
if let focusedItem {
var counters: StoryAuthorInfoComponent.Counters?
@ -4295,7 +4285,7 @@ public final class StoryItemSetContainerComponent: Component {
isLiveStream: isLiveStream,
customSubtitle: customSubtitle
))
if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent {
if let centerInfoItem = self.centerInfoItem {
currentCenterInfoItem = centerInfoItem
} else {
currentCenterInfoItem = InfoItem(component: centerInfoComponent)
@ -6644,6 +6634,19 @@ public final class StoryItemSetContainerComponent: Component {
}
if case .liveStream = component.slice.item.storyItem.media {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Live Settings", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.sendMessageContext.displayLiveStreamSettings(view: self)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
@ -6921,6 +6924,21 @@ public final class StoryItemSetContainerComponent: Component {
component.controller()?.present(tooltipScreen, in: .current)
})))
if channel.hasPermission(.postStories) {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Live Settings", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.sendMessageContext.displayLiveStreamSettings(view: self)
})))
}
if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)

View file

@ -53,6 +53,7 @@ import ChatMessagePaymentAlertController
import ChatSendStarsScreen
import AnimatedTextComponent
import ChatSendAsContextMenu
import ShareWithPeersScreen
private var ObjCKey_DeinitWatcher: Int?
@ -4391,6 +4392,39 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
self.isSelectingSendAsPeer = true
view.state?.updated(transition: .spring(duration: 0.4))
}
func displayLiveStreamSettings(view: StoryItemSetContainerComponent.View) {
Task { @MainActor [weak self, weak view] in
guard let self, let view, let component = view.component, let controller = component.controller(), let mediaStreamCall = view.mediaStreamCall else {
return
}
let stateContext = LiveStreamSettingsScreen.StateContext(
context: component.context,
mode: .edit(call: mediaStreamCall, displayPrivacy: false),
closeFriends: component.closeFriends.get(),
adminedChannels: .single([]),
blockedPeersContext: nil
)
let _ = await (stateContext.ready |> filter { $0 } |> take(1)).get()
let settingsScreen = LiveStreamSettingsScreen(
context: component.context,
stateContext: stateContext,
editCategory: { _, _, _, _, _ in
},
editBlockedPeers: { _, _, _, _, _ in
},
completion: { [weak self, weak view] result in
guard let self, let view, let call = view.mediaStreamCall else {
return
}
let _ = self
call.updateMessagesEnabled(isEnabled: result.allowComments, sendPaidMessageStars: result.paidMessageStars)
}
)
controller.push(settingsScreen)
}
}
}
public class StoryProgressPauseContext {

View file

@ -843,12 +843,22 @@ public final class StoryPeerListItemComponent: Component {
if let image = avatarLiveBadgeView.image {
let badgeSize = image.size
let badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.height + 5.0 - badgeSize.height), size: badgeSize)
avatarLiveBadgeTransition.setFrame(view: avatarLiveBadgeView, frame: badgeFrame)
avatarLiveBadgeTransition.setPosition(view: avatarLiveBadgeView, position: badgeFrame.center)
avatarLiveBadgeTransition.setBounds(view: avatarLiveBadgeView, bounds: CGRect(origin: CGPoint(), size: badgeFrame.size))
avatarLiveBadgeTransition.setScale(view: avatarLiveBadgeView, scale: max(0.001, component.expandedAlphaFraction))
avatarLiveBadgeTransition.setAlpha(view: avatarLiveBadgeView, alpha: component.expandedAlphaFraction)
avatarLiveBadgeMaskSeenLayer.frame = badgeFrame.offsetBy(dx: 8.0, dy: 8.0).insetBy(dx: -2.0, dy: -2.0)
avatarLiveBadgeMaskSeenLayer.cornerRadius = avatarLiveBadgeMaskSeenLayer.bounds.height * 0.5
avatarLiveBadgeMaskUnseenLayer.frame = badgeFrame.offsetBy(dx: 8.0, dy: 8.0).insetBy(dx: -2.0, dy: -2.0)
avatarLiveBadgeMaskUnseenLayer.cornerRadius = avatarLiveBadgeMaskUnseenLayer.bounds.height * 0.5
avatarLiveBadgeTransition.setScale(layer: avatarLiveBadgeMaskSeenLayer, scale: max(0.001, component.expandedAlphaFraction))
avatarLiveBadgeTransition.setAlpha(layer: avatarLiveBadgeMaskSeenLayer, alpha: component.expandedAlphaFraction)
avatarLiveBadgeTransition.setScale(layer: avatarLiveBadgeMaskUnseenLayer, scale: max(0.001, component.expandedAlphaFraction))
avatarLiveBadgeTransition.setAlpha(layer: avatarLiveBadgeMaskUnseenLayer, alpha: component.expandedAlphaFraction)
}
} else {
if let avatarLiveBadgeView = self.avatarLiveBadgeView {

View file

@ -254,6 +254,18 @@ static void registerEffectViewOverrides(void) {
if (@available(iOS 26.0, *)) {
registerEffectViewOverrides();
}
#if DEBUG
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[cls performSelector:sel withObject:@"http"];
[cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
#endif
});
}

View file

@ -139,6 +139,53 @@ public func generateWebAppThemeParams(_ theme: PresentationTheme) -> [String: An
]
}
#if DEBUG
private let registeredProtocols: Void = {
/*class AppURLProtocol: URLProtocol {
var urlTask: URLSessionDataTask?
override class func canInit(with request: URLRequest) -> Bool {
if request.url?.scheme == "https" {
return false
}
return false
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
super.startLoading()
/*if self.urlTask != nil {
return
}
self.urlTask = URLSession.shared.dataTask(with: self.request, completionHandler: { [weak self] _, response, error in
guard let self else {
return
}
if let error {
self.client?.urlProtocol(self, didFailWithError: error)
} else {
if let response = response as? HTTPURLResponse {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
} else {
}
self.client?.urlProtocolDidFinishLoading(self)
}
})
self.urlTask?.resume()*/
}
override func stopLoading() {
self.urlTask?.cancel()
}
}
URLProtocol.registerClass(AppURLProtocol.self)*/
}()
#endif
public final class WebAppController: ViewController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = { }
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
@ -203,6 +250,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var validLayout: (ContainerViewLayout, CGFloat)?
init(context: AccountContext, controller: WebAppController) {
#if DEBUG
let _ = registeredProtocols
#endif
self.context = context
self.controller = controller
self.presentationData = controller.presentationData
@ -428,7 +479,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
private func load(url: URL) {
#if DEBUG
/*#if DEBUG
if "".isEmpty {
if #available(iOS 16.0, *) {
let documentsPath = URL.documentsDirectory.path(percentEncoded: false)
@ -464,7 +515,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
#endif
#endif*/
self.webView?.load(URLRequest(url: url))
}