Fixing streaming error message, dismiss animation, 1 member, stream title, context menu dismiss on recording, shimmer animation, adding animated counter to toolbar,

This commit is contained in:
Ilya Yelagov 2023-01-04 22:07:05 +04:00
parent ea8be9e909
commit 1424e7135d
8 changed files with 281 additions and 109 deletions

View File

@ -5956,7 +5956,7 @@ Sorry for the inconvenience.";
"LiveStream.RecordingInProgress" = "Live stream is being recorded"; "LiveStream.RecordingInProgress" = "Live stream is being recorded";
"VoiceChat.StopRecordingTitle" = "Stop Recording?"; "VoiceChat.StopRecordingTitle" = "Stop Recording?";
"VoiceChat.StopRecordingStop" = "Stop"; "VoiceChat.StopRecordingStop" = "Stop Recording";
"VoiceChat.RecordingSaved" = "Audio saved to **Saved Messages**."; "VoiceChat.RecordingSaved" = "Audio saved to **Saved Messages**.";

View File

@ -46,6 +46,9 @@ public enum ContextMenuActionItemTextColor {
public enum ContextMenuActionResult { public enum ContextMenuActionResult {
case `default` case `default`
case dismissWithoutContent case dismissWithoutContent
/// Temporary
static var safeStreamRecordingDismissWithoutContent: ContextMenuActionResult { .dismissWithoutContent }
case custom(ContainedViewLayoutTransition) case custom(ContainedViewLayoutTransition)
} }

View File

@ -403,16 +403,19 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C
private let getController: () -> ContextControllerProtocol? private let getController: () -> ContextControllerProtocol?
private let item: ContextMenuCustomItem private let item: ContextMenuCustomItem
private let requestDismiss: (ContextMenuActionResult) -> Void
private var presentationData: PresentationData? private var presentationData: PresentationData?
private var itemNode: ContextMenuCustomNode? private var itemNode: ContextMenuCustomNode?
init( init(
getController: @escaping () -> ContextControllerProtocol?, getController: @escaping () -> ContextControllerProtocol?,
item: ContextMenuCustomItem item: ContextMenuCustomItem,
requestDismiss: @escaping (ContextMenuActionResult) -> Void
) { ) {
self.getController = getController self.getController = getController
self.item = item self.item = item
self.requestDismiss = requestDismiss
super.init() super.init()
} }
@ -433,7 +436,12 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C
presentationData: presentationData, presentationData: presentationData,
getController: self.getController, getController: self.getController,
actionSelected: { result in actionSelected: { result in
let _ = result switch result {
case .dismissWithoutContent/* where ContextMenuActionResult.safeStreamRecordingDismissWithoutContent == .dismissWithoutContent*/:
self.requestDismiss(result)
default: break
}
} }
) )
self.itemNode = itemNode self.itemNode = itemNode
@ -505,7 +513,8 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
return Item( return Item(
node: ContextControllerActionsListCustomItemNode( node: ContextControllerActionsListCustomItemNode(
getController: getController, getController: getController,
item: customItem item: customItem,
requestDismiss: requestDismiss
), ),
separatorNode: ASDisplayNode() separatorNode: ASDisplayNode()
) )

View File

@ -500,7 +500,7 @@ public final class StandaloneShimmerEffect {
let colorSpace = CGColorSpaceCreateDeviceRGB() let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) else { return } guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) else { return }
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.3), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.2), end: CGPoint(x: size.width, y: 0.8), options: CGGradientDrawingOptions())
}) })
self.updateHorizontalLayer() self.updateHorizontalLayer()
@ -533,14 +533,28 @@ public final class StandaloneShimmerEffect {
layer.contents = image.cgImage layer.contents = image.cgImage
if layer.animation(forKey: "shimmer") == nil { if layer.animation(forKey: "shimmer") == nil {
var delay: TimeInterval { 1.6 }
let animation = CABasicAnimation(keyPath: "contentsRect.origin.x") let animation = CABasicAnimation(keyPath: "contentsRect.origin.x")
animation.fromValue = 1.0 as NSNumber animation.fromValue = NSNumber(floatLiteral: delay)
animation.toValue = -1.0 as NSNumber animation.toValue = NSNumber(floatLiteral: -delay)
animation.isAdditive = true animation.isAdditive = true
animation.repeatCount = .infinity animation.repeatCount = .infinity
animation.duration = 0.8 animation.duration = 0.8 * delay
animation.beginTime = layer.convertTime(1.0, from: nil) animation.timingFunction = .init(name: .easeInEaseOut)
// animation.beginTime = layer.convertTime(1.0, from: nil)
layer.add(animation, forKey: "shimmer") layer.add(animation, forKey: "shimmer")
/*let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
opacityAnimation.values = [0.0, 1.0, 0.0]
opacityAnimation.keyTimes = [0, 0.5, 0]
opacityAnimation.calculationMode = .linear
// opacityAnimation.fromValue = 2.0 as NSNumber
// opacityAnimation.toValue = -2.0 as NSNumber
// opacityAnimation.isAdditive = true
opacityAnimation.repeatCount = .infinity
opacityAnimation.duration = 1.6
opacityAnimation.timingFunctions = [.init(name: .easeInEaseOut)]
// opacityAnimation.beginTime = layer.convertTime(1.0, from: nil)
layer.add(opacityAnimation, forKey: "opacity")*/
} }
} }
} }

View File

@ -50,14 +50,14 @@ public final class AnimatedCountView: UIView {
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 8 : bounds.height - 12, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20) subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 8 : bounds.height - 12, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
} }
func update(countString: String, subtitle: String) { func update(countString: String, subtitle: String, fontSize: CGFloat = 48.0) {
self.setupGradientAnimations() self.setupGradientAnimations()
let text: String = countString let text: String = countString
self.countLabel.fontSize = 48 self.countLabel.fontSize = fontSize
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 48, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: fontSize, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .semibold)]) self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: max(floor(fontSize / 3), 12), weight: .semibold)])
self.subtitleLabel.isHidden = subtitle.isEmpty self.subtitleLabel.isHidden = subtitle.isEmpty
} }

View File

@ -170,7 +170,9 @@ public final class MediaStreamComponent: CombinedComponent {
var updated = false var updated = false
// TODO: remove debug timer // TODO: remove debug timer
// Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in // Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in var shouldReplaceNoViewersWithOne: Bool { true }
strongSelf.infoThrottler.publish(shouldReplaceNoViewersWithOne ? max(members.totalCount, 1) : members.totalCount /*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in
// let _ = members.totalCount // let _ = members.totalCount
guard let strongSelf = strongSelf else { return } guard let strongSelf = strongSelf else { return }
var updated = false var updated = false
@ -411,7 +413,9 @@ public final class MediaStreamComponent: CombinedComponent {
var navigationRightItems: [AnyComponentWithIdentity<Empty>] = [] var navigationRightItems: [AnyComponentWithIdentity<Empty>] = []
if context.state.isPictureInPictureSupported, context.state.videoIsPlayable { // let videoIsPlayable = context.state.videoIsPlayable
if context.state.isPictureInPictureSupported /*, context.state.videoIsPlayable*/ {
navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button(
content: AnyComponent(ZStack([ content: AnyComponent(ZStack([
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
@ -420,7 +424,7 @@ public final class MediaStreamComponent: CombinedComponent {
))), ))),
AnyComponentWithIdentity(id: "a", component: AnyComponent(BundleIconComponent( AnyComponentWithIdentity(id: "a", component: AnyComponent(BundleIconComponent(
name: "Call/pip", name: "Call/pip",
tintColor: .white tintColor: .white // .withAlphaComponent(context.state.videoIsPlayable ? 1.0 : 0.6)
))) )))
] ]
)), )),
@ -435,6 +439,7 @@ public final class MediaStreamComponent: CombinedComponent {
).minSize(CGSize(width: 44.0, height: 44.0))))) ).minSize(CGSize(width: 44.0, height: 44.0)))))
} }
var topLeftButton: AnyComponent<Empty>? var topLeftButton: AnyComponent<Empty>?
if context.state.canManageCall { if context.state.canManageCall {
let whiteColor = UIColor(white: 1.0, alpha: 1.0) let whiteColor = UIColor(white: 1.0, alpha: 1.0)
topLeftButton = AnyComponent(Button( topLeftButton = AnyComponent(Button(
@ -477,7 +482,7 @@ public final class MediaStreamComponent: CombinedComponent {
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak call, weak controller, weak state] _, a in }, action: { [weak call, weak controller, weak state] _, dismissWithResult in
guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else { guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else {
return return
} }
@ -507,12 +512,11 @@ public final class MediaStreamComponent: CombinedComponent {
}) })
controller.present(editController, in: .window(.root)) controller.present(editController, in: .window(.root))
a(.default) dismissWithResult(.default)
}))) })))
if let recordingStartTimestamp = state.recordingStartTimestamp { if let recordingStartTimestamp = state.recordingStartTimestamp {
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, f in items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, dismissWithResult in
f(.dismissWithoutContent)
guard let call = call, let controller = controller else { guard let call = call, let controller = controller else {
return return
@ -547,6 +551,8 @@ public final class MediaStreamComponent: CombinedComponent {
})*/ })*/
})]) })])
controller.present(alertController, in: .window(.root)) controller.present(alertController, in: .window(.root))
// TODO: спросить про dismissWithoutContent и default
dismissWithResult(.dismissWithoutContent)
}), false)) }), false))
} else { } else {
let text = presentationData.strings.LiveStream_StartRecording let text = presentationData.strings.LiveStream_StartRecording
@ -605,14 +611,34 @@ public final class MediaStreamComponent: CombinedComponent {
a(.default) a(.default)
}))) })))
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in items.append(.action(ContextMenuActionItem(id: nil, text: /*presentationData.strings.VoiceChat_StopRecordingStop*/"Stop Live Stream", textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil)
}, action: { [weak call] _, a in }, action: { [weak call] _, a in
guard let call = call else { guard let call = call else {
return return
} }
let alertController = textAlertController(
context: call.accountContext,
forceTheme: defaultDarkPresentationTheme,
title: nil,
text: presentationData.strings.VoiceChat_StopRecordingTitle,
actions: [
TextAlertAction(
type: .genericAction,
title: presentationData.strings.Common_Cancel,
action: {}
),
TextAlertAction(
type: .defaultAction,
title: presentationData.strings.VoiceChat_StopRecordingStop,
action: { [weak call] in
guard let call = call else {
return
}
let _ = call.leave(terminateIfPossible: true).start() let _ = call.leave(terminateIfPossible: true).start()
})
])
controller.present(alertController, in: .window(.root))
a(.default) a(.default)
}))) })))
@ -669,9 +695,10 @@ public final class MediaStreamComponent: CombinedComponent {
let navigationComponent = NavigationBarComponent( let navigationComponent = NavigationBarComponent(
topInset: environment.statusBarHeight, topInset: environment.statusBarHeight,
sideInset: environment.safeInsets.left, sideInset: environment.safeInsets.left,
backgroundVisible: isFullscreen,
leftItem: topLeftButton, leftItem: topLeftButton,
rightItems: navigationRightItems, rightItems: navigationRightItems,
centerItem: AnyComponent(StreamTitleComponent(text: state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isActive: context.state.videoIsPlayable)) centerItem: AnyComponent(StreamTitleComponent(text: state.callTitle ?? state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isActive: context.state.videoIsPlayable))
) )
if context.state.storedIsFullscreen != isFullscreen { if context.state.storedIsFullscreen != isFullscreen {
@ -685,15 +712,8 @@ public final class MediaStreamComponent: CombinedComponent {
var infoItem: AnyComponent<Empty>? var infoItem: AnyComponent<Empty>?
if let originInfo = context.state.originInfo { if let originInfo = context.state.originInfo {
let memberCountString: String
if originInfo.memberCount == 0 {
memberCountString = environment.strings.LiveStream_NoViewers
} else {
memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount))
}
infoItem = AnyComponent(OriginInfoComponent( infoItem = AnyComponent(OriginInfoComponent(
title: state.callTitle ?? originInfo.title, memberCount: originInfo.memberCount
subtitle: memberCountString
)) ))
} }
let availableSize = context.availableSize let availableSize = context.availableSize
@ -723,7 +743,13 @@ public final class MediaStreamComponent: CombinedComponent {
if isFullyDragged || state.initialOffset != 0 { if isFullyDragged || state.initialOffset != 0 {
state.updateDismissOffset(value: 0.0, interactive: false) state.updateDismissOffset(value: 0.0, interactive: false)
} else { } else {
let _ = call.leave(terminateIfPossible: false) activatePictureInPicture.invoke(Action {
guard let controller = controller() as? MediaStreamComponentController else {
return
}
controller.dismiss(closing: false, manual: true)
})
// let _ = call.leave(terminateIfPossible: false)
} }
} }
} else { } else {
@ -757,7 +783,11 @@ public final class MediaStreamComponent: CombinedComponent {
context.add(dismissTapComponent context.add(dismissTapComponent
.position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2)) .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2))
.gesture(.tap { .gesture(.tap {
_ = call.leave(terminateIfPossible: false) guard let controller = controller() as? MediaStreamComponentController else {
return
}
controller.dismiss(closing: false, manual: true)
// _ = call.leave(terminateIfPossible: false)
}) })
.gesture(.pan(onPanGesture)) .gesture(.pan(onPanGesture))
) )
@ -955,10 +985,10 @@ public final class MediaStreamComponent: CombinedComponent {
component: StreamSheetComponent( component: StreamSheetComponent(
topComponent: AnyComponent(navigationComponent), topComponent: AnyComponent(navigationComponent),
bottomButtonsRow: fullScreenToolbarComponent, bottomButtonsRow: fullScreenToolbarComponent,
topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset, topOffset: /*context.availableSize.height - sheetHeight +*/ max(context.state.dismissOffset, 0),
sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), sheetHeight: context.availableSize.height,// max(sheetHeight - context.state.dismissOffset, sheetHeight),
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), backgroundColor: /*isFullscreen ? .clear : */ (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
bottomPadding: 12, bottomPadding: 0,
participantsCount: -1, participantsCount: -1,
isFullyExtended: isFullyDragged, isFullyExtended: isFullyDragged,
deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1, deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1,
@ -1053,13 +1083,15 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
self.view.clipsToBounds = false self.view.clipsToBounds = false
} }
override public func viewWillAppear(_ animated: Bool) { override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
} }
override public func viewDidLayoutSubviews() { override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
backgroundDimView.frame = .init(x: 0, y: -view.bounds.height * 3, width: view.bounds.width, height: view.bounds.height * 4) let dimViewSide: CGFloat = max(view.bounds.width, view.bounds.height)
backgroundDimView.frame = .init(x: view.bounds.midX - dimViewSide / 2, y: -view.bounds.height * 3, width: dimViewSide, height: view.bounds.height * 4)
} }
public func dismiss(closing: Bool, manual: Bool) { public func dismiss(closing: Bool, manual: Bool) {
@ -1070,7 +1102,11 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
override public func dismiss(completion: (() -> Void)? = nil) { override public func dismiss(completion: (() -> Void)? = nil) {
self.view.layer.allowsGroupOpacity = true self.view.layer.allowsGroupOpacity = true
self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in // self.view.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in
//
// })
self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false)
self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in
guard let strongSelf = self else { guard let strongSelf = self else {
completion?() completion?()
return return
@ -1078,9 +1114,6 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
strongSelf.view.layer.allowsGroupOpacity = false strongSelf.view.layer.allowsGroupOpacity = false
strongSelf.dismissImpl(completion: completion) strongSelf.dismissImpl(completion: completion)
}) })
self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false)
self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, completion: { _ in
})
} }
private func dismissImpl(completion: (() -> Void)? = nil) { private func dismissImpl(completion: (() -> Void)? = nil) {
@ -1272,7 +1305,7 @@ final class StreamTitleComponent: Component {
if !wasLive { if !wasLive {
wasLive = true wasLive = true
let anim = CAKeyframeAnimation(keyPath: "transform.scale") let anim = CAKeyframeAnimation(keyPath: "transform.scale")
anim.values = [1.0, 1.4, 0.9, 1.0] anim.values = [1.0, 1.12, 0.9, 1.0]
anim.keyTimes = [0, 0.5, 0.8, 1] anim.keyTimes = [0, 0.5, 0.8, 1]
anim.duration = 0.4 anim.duration = 0.4
self.layer.add(anim, forKey: "transform") self.layer.add(anim, forKey: "transform")
@ -1281,7 +1314,7 @@ final class StreamTitleComponent: Component {
self.toggle(isLive: true) }) self.toggle(isLive: true) })
return return
} }
self.backgroundColor = UIColor(red: 0.82, green: 0.26, blue: 0.37, alpha: 1) self.backgroundColor = UIColor(red: 1, green: 0.176, blue: 0.333, alpha: 1)
stalledAnimatedGradient.opacity = 0 stalledAnimatedGradient.opacity = 0
stalledAnimatedGradient.removeAllAnimations() stalledAnimatedGradient.removeAllAnimations()
} else { } else {
@ -1300,21 +1333,87 @@ final class StreamTitleComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
private let textView: ComponentHostView<Empty>
private var indicatorView: UIImageView? private var indicatorView: UIImageView?
let liveIndicatorView = LiveIndicatorView() let liveIndicatorView = LiveIndicatorView()
let titleLabel = UILabel() let titleLabel = UILabel()
private let titleFadeLayer = CALayer()
private let trackingLayer: HierarchyTrackingLayer private let trackingLayer: HierarchyTrackingLayer
override init(frame: CGRect) { private func updateTitleFadeLayer(textFrame: CGRect) {
self.textView = ComponentHostView<Empty>() // titleLabel.backgroundColor = .red
guard let string = titleLabel.attributedText,
string.boundingRect(with: .init(width: .max, height: .max), context: nil).width > textFrame.width
else {
titleLabel.layer.mask = nil
titleLabel.frame = textFrame
self.titleLabel.textAlignment = .center
return
}
var isRTL: Bool = false
if let string = titleLabel.attributedText {
let coreTextLine = CTLineCreateWithAttributedString(string)
let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
if glyphRuns.count > 0 {
let run = glyphRuns[0] as! CTRun
if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
isRTL = true
}
}
}
let gradientInset: CGFloat = 0
let gradientRadius: CGFloat = 50
let solidPartLayer = CALayer()
solidPartLayer.backgroundColor = UIColor.black.cgColor
let containerWidth: CGFloat = textFrame.width
let availableWidth: CGFloat = textFrame.width - gradientRadius
let extraSpace: CGFloat = 100
if isRTL {
let adjustForRTL: CGFloat = 12
let safeSolidWidth: CGFloat = containerWidth + adjustForRTL
solidPartLayer.frame = CGRect(
origin: CGPoint(x: max(containerWidth - availableWidth, gradientRadius), y: 0),
size: CGSize(width: safeSolidWidth, height: textFrame.height))
titleLabel.frame = CGRect(x: textFrame.minX - extraSpace, y: textFrame.minY, width: textFrame.width + extraSpace, height: textFrame.height)
} else {
solidPartLayer.frame = CGRect(
origin: .zero,
size: CGSize(width: availableWidth, height: textFrame.height))
titleLabel.frame = CGRect(origin: textFrame.origin, size: CGSize(width: textFrame.width + extraSpace, height: textFrame.height))
}
self.titleLabel.textAlignment = .natural
titleFadeLayer.addSublayer(solidPartLayer)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor]
if isRTL {
gradientLayer.startPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.frame = CGRect(x: solidPartLayer.frame.minX - gradientRadius, y: 0, width: gradientRadius, height: textFrame.height)
} else {
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.frame = CGRect(x: availableWidth + gradientInset, y: 0, width: gradientRadius, height: textFrame.height)
}
titleFadeLayer.addSublayer(gradientLayer)
titleFadeLayer.masksToBounds = false
titleFadeLayer.frame = titleLabel.bounds
titleLabel.layer.mask = titleFadeLayer
}
override init(frame: CGRect) {
self.trackingLayer = HierarchyTrackingLayer() self.trackingLayer = HierarchyTrackingLayer()
super.init(frame: frame) super.init(frame: frame)
// self.addSubview(self.textView)
self.addSubview(self.titleLabel) self.addSubview(self.titleLabel)
self.addSubview(self.liveIndicatorView) self.addSubview(self.liveIndicatorView)
@ -1350,7 +1449,6 @@ final class StreamTitleComponent: Component {
self.titleLabel.text = component.text self.titleLabel.text = component.text
self.titleLabel.font = Font.semibold(17.0) self.titleLabel.font = Font.semibold(17.0)
self.titleLabel.textColor = .white self.titleLabel.textColor = .white
self.titleLabel.textAlignment = .center
self.titleLabel.numberOfLines = 1 self.titleLabel.numberOfLines = 1
self.titleLabel.invalidateIntrinsicContentSize() self.titleLabel.invalidateIntrinsicContentSize()
@ -1385,7 +1483,7 @@ final class StreamTitleComponent: Component {
let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height) let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize) let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
// self.textView.frame = textFrame // self.textView.frame = textFrame
self.titleLabel.frame = textFrame self.updateTitleFadeLayer(textFrame: textFrame)
liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - 22 / 2), size: .init(width: 40, height: 22)) liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - 22 / 2), size: .init(width: 40, height: 22))
self.liveIndicatorView.toggle(isLive: component.isActive) self.liveIndicatorView.toggle(isLive: component.isActive)
@ -1413,16 +1511,20 @@ private final class NavigationBarComponent: CombinedComponent {
let leftItem: AnyComponent<Empty>? let leftItem: AnyComponent<Empty>?
let rightItems: [AnyComponentWithIdentity<Empty>] let rightItems: [AnyComponentWithIdentity<Empty>]
let centerItem: AnyComponent<Empty>? let centerItem: AnyComponent<Empty>?
let backgroundVisible: Bool
init( init(
topInset: CGFloat, topInset: CGFloat,
sideInset: CGFloat, sideInset: CGFloat,
backgroundVisible: Bool,
leftItem: AnyComponent<Empty>?, leftItem: AnyComponent<Empty>?,
rightItems: [AnyComponentWithIdentity<Empty>], rightItems: [AnyComponentWithIdentity<Empty>],
centerItem: AnyComponent<Empty>? centerItem: AnyComponent<Empty>?
) { ) {
self.topInset = 0 // topInset self.topInset = 0 // topInset
self.sideInset = sideInset self.sideInset = sideInset
self.backgroundVisible = backgroundVisible
self.leftItem = leftItem self.leftItem = leftItem
self.rightItems = rightItems self.rightItems = rightItems
self.centerItem = centerItem self.centerItem = centerItem
@ -1449,6 +1551,7 @@ private final class NavigationBarComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let background = Child(Rectangle.self)
let leftItem = Child(environment: Empty.self) let leftItem = Child(environment: Empty.self)
let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let centerItem = Child(environment: Empty.self) let centerItem = Child(environment: Empty.self)
@ -1460,6 +1563,8 @@ private final class NavigationBarComponent: CombinedComponent {
let contentHeight: CGFloat = 44.0 let contentHeight: CGFloat = 44.0
let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: context.component.backgroundVisible ? 0.5 : 0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let leftItem = context.component.leftItem.flatMap { leftItemComponent in let leftItem = context.component.leftItem.flatMap { leftItemComponent in
return leftItem.update( return leftItem.update(
component: leftItemComponent, component: leftItemComponent,
@ -1493,6 +1598,10 @@ private final class NavigationBarComponent: CombinedComponent {
availableWidth -= centerItem.size.width availableWidth -= centerItem.size.width
} }
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
var centerLeftInset = sideInset var centerLeftInset = sideInset
if let leftItem = leftItem { if let leftItem = leftItem {
context.add(leftItem context.add(leftItem
@ -1522,22 +1631,18 @@ private final class NavigationBarComponent: CombinedComponent {
} }
private final class OriginInfoComponent: CombinedComponent { private final class OriginInfoComponent: CombinedComponent {
let title: String let participantsCount: Int
let subtitle: String
private static var usingAnimatedCounter: Bool { true }
init( init(
title: String, memberCount: Int
subtitle: String
) { ) {
self.title = title self.participantsCount = memberCount
self.subtitle = subtitle
} }
static func ==(lhs: OriginInfoComponent, rhs: OriginInfoComponent) -> Bool { static func ==(lhs: OriginInfoComponent, rhs: OriginInfoComponent) -> Bool {
if lhs.title != rhs.title { if lhs.participantsCount != rhs.participantsCount {
return false
}
if lhs.subtitle != rhs.subtitle {
return false return false
} }
@ -1545,39 +1650,64 @@ private final class OriginInfoComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let title = Child(Text.self) if usingAnimatedCounter {
let subtitle = Child(Text.self) let viewerCounter = Child(ParticipantsComponent.self)
return { context in return { context in
let spacing: CGFloat = 0.0 // let spacing: CGFloat = 0.0
let title = title.update( let viewerCounter = viewerCounter.update(
component: Text( component: ParticipantsComponent(
text: context.component.title, font: Font.semibold(17.0), color: .white), count: context.component.participantsCount,
availableSize: context.availableSize, showsSubtitle: true,
fontSize: 24
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: context.transition transition: context.transition
) )
let subtitle = subtitle.update( var size = CGSize(width: viewerCounter.size.width, height: viewerCounter.size.height)
component: Text(
text: context.component.subtitle, font: Font.regular(14.0), color: .white),
availableSize: context.availableSize,
transition: context.transition
)
var size = CGSize(width: max(title.size.width, subtitle.size.width), height: title.size.height + spacing + subtitle.size.height)
size.width = min(size.width, context.availableSize.width) size.width = min(size.width, context.availableSize.width)
size.height = min(size.height, context.availableSize.height) size.height = min(size.height, context.availableSize.height)
context.add(title context.add(viewerCounter
.position(CGPoint(x: size.width / 2.0, y: title.size.height / 2.0)) .position(CGPoint(x: size.width / 2.0, y: viewerCounter.size.height / 2.0))
)
context.add(subtitle
.position(CGPoint(x: size.width / 2.0, y: title.size.height + spacing + subtitle.size.height / 2.0))
) )
return size return size
} }
} else {
let subtitle = Child(Text.self)
return { context in
// let spacing: CGFloat = 0.0
let memberCount = context.component.participantsCount
let memberCountString: String
if memberCount == 0 {
memberCountString = "no viewers"
} else {
memberCountString = memberCount > 0 ? presentationStringsFormattedNumber(Int32(memberCount), ",") : ""
}
let subtitle = subtitle.update(
component: Text(
text: memberCountString, font: Font.regular(14.0), color: .white),
availableSize: context.availableSize,
transition: context.transition
)
var size = CGSize(width: subtitle.size.width, height: subtitle.size.height)
size.width = min(size.width, context.availableSize.width)
size.height = min(size.height, context.availableSize.height)
context.add(subtitle
.position(CGPoint(x: size.width / 2.0, y: subtitle.size.height / 2.0))
)
return size
}
}
} }
} }
@ -1635,7 +1765,7 @@ private final class ToolbarComponent: CombinedComponent {
let contentHeight: CGFloat = 44.0 let contentHeight: CGFloat = 44.0
let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset) let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition) let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let leftItem = context.component.leftItem.flatMap { leftItemComponent in let leftItem = context.component.leftItem.flatMap { leftItemComponent in
return leftItem.update( return leftItem.update(
@ -1659,10 +1789,11 @@ private final class ToolbarComponent: CombinedComponent {
availableWidth -= rightItem.size.width availableWidth -= rightItem.size.width
} }
let temporaryOffsetForSmallerSubtitle: CGFloat = 12
let centerItem = context.component.centerItem.flatMap { centerItemComponent in let centerItem = context.component.centerItem.flatMap { centerItemComponent in
return centerItem.update( return centerItem.update(
component: centerItemComponent, component: centerItemComponent,
availableSize: CGSize(width: availableWidth, height: contentHeight), availableSize: CGSize(width: availableWidth, height: contentHeight - temporaryOffsetForSmallerSubtitle / 2),
transition: context.transition transition: context.transition
) )
} }
@ -1693,7 +1824,7 @@ private final class ToolbarComponent: CombinedComponent {
let maxCenterInset = max(centerLeftInset, centerRightInset) let maxCenterInset = max(centerLeftInset, centerRightInset)
if let centerItem = centerItem { if let centerItem = centerItem {
context.add(centerItem context.add(centerItem
.position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: contentHeight / 2.0)) .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: contentHeight / 2.0 - temporaryOffsetForSmallerSubtitle))
) )
} }

View File

@ -214,7 +214,8 @@ final class MediaStreamVideoComponent: Component {
loadingBlurView.layer.add(anim, forKey: "opacity") loadingBlurView.layer.add(anim, forKey: "opacity")
} }
} }
loadingBlurView.layer.zPosition = 999 loadingBlurView.layer.zPosition = 998
self.noSignalView?.layer.zPosition = loadingBlurView.layer.zPosition + 1
if shimmerBorderLayer.superlayer == nil { if shimmerBorderLayer.superlayer == nil {
loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer) loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer)
} }
@ -230,9 +231,10 @@ final class MediaStreamVideoComponent: Component {
borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor
borderMask.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor borderMask.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor
borderMask.lineWidth = 3 borderMask.lineWidth = 3
borderMask.compositingFilter = "softLightBlendMode"
shimmerBorderLayer.mask = borderMask shimmerBorderLayer.mask = borderMask
borderShimmer = .init() borderShimmer = StandaloneShimmerEffect()
borderShimmer.layer = shimmerBorderLayer borderShimmer.layer = shimmerBorderLayer
borderShimmer.updateHorizontal(background: .clear, foreground: .white) borderShimmer.updateHorizontal(background: .clear, foreground: .white)
loadingBlurView.alpha = 1 loadingBlurView.alpha = 1
@ -314,7 +316,6 @@ final class MediaStreamVideoComponent: Component {
UIView.animate(withDuration: 0.3) { UIView.animate(withDuration: 0.3) {
videoBlurView.alpha = 1 videoBlurView.alpha = 1
} }
self.maskGradientLayer.type = .radial self.maskGradientLayer.type = .radial
self.maskGradientLayer.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor] self.maskGradientLayer.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
@ -409,13 +410,18 @@ final class MediaStreamVideoComponent: Component {
} else if component.isFullscreen { } else if component.isFullscreen {
if fullScreenBackgroundPlaceholder.superview == nil { if fullScreenBackgroundPlaceholder.superview == nil {
insertSubview(fullScreenBackgroundPlaceholder, at: 0) insertSubview(fullScreenBackgroundPlaceholder, at: 0)
transition.animateAlpha(view: fullScreenBackgroundPlaceholder, from: 0, to: 1)
} }
fullScreenBackgroundPlaceholder.backgroundColor = UIColor.black.withAlphaComponent(0.5) fullScreenBackgroundPlaceholder.backgroundColor = UIColor.black.withAlphaComponent(0.5)
} else { } else {
fullScreenBackgroundPlaceholder.removeFromSuperview() transition.animateAlpha(view: fullScreenBackgroundPlaceholder, from: 1, to: 0, completion: { didComplete in
if didComplete {
self.fullScreenBackgroundPlaceholder.removeFromSuperview()
}
})
} }
fullScreenBackgroundPlaceholder.frame = .init(origin: .zero, size: availableSize) fullScreenBackgroundPlaceholder.frame = .init(origin: .zero, size: availableSize)
// fullScreenBackgroundPlaceholder.isHidden = true
let videoInset: CGFloat let videoInset: CGFloat
if !component.isFullscreen { if !component.isFullscreen {
videoInset = 16 videoInset = 16
@ -556,6 +562,8 @@ final class MediaStreamVideoComponent: Component {
self.noSignalView = noSignalView self.noSignalView = noSignalView
// TODO: above blurred animation // TODO: above blurred animation
self.addSubview(noSignalView) self.addSubview(noSignalView)
noSignalView.layer.zPosition = loadingBlurView.layer.zPosition + 1
noSignalView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) noSignalView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
} }

View File

@ -95,12 +95,12 @@ final class StreamSheetComponent: CombinedComponent {
override func draw(_ rect: CGRect) { override func draw(_ rect: CGRect) {
super.draw(rect) super.draw(rect)
// Debug interactive area // Debug interactive area
// guard let context = UIGraphicsGetCurrentContext() else { return } guard let context = UIGraphicsGetCurrentContext() else { return }
// context.setFillColor(UIColor.red.cgColor) context.setFillColor(UIColor.red.withAlphaComponent(0.3).cgColor)
// overlayComponentsFrames.forEach { frame in overlayComponentsFrames.forEach { frame in
// context.addRect(frame) context.addRect(frame)
// context.fillPath() context.fillPath()
// } }
} }
} }
@ -172,6 +172,8 @@ final class StreamSheetComponent: CombinedComponent {
transition: context.transition transition: context.transition
) )
} }
// TODO: replace
let isFullscreen = context.component.participantsCount == -1
context.add(background context.add(background
.position(CGPoint(x: size.width / 2.0, y: topOffset + context.component.sheetHeight / 2)) .position(CGPoint(x: size.width / 2.0, y: topOffset + context.component.sheetHeight / 2))
@ -182,7 +184,7 @@ final class StreamSheetComponent: CombinedComponent {
if let topItem = topItem { if let topItem = topItem {
context.add(topItem context.add(topItem
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + 32)) .position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 32)))
) )
(context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height)) (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height))
} }
@ -297,16 +299,21 @@ final class ParticipantsComponent: Component {
func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment<ComponentFlow.Empty>, transition: ComponentFlow.Transition) -> CGSize { func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment<ComponentFlow.Empty>, transition: ComponentFlow.Transition) -> CGSize {
view.counter.update( view.counter.update(
countString: count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "", countString: self.count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "",
subtitle: count > 0 ? "watching" : "no viewers" subtitle: self.showsSubtitle ? (self.count > 0 ? "watching" : "no viewers") : "",
fontSize: self.fontSize
)// environment.strings.LiveStream_NoViewers) )// environment.strings.LiveStream_NoViewers)
return availableSize return availableSize
} }
private let count: Int private let count: Int
private let showsSubtitle: Bool
private let fontSize: CGFloat
init(count: Int) { init(count: Int, showsSubtitle: Bool = true, fontSize: CGFloat = 48) {
self.count = count self.count = count
self.showsSubtitle = showsSubtitle
self.fontSize = fontSize
} }
final class View: UIView { final class View: UIView {