mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Media-related improvements
This commit is contained in:
@@ -10,10 +10,14 @@ import Postbox
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import LottieAnimationComponent
|
||||
import ContextUI
|
||||
import ViewControllerComponent
|
||||
import BundleIconComponent
|
||||
import CreateExternalMediaStreamScreen
|
||||
import HierarchyTrackingLayer
|
||||
import UndoPanelComponent
|
||||
|
||||
final class NavigationBackButtonComponent: Component {
|
||||
let text: String
|
||||
@@ -99,6 +103,116 @@ final class NavigationBackButtonComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class StreamTitleComponent: Component {
|
||||
let text: String
|
||||
let isRecording: Bool
|
||||
|
||||
init(text: String, isRecording: Bool) {
|
||||
self.text = text
|
||||
self.isRecording = isRecording
|
||||
}
|
||||
|
||||
static func ==(lhs: StreamTitleComponent, rhs: StreamTitleComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.isRecording != rhs.isRecording {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let textView: ComponentHostView<Empty>
|
||||
private var indicatorView: UIImageView?
|
||||
|
||||
private let trackingLayer: HierarchyTrackingLayer
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.textView = ComponentHostView<Empty>()
|
||||
|
||||
self.trackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.trackingLayer.didEnterHierarchy = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateIndicatorAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateIndicatorAnimation() {
|
||||
guard let indicatorView = self.indicatorView else {
|
||||
return
|
||||
}
|
||||
if indicatorView.layer.animation(forKey: "blink") == nil {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
|
||||
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
||||
animation.duration = 0.7
|
||||
animation.autoreverses = true
|
||||
animation.repeatCount = Float.infinity
|
||||
indicatorView.layer.add(animation, forKey: "recording")
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.text,
|
||||
font: Font.semibold(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if component.isRecording {
|
||||
if self.indicatorView == nil {
|
||||
let indicatorView = UIImageView(image: generateFilledCircleImage(diameter: 8.0, color: .red, strokeColor: nil, strokeWidth: nil, backgroundColor: nil))
|
||||
self.addSubview(indicatorView)
|
||||
self.indicatorView = indicatorView
|
||||
|
||||
self.updateIndicatorAnimation()
|
||||
}
|
||||
} else {
|
||||
if let indicatorView = self.indicatorView {
|
||||
self.indicatorView = nil
|
||||
indicatorView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 20.0
|
||||
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)
|
||||
self.textView.frame = textFrame
|
||||
|
||||
if let indicatorView = self.indicatorView, let image = indicatorView.image {
|
||||
indicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floorToScreenPixels((size.height - image.size.height) / 2.0) + 1.0), size: image.size)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class NavigationBarComponent: CombinedComponent {
|
||||
let topInset: CGFloat
|
||||
let sideInset: CGFloat
|
||||
@@ -442,7 +556,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
private(set) var canManageCall: Bool = false
|
||||
let isPictureInPictureSupported: Bool
|
||||
|
||||
private(set) var callTitle: String?
|
||||
private(set) var recordingStartTimestamp: Int32?
|
||||
|
||||
private(set) var peerTitle: String = ""
|
||||
private(set) var chatPeer: Peer?
|
||||
|
||||
private(set) var isVisibleInHierarchy: Bool = false
|
||||
private var isVisibleInHierarchyDisposable: Disposable?
|
||||
@@ -498,6 +616,17 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
strongSelf.peerTitle = callPeer.debugDisplayTitle
|
||||
updated = true
|
||||
}
|
||||
strongSelf.chatPeer = callPeer
|
||||
|
||||
if strongSelf.callTitle != state.title {
|
||||
strongSelf.callTitle = state.title
|
||||
updated = true
|
||||
}
|
||||
|
||||
if strongSelf.recordingStartTimestamp != state.recordingStartTimestamp {
|
||||
strongSelf.recordingStartTimestamp = state.recordingStartTimestamp
|
||||
updated = true
|
||||
}
|
||||
|
||||
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
|
||||
if strongSelf.originInfo != originInfo {
|
||||
@@ -593,6 +722,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
)
|
||||
|
||||
let call = context.component.call
|
||||
let state = context.state
|
||||
let controller = environment.controller
|
||||
|
||||
let video = video.update(
|
||||
@@ -659,8 +789,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
size: CGSize(width: 22.0, height: 22.0)
|
||||
).tagged(moreAnimationTag))),
|
||||
])),
|
||||
action: { [weak call] in
|
||||
guard let call = call else {
|
||||
action: { [weak call, weak state] in
|
||||
guard let call = call, let state = state else {
|
||||
return
|
||||
}
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
@@ -677,8 +807,141 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor, backgroundColor: nil)
|
||||
|
||||
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)
|
||||
}, action: { [weak call, weak controller, weak state] _, a in
|
||||
guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else {
|
||||
return
|
||||
}
|
||||
|
||||
let initialTitle = state.callTitle ?? ""
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let title: String = presentationData.strings.LiveStream_EditTitle
|
||||
let text: String = presentationData.strings.LiveStream_EditTitleText
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in
|
||||
guard let call = call else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if let title = title, title != initialTitle {
|
||||
call.updateTitle(title)
|
||||
|
||||
let text: String = title.isEmpty ? presentationData.strings.LiveStream_EditTitleRemoveSuccess : presentationData.strings.LiveStream_EditTitleSuccess(title).string
|
||||
|
||||
let _ = text
|
||||
//strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
controller.present(editController, in: .window(.root))
|
||||
|
||||
a(.default)
|
||||
})))
|
||||
|
||||
if let recordingStartTimestamp = state.recordingStartTimestamp {
|
||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
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, weak controller] in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = presentationData.strings.LiveStream_RecordingSaved
|
||||
|
||||
let _ = text
|
||||
let _ = controller
|
||||
|
||||
/*strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in
|
||||
if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
let context = strongSelf.context
|
||||
strongSelf.controller?.dismiss(completion: {
|
||||
Queue.mainQueue().justDispatch {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})*/
|
||||
})])
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}), false))
|
||||
} else {
|
||||
let text = presentationData.strings.LiveStream_StartRecording
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in
|
||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak call, weak state, weak controller] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let call = call, let state = state, let _ = state.chatPeer, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
let placeholder: String = presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo
|
||||
|
||||
title = presentationData.strings.LiveStream_StartRecordingTitle
|
||||
text = presentationData.strings.LiveStream_StartRecordingTextVideo
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if let title = title {
|
||||
call.setShouldBeRecording(true, title: title, videoOrientation: false)
|
||||
|
||||
let text = presentationData.strings.LiveStream_RecordingStarted
|
||||
let _ = text
|
||||
|
||||
let _ = controller
|
||||
|
||||
call.playTone(.recordingStarted)
|
||||
}
|
||||
})
|
||||
controller.present(editController, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
|
||||
let credentialsPromise = Promise<GroupCallStreamCredentials>()
|
||||
credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: "View Stream Key", textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor, backgroundColor: nil)
|
||||
}, action: { [weak call, weak controller] _, a in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
controller.push(CreateExternalMediaStreamScreen(context: call.accountContext, peerId: call.peerId, credentialsPromise: credentialsPromise, mode: .view))
|
||||
|
||||
a(.default)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, 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)
|
||||
}, action: { [weak call] _, a in
|
||||
guard let call = call else {
|
||||
return
|
||||
@@ -750,7 +1013,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
})
|
||||
),
|
||||
rightItems: navigationRightItems,
|
||||
centerItem: AnyComponent(Text(text: environment.strings.VoiceChatChannel_Title, font: Font.semibold(17.0), color: .white))
|
||||
centerItem: AnyComponent(StreamTitleComponent(text: environment.strings.VoiceChatChannel_Title, isRecording: state.recordingStartTimestamp != nil))
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
@@ -775,7 +1038,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount))
|
||||
}
|
||||
infoItem = AnyComponent(OriginInfoComponent(
|
||||
title: originInfo.title,
|
||||
title: state.callTitle ?? originInfo.title,
|
||||
subtitle: memberCountString
|
||||
))
|
||||
}
|
||||
@@ -813,7 +1076,6 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let state = context.state
|
||||
let height = context.availableSize.height
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
|
||||
Reference in New Issue
Block a user