mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-17 03:40:18 +00:00
Merge commit '243d0be94058e9f86d72aca0b0455927003739f2' into beta
This commit is contained in:
commit
10e8154f94
@ -12285,6 +12285,8 @@ Sorry for the inconvenience.";
|
|||||||
"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment";
|
"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment";
|
||||||
"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal";
|
"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal";
|
||||||
"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads";
|
"Stars.Intro.Transaction.TelegramAds.Subtitle" = "via Telegram Ads";
|
||||||
|
"Stars.Intro.Transaction.Gift" = "Gift";
|
||||||
|
"Stars.Intro.Transaction.ConvertedGift" = "Converted Gift";
|
||||||
"Stars.Intro.Transaction.Unsupported.Title" = "Unsupported";
|
"Stars.Intro.Transaction.Unsupported.Title" = "Unsupported";
|
||||||
"Stars.Intro.Transaction.Refund" = "Refund";
|
"Stars.Intro.Transaction.Refund" = "Refund";
|
||||||
|
|
||||||
|
|||||||
@ -1020,6 +1020,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
func makeStarsIntroScreen(context: AccountContext) -> ViewController
|
func makeStarsIntroScreen(context: AccountContext) -> ViewController
|
||||||
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||||
|
|
||||||
|
func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void)
|
||||||
|
|
||||||
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
|
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
|
||||||
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
|
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
|
||||||
|
|
||||||
|
|||||||
@ -102,7 +102,7 @@ final class CameraDeviceContext {
|
|||||||
return 30.0
|
return 30.0
|
||||||
}
|
}
|
||||||
switch DeviceModel.current {
|
switch DeviceModel.current {
|
||||||
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax:
|
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax:
|
||||||
return 60.0
|
return 60.0
|
||||||
default:
|
default:
|
||||||
return 30.0
|
return 30.0
|
||||||
|
|||||||
@ -34,6 +34,10 @@ public extension Camera {
|
|||||||
self = .iPhone15Pro
|
self = .iPhone15Pro
|
||||||
case .iPhone15ProMax:
|
case .iPhone15ProMax:
|
||||||
self = .iPhone15ProMax
|
self = .iPhone15ProMax
|
||||||
|
case .iPhone16Pro:
|
||||||
|
self = .iPhone15Pro
|
||||||
|
case .iPhone16ProMax:
|
||||||
|
self = .iPhone15ProMax
|
||||||
case .unknown:
|
case .unknown:
|
||||||
self = .unknown
|
self = .unknown
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
case iPhone14ProZoomed
|
case iPhone14ProZoomed
|
||||||
case iPhone14ProMax
|
case iPhone14ProMax
|
||||||
case iPhone14ProMaxZoomed
|
case iPhone14ProMaxZoomed
|
||||||
|
case iPhone16Pro
|
||||||
|
case iPhone16ProMax
|
||||||
case iPad
|
case iPad
|
||||||
case iPadMini
|
case iPadMini
|
||||||
case iPad102Inch
|
case iPad102Inch
|
||||||
@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
.iPhone14ProZoomed,
|
.iPhone14ProZoomed,
|
||||||
.iPhone14ProMax,
|
.iPhone14ProMax,
|
||||||
.iPhone14ProMaxZoomed,
|
.iPhone14ProMaxZoomed,
|
||||||
|
.iPhone16Pro,
|
||||||
|
.iPhone16ProMax,
|
||||||
.iPad,
|
.iPad,
|
||||||
.iPadMini,
|
.iPadMini,
|
||||||
.iPad102Inch,
|
.iPad102Inch,
|
||||||
@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
return CGSize(width: 430.0, height: 932.0)
|
return CGSize(width: 430.0, height: 932.0)
|
||||||
case .iPhone14ProMaxZoomed:
|
case .iPhone14ProMaxZoomed:
|
||||||
return CGSize(width: 375.0, height: 812.0)
|
return CGSize(width: 375.0, height: 812.0)
|
||||||
|
case .iPhone16Pro:
|
||||||
|
return CGSize(width: 402.0, height: 874.0)
|
||||||
|
case .iPhone16ProMax:
|
||||||
|
return CGSize(width: 440.0, height: 956.0)
|
||||||
case .iPad:
|
case .iPad:
|
||||||
return CGSize(width: 768.0, height: 1024.0)
|
return CGSize(width: 768.0, height: 1024.0)
|
||||||
case .iPadMini:
|
case .iPadMini:
|
||||||
@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
return 53.0 + UIScreenPixel
|
return 53.0 + UIScreenPixel
|
||||||
case .iPhone14Pro, .iPhone14ProMax:
|
case .iPhone14Pro, .iPhone14ProMax:
|
||||||
return 55.0
|
return 55.0
|
||||||
|
case .iPhone16Pro, .iPhone16ProMax:
|
||||||
|
return 55.0
|
||||||
case let .unknown(_, _, _, screenCornerRadius):
|
case let .unknown(_, _, _, screenCornerRadius):
|
||||||
return screenCornerRadius
|
return screenCornerRadius
|
||||||
default:
|
default:
|
||||||
@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
|
|
||||||
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
|
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
|
||||||
switch self {
|
switch self {
|
||||||
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
|
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||||
default:
|
default:
|
||||||
return UIEdgeInsets.zero
|
return UIEdgeInsets.zero
|
||||||
@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
|
|
||||||
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
|
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
|
||||||
switch self {
|
switch self {
|
||||||
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax:
|
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return inLandscape ? 21.0 : 34.0
|
return inLandscape ? 21.0 : 34.0
|
||||||
case .iPhone14ProZoomed:
|
case .iPhone14ProZoomed:
|
||||||
return inLandscape ? 21.0 : 28.0
|
return inLandscape ? 21.0 : 28.0
|
||||||
@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
return 54.0
|
return 54.0
|
||||||
case .iPhone14ProMaxZoomed:
|
case .iPhone14ProMaxZoomed:
|
||||||
return 47.0
|
return 47.0
|
||||||
|
case .iPhone16Pro, .iPhone16ProMax:
|
||||||
|
return 54.0
|
||||||
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
|
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
|
||||||
return 44.0
|
return 44.0
|
||||||
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
|
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
|
||||||
@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
return 162.0
|
return 162.0
|
||||||
case .iPhone6, .iPhone6Plus:
|
case .iPhone6, .iPhone6Plus:
|
||||||
return 163.0
|
return 163.0
|
||||||
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return 172.0
|
return 172.0
|
||||||
case .iPad, .iPad102Inch, .iPadPro10Inch:
|
case .iPad, .iPad102Inch, .iPadPro10Inch:
|
||||||
return 348.0
|
return 348.0
|
||||||
@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
return 216.0
|
return 216.0
|
||||||
case .iPhone6Plus:
|
case .iPhone6Plus:
|
||||||
return 226.0
|
return 226.0
|
||||||
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
|
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
|
||||||
return 292.0
|
return 292.0
|
||||||
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
|
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
|
||||||
return 302.0
|
return 302.0
|
||||||
case .iPad, .iPad102Inch, .iPadPro10Inch:
|
case .iPad, .iPad102Inch, .iPadPro10Inch:
|
||||||
return 263.0
|
return 263.0
|
||||||
@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
|
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
|
||||||
if inLandscape {
|
if inLandscape {
|
||||||
switch self {
|
switch self {
|
||||||
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return 37.0
|
return 37.0
|
||||||
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
|
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
|
||||||
return 50.0
|
return 50.0
|
||||||
@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .iPhone4, .iPhone5:
|
case .iPhone4, .iPhone5:
|
||||||
return 37.0
|
return 37.0
|
||||||
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return 44.0
|
return 44.0
|
||||||
case .iPhone6Plus:
|
case .iPhone6Plus:
|
||||||
return 45.0
|
return 45.0
|
||||||
@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
|||||||
|
|
||||||
public var hasDynamicIsland: Bool {
|
public var hasDynamicIsland: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -245,7 +245,7 @@ public func galleryItemForEntry(
|
|||||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||||
} else {
|
} else {
|
||||||
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") {
|
||||||
if NativeVideoContent.isHLSVideo(file: file), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
if NativeVideoContent.isHLSVideo(file: file) {
|
||||||
content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
|
||||||
} else {
|
} else {
|
||||||
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
|
||||||
|
|||||||
@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout {
|
|||||||
self.topOffset = 226.0
|
self.topOffset = 226.0
|
||||||
self.biometricsOffset = 30.0
|
self.biometricsOffset = 30.0
|
||||||
self.deleteOffset = 20.0
|
self.deleteOffset = 20.0
|
||||||
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
|
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
|
||||||
self.buttonSize = 75.0
|
self.buttonSize = 75.0
|
||||||
self.horizontalSecond = 103.0
|
self.horizontalSecond = 103.0
|
||||||
self.horizontalThird = 206.0
|
self.horizontalThird = 206.0
|
||||||
@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout {
|
|||||||
self.topOffset = 294.0
|
self.topOffset = 294.0
|
||||||
self.biometricsOffset = 30.0
|
self.biometricsOffset = 30.0
|
||||||
self.deleteOffset = 20.0
|
self.deleteOffset = 20.0
|
||||||
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
|
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
|
||||||
self.buttonSize = 85.0
|
self.buttonSize = 85.0
|
||||||
self.horizontalSecond = 115.0
|
self.horizontalSecond = 115.0
|
||||||
self.horizontalThird = 230.0
|
self.horizontalThird = 230.0
|
||||||
@ -151,11 +151,11 @@ public struct PasscodeLayout {
|
|||||||
self.titleOffset = 112.0
|
self.titleOffset = 112.0
|
||||||
self.subtitleOffset = -6.0
|
self.subtitleOffset = -6.0
|
||||||
self.inputFieldOffset = 156.0
|
self.inputFieldOffset = 156.0
|
||||||
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
|
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
|
||||||
self.titleOffset = 162.0
|
self.titleOffset = 162.0
|
||||||
self.subtitleOffset = 0.0
|
self.subtitleOffset = 0.0
|
||||||
self.inputFieldOffset = 206.0
|
self.inputFieldOffset = 206.0
|
||||||
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
|
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
|
||||||
self.titleOffset = 180.0
|
self.titleOffset = 180.0
|
||||||
self.subtitleOffset = 0.0
|
self.subtitleOffset = 0.0
|
||||||
self.inputFieldOffset = 226.0
|
self.inputFieldOffset = 226.0
|
||||||
|
|||||||
@ -631,6 +631,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) }
|
dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) }
|
||||||
dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) }
|
dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) }
|
||||||
dict[240843065] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) }
|
dict[240843065] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) }
|
||||||
|
dict[2030298073] = { return Api.MessageReportOption.parse_messageReportOption($0) }
|
||||||
dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) }
|
dict[1163625789] = { return Api.MessageViews.parse_messageViews($0) }
|
||||||
dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) }
|
dict[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($0) }
|
||||||
dict[-530392189] = { return Api.MessagesFilter.parse_inputMessagesFilterContacts($0) }
|
dict[-530392189] = { return Api.MessagesFilter.parse_inputMessagesFilterContacts($0) }
|
||||||
@ -803,6 +804,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[777640226] = { return Api.ReportReason.parse_inputReportReasonPornography($0) }
|
dict[777640226] = { return Api.ReportReason.parse_inputReportReasonPornography($0) }
|
||||||
dict[1490799288] = { return Api.ReportReason.parse_inputReportReasonSpam($0) }
|
dict[1490799288] = { return Api.ReportReason.parse_inputReportReasonSpam($0) }
|
||||||
dict[505595789] = { return Api.ReportReason.parse_inputReportReasonViolence($0) }
|
dict[505595789] = { return Api.ReportReason.parse_inputReportReasonViolence($0) }
|
||||||
|
dict[1862904881] = { return Api.ReportResult.parse_reportResultAddComment($0) }
|
||||||
|
dict[-253435722] = { return Api.ReportResult.parse_reportResultChooseOption($0) }
|
||||||
|
dict[-1917633461] = { return Api.ReportResult.parse_reportResultReported($0) }
|
||||||
dict[865857388] = { return Api.RequestPeerType.parse_requestPeerTypeBroadcast($0) }
|
dict[865857388] = { return Api.RequestPeerType.parse_requestPeerTypeBroadcast($0) }
|
||||||
dict[-906990053] = { return Api.RequestPeerType.parse_requestPeerTypeChat($0) }
|
dict[-906990053] = { return Api.RequestPeerType.parse_requestPeerTypeChat($0) }
|
||||||
dict[1597737472] = { return Api.RequestPeerType.parse_requestPeerTypeUser($0) }
|
dict[1597737472] = { return Api.RequestPeerType.parse_requestPeerTypeUser($0) }
|
||||||
@ -898,7 +902,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) }
|
dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) }
|
||||||
dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) }
|
dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) }
|
||||||
dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) }
|
dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) }
|
||||||
dict[-294313259] = { return Api.StarsTransaction.parse_starsTransaction($0) }
|
dict[178185410] = { return Api.StarsTransaction.parse_starsTransaction($0) }
|
||||||
dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) }
|
dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) }
|
||||||
dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) }
|
dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) }
|
||||||
dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) }
|
dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) }
|
||||||
@ -1853,6 +1857,8 @@ public extension Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MessageReplyHeader:
|
case let _1 as Api.MessageReplyHeader:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.MessageReportOption:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MessageViews:
|
case let _1 as Api.MessageViews:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.MessagesFilter:
|
case let _1 as Api.MessagesFilter:
|
||||||
@ -1969,6 +1975,8 @@ public extension Api {
|
|||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.ReportReason:
|
case let _1 as Api.ReportReason:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
|
case let _1 as Api.ReportResult:
|
||||||
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.RequestPeerType:
|
case let _1 as Api.RequestPeerType:
|
||||||
_1.serialize(buffer, boxed)
|
_1.serialize(buffer, boxed)
|
||||||
case let _1 as Api.RequestedPeer:
|
case let _1 as Api.RequestedPeer:
|
||||||
|
|||||||
@ -482,6 +482,46 @@ public extension Api {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public extension Api {
|
||||||
|
enum MessageReportOption: TypeConstructorDescription {
|
||||||
|
case messageReportOption(text: String, option: Buffer)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .messageReportOption(let text, let option):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(2030298073)
|
||||||
|
}
|
||||||
|
serializeString(text, buffer: buffer, boxed: false)
|
||||||
|
serializeBytes(option, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .messageReportOption(let text, let option):
|
||||||
|
return ("messageReportOption", [("text", text as Any), ("option", option as Any)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_messageReportOption(_ reader: BufferReader) -> MessageReportOption? {
|
||||||
|
var _1: String?
|
||||||
|
_1 = parseString(reader)
|
||||||
|
var _2: Buffer?
|
||||||
|
_2 = parseBytes(reader)
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.MessageReportOption.messageReportOption(text: _1!, option: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum MessageViews: TypeConstructorDescription {
|
enum MessageViews: TypeConstructorDescription {
|
||||||
case messageViews(flags: Int32, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?)
|
case messageViews(flags: Int32, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?)
|
||||||
@ -902,87 +942,3 @@ public extension Api {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api {
|
|
||||||
enum NotificationSound: TypeConstructorDescription {
|
|
||||||
case notificationSoundDefault
|
|
||||||
case notificationSoundLocal(title: String, data: String)
|
|
||||||
case notificationSoundNone
|
|
||||||
case notificationSoundRingtone(id: Int64)
|
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
|
||||||
switch self {
|
|
||||||
case .notificationSoundDefault:
|
|
||||||
if boxed {
|
|
||||||
buffer.appendInt32(-1746354498)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case .notificationSoundLocal(let title, let data):
|
|
||||||
if boxed {
|
|
||||||
buffer.appendInt32(-2096391452)
|
|
||||||
}
|
|
||||||
serializeString(title, buffer: buffer, boxed: false)
|
|
||||||
serializeString(data, buffer: buffer, boxed: false)
|
|
||||||
break
|
|
||||||
case .notificationSoundNone:
|
|
||||||
if boxed {
|
|
||||||
buffer.appendInt32(1863070943)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case .notificationSoundRingtone(let id):
|
|
||||||
if boxed {
|
|
||||||
buffer.appendInt32(-9666487)
|
|
||||||
}
|
|
||||||
serializeInt64(id, buffer: buffer, boxed: false)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
|
||||||
switch self {
|
|
||||||
case .notificationSoundDefault:
|
|
||||||
return ("notificationSoundDefault", [])
|
|
||||||
case .notificationSoundLocal(let title, let data):
|
|
||||||
return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)])
|
|
||||||
case .notificationSoundNone:
|
|
||||||
return ("notificationSoundNone", [])
|
|
||||||
case .notificationSoundRingtone(let id):
|
|
||||||
return ("notificationSoundRingtone", [("id", id as Any)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? {
|
|
||||||
return Api.NotificationSound.notificationSoundDefault
|
|
||||||
}
|
|
||||||
public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? {
|
|
||||||
var _1: String?
|
|
||||||
_1 = parseString(reader)
|
|
||||||
var _2: String?
|
|
||||||
_2 = parseString(reader)
|
|
||||||
let _c1 = _1 != nil
|
|
||||||
let _c2 = _2 != nil
|
|
||||||
if _c1 && _c2 {
|
|
||||||
return Api.NotificationSound.notificationSoundLocal(title: _1!, data: _2!)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? {
|
|
||||||
return Api.NotificationSound.notificationSoundNone
|
|
||||||
}
|
|
||||||
public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? {
|
|
||||||
var _1: Int64?
|
|
||||||
_1 = reader.readInt64()
|
|
||||||
let _c1 = _1 != nil
|
|
||||||
if _c1 {
|
|
||||||
return Api.NotificationSound.notificationSoundRingtone(id: _1!)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,87 @@
|
|||||||
|
public extension Api {
|
||||||
|
enum NotificationSound: TypeConstructorDescription {
|
||||||
|
case notificationSoundDefault
|
||||||
|
case notificationSoundLocal(title: String, data: String)
|
||||||
|
case notificationSoundNone
|
||||||
|
case notificationSoundRingtone(id: Int64)
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .notificationSoundDefault:
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-1746354498)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case .notificationSoundLocal(let title, let data):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-2096391452)
|
||||||
|
}
|
||||||
|
serializeString(title, buffer: buffer, boxed: false)
|
||||||
|
serializeString(data, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
case .notificationSoundNone:
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(1863070943)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case .notificationSoundRingtone(let id):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-9666487)
|
||||||
|
}
|
||||||
|
serializeInt64(id, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .notificationSoundDefault:
|
||||||
|
return ("notificationSoundDefault", [])
|
||||||
|
case .notificationSoundLocal(let title, let data):
|
||||||
|
return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)])
|
||||||
|
case .notificationSoundNone:
|
||||||
|
return ("notificationSoundNone", [])
|
||||||
|
case .notificationSoundRingtone(let id):
|
||||||
|
return ("notificationSoundRingtone", [("id", id as Any)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_notificationSoundDefault(_ reader: BufferReader) -> NotificationSound? {
|
||||||
|
return Api.NotificationSound.notificationSoundDefault
|
||||||
|
}
|
||||||
|
public static func parse_notificationSoundLocal(_ reader: BufferReader) -> NotificationSound? {
|
||||||
|
var _1: String?
|
||||||
|
_1 = parseString(reader)
|
||||||
|
var _2: String?
|
||||||
|
_2 = parseString(reader)
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.NotificationSound.notificationSoundLocal(title: _1!, data: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static func parse_notificationSoundNone(_ reader: BufferReader) -> NotificationSound? {
|
||||||
|
return Api.NotificationSound.notificationSoundNone
|
||||||
|
}
|
||||||
|
public static func parse_notificationSoundRingtone(_ reader: BufferReader) -> NotificationSound? {
|
||||||
|
var _1: Int64?
|
||||||
|
_1 = reader.readInt64()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
if _c1 {
|
||||||
|
return Api.NotificationSound.notificationSoundRingtone(id: _1!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum NotifyPeer: TypeConstructorDescription {
|
enum NotifyPeer: TypeConstructorDescription {
|
||||||
case notifyBroadcasts
|
case notifyBroadcasts
|
||||||
|
|||||||
@ -134,6 +134,88 @@ public extension Api {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public extension Api {
|
||||||
|
enum ReportResult: TypeConstructorDescription {
|
||||||
|
case reportResultAddComment(flags: Int32, option: Buffer)
|
||||||
|
case reportResultChooseOption(title: String, options: [Api.MessageReportOption])
|
||||||
|
case reportResultReported
|
||||||
|
|
||||||
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
|
switch self {
|
||||||
|
case .reportResultAddComment(let flags, let option):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(1862904881)
|
||||||
|
}
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
|
serializeBytes(option, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
|
case .reportResultChooseOption(let title, let options):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-253435722)
|
||||||
|
}
|
||||||
|
serializeString(title, buffer: buffer, boxed: false)
|
||||||
|
buffer.appendInt32(481674261)
|
||||||
|
buffer.appendInt32(Int32(options.count))
|
||||||
|
for item in options {
|
||||||
|
item.serialize(buffer, true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .reportResultReported:
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(-1917633461)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
|
switch self {
|
||||||
|
case .reportResultAddComment(let flags, let option):
|
||||||
|
return ("reportResultAddComment", [("flags", flags as Any), ("option", option as Any)])
|
||||||
|
case .reportResultChooseOption(let title, let options):
|
||||||
|
return ("reportResultChooseOption", [("title", title as Any), ("options", options as Any)])
|
||||||
|
case .reportResultReported:
|
||||||
|
return ("reportResultReported", [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse_reportResultAddComment(_ reader: BufferReader) -> ReportResult? {
|
||||||
|
var _1: Int32?
|
||||||
|
_1 = reader.readInt32()
|
||||||
|
var _2: Buffer?
|
||||||
|
_2 = parseBytes(reader)
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.ReportResult.reportResultAddComment(flags: _1!, option: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static func parse_reportResultChooseOption(_ reader: BufferReader) -> ReportResult? {
|
||||||
|
var _1: String?
|
||||||
|
_1 = parseString(reader)
|
||||||
|
var _2: [Api.MessageReportOption]?
|
||||||
|
if let _ = reader.readInt32() {
|
||||||
|
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageReportOption.self)
|
||||||
|
}
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.ReportResult.reportResultChooseOption(title: _1!, options: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static func parse_reportResultReported(_ reader: BufferReader) -> ReportResult? {
|
||||||
|
return Api.ReportResult.reportResultReported
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum RequestPeerType: TypeConstructorDescription {
|
enum RequestPeerType: TypeConstructorDescription {
|
||||||
case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?)
|
case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?)
|
||||||
|
|||||||
@ -1002,13 +1002,13 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum StarsTransaction: TypeConstructorDescription {
|
enum StarsTransaction: TypeConstructorDescription {
|
||||||
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?)
|
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId):
|
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-294313259)
|
buffer.appendInt32(178185410)
|
||||||
}
|
}
|
||||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeString(id, buffer: buffer, boxed: false)
|
serializeString(id, buffer: buffer, boxed: false)
|
||||||
@ -1029,14 +1029,15 @@ public extension Api {
|
|||||||
}}
|
}}
|
||||||
if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)}
|
||||||
if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)}
|
||||||
|
if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId):
|
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift):
|
||||||
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any)])
|
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1077,6 +1078,10 @@ public extension Api {
|
|||||||
if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() }
|
if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() }
|
||||||
var _15: Int32?
|
var _15: Int32?
|
||||||
if Int(_1!) & Int(1 << 13) != 0 {_15 = reader.readInt32() }
|
if Int(_1!) & Int(1 << 13) != 0 {_15 = reader.readInt32() }
|
||||||
|
var _16: Api.StarGift?
|
||||||
|
if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() {
|
||||||
|
_16 = Api.parse(reader, signature: signature) as? Api.StarGift
|
||||||
|
} }
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
@ -1092,8 +1097,9 @@ public extension Api {
|
|||||||
let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil
|
let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil
|
||||||
let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil
|
let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil
|
||||||
let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil
|
let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil
|
||||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 {
|
let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil
|
||||||
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15)
|
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 {
|
||||||
|
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -7380,22 +7380,22 @@ public extension Api.functions.messages {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api.functions.messages {
|
public extension Api.functions.messages {
|
||||||
static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ReportResult>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(-1991005362)
|
buffer.appendInt32(-59199589)
|
||||||
peer.serialize(buffer, true)
|
peer.serialize(buffer, true)
|
||||||
buffer.appendInt32(481674261)
|
buffer.appendInt32(481674261)
|
||||||
buffer.appendInt32(Int32(id.count))
|
buffer.appendInt32(Int32(id.count))
|
||||||
for item in id {
|
for item in id {
|
||||||
serializeInt32(item, buffer: buffer, boxed: false)
|
serializeInt32(item, buffer: buffer, boxed: false)
|
||||||
}
|
}
|
||||||
reason.serialize(buffer, true)
|
serializeBytes(option, buffer: buffer, boxed: false)
|
||||||
serializeString(message, buffer: buffer, boxed: false)
|
serializeString(message, buffer: buffer, boxed: false)
|
||||||
return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
return (FunctionDescription(name: "messages.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.Bool?
|
var result: Api.ReportResult?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
result = Api.parse(reader, signature: signature) as? Api.ReportResult
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@ -10787,22 +10787,22 @@ public extension Api.functions.stories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public extension Api.functions.stories {
|
public extension Api.functions.stories {
|
||||||
static func report(peer: Api.InputPeer, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
static func report(peer: Api.InputPeer, id: [Int32], option: Buffer, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ReportResult>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(421788300)
|
buffer.appendInt32(433646405)
|
||||||
peer.serialize(buffer, true)
|
peer.serialize(buffer, true)
|
||||||
buffer.appendInt32(481674261)
|
buffer.appendInt32(481674261)
|
||||||
buffer.appendInt32(Int32(id.count))
|
buffer.appendInt32(Int32(id.count))
|
||||||
for item in id {
|
for item in id {
|
||||||
serializeInt32(item, buffer: buffer, boxed: false)
|
serializeInt32(item, buffer: buffer, boxed: false)
|
||||||
}
|
}
|
||||||
reason.serialize(buffer, true)
|
serializeBytes(option, buffer: buffer, boxed: false)
|
||||||
serializeString(message, buffer: buffer, boxed: false)
|
serializeString(message, buffer: buffer, boxed: false)
|
||||||
return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
return (FunctionDescription(name: "stories.report", parameters: [("peer", String(describing: peer)), ("id", String(describing: id)), ("option", String(describing: option)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ReportResult? in
|
||||||
let reader = BufferReader(buffer)
|
let reader = BufferReader(buffer)
|
||||||
var result: Api.Bool?
|
var result: Api.ReportResult?
|
||||||
if let signature = reader.readInt32() {
|
if let signature = reader.readInt32() {
|
||||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
result = Api.parse(reader, signature: signature) as? Api.ReportResult
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,185 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import AvatarNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import Markdown
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
|
final class VideoChatExpandedSpeakingToastComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let peer: EnginePeer
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let action: (EnginePeer) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
peer: EnginePeer,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
action: @escaping (EnginePeer) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.peer = peer
|
||||||
|
self.strings = strings
|
||||||
|
self.theme = theme
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: VideoChatExpandedSpeakingToastComponent, rhs: VideoChatExpandedSpeakingToastComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: HighlightTrackingButton {
|
||||||
|
private let background = ComponentView<Empty>()
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private var avatarNode: AvatarNode?
|
||||||
|
|
||||||
|
private var component: VideoChatExpandedSpeakingToastComponent?
|
||||||
|
private var isUpdating: Bool = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
if let component = self.component {
|
||||||
|
component.action(component.peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: VideoChatExpandedSpeakingToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.isUpdating = true
|
||||||
|
defer {
|
||||||
|
self.isUpdating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
let avatarLeftInset: CGFloat = 3.0
|
||||||
|
let avatarVerticalInset: CGFloat = 3.0
|
||||||
|
let avatarSpacing: CGFloat = 12.0
|
||||||
|
let rightInset: CGFloat = 16.0
|
||||||
|
let avatarWidth: CGFloat = 32.0
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
|
let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:])
|
||||||
|
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:])
|
||||||
|
let titleText = addAttributesToStringWithRanges(component.strings.VoiceChat_ParticipantIsSpeaking(component.peer.displayTitle(strings: component.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(titleText)
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - avatarLeftInset - avatarWidth - avatarSpacing - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let size = CGSize(width: avatarLeftInset + avatarWidth + avatarSpacing + titleSize.width + rightInset, height: avatarWidth + avatarVerticalInset * 2.0)
|
||||||
|
|
||||||
|
let _ = self.background.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||||
|
color: UIColor(white: 0.0, alpha: 0.9),
|
||||||
|
cornerRadius: size.height * 0.5,
|
||||||
|
smoothCorners: false
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: size
|
||||||
|
)
|
||||||
|
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
if let backgroundView = self.background.view {
|
||||||
|
if backgroundView.superview == nil {
|
||||||
|
backgroundView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(backgroundView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarWidth + avatarSpacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
titleView.layer.anchorPoint = CGPoint()
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||||
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarNode: AvatarNode
|
||||||
|
if let current = self.avatarNode {
|
||||||
|
avatarNode = current
|
||||||
|
} else {
|
||||||
|
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
|
||||||
|
self.avatarNode = avatarNode
|
||||||
|
self.addSubview(avatarNode.view)
|
||||||
|
avatarNode.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize = CGSize(width: avatarWidth, height: avatarWidth)
|
||||||
|
|
||||||
|
let clipStyle: AvatarNodeClipStyle
|
||||||
|
if case let .channel(channel) = component.peer, channel.flags.contains(.isForum) {
|
||||||
|
clipStyle = .roundedRect
|
||||||
|
} else {
|
||||||
|
clipStyle = .round
|
||||||
|
}
|
||||||
|
|
||||||
|
if component.peer.smallProfileImage != nil {
|
||||||
|
avatarNode.setPeerV2(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
peer: component.peer,
|
||||||
|
authorOfMessage: nil,
|
||||||
|
overrideImage: nil,
|
||||||
|
emptyColor: nil,
|
||||||
|
clipStyle: .round,
|
||||||
|
synchronousLoad: false,
|
||||||
|
displayDimensions: avatarSize
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, clipStyle: clipStyle, synchronousLoad: false, displayDimensions: avatarSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: avatarVerticalInset), size: avatarSize)
|
||||||
|
transition.setPosition(view: avatarNode.view, position: avatarFrame.center)
|
||||||
|
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
avatarNode.updateSize(size: avatarSize)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -184,7 +184,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
case connecting
|
case connecting
|
||||||
case muted
|
case muted
|
||||||
case unmuted(pushToTalk: Bool)
|
case unmuted(pushToTalk: Bool)
|
||||||
case raiseHand
|
case raiseHand(isRaised: Bool)
|
||||||
case scheduled(state: ScheduledState)
|
case scheduled(state: ScheduledState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +226,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
private var disappearingBackgrounds: [UIImageView] = []
|
private var disappearingBackgrounds: [UIImageView] = []
|
||||||
private var progressIndicator: RadialStatusNode?
|
private var progressIndicator: RadialStatusNode?
|
||||||
private let title = ComponentView<Empty>()
|
private let title = ComponentView<Empty>()
|
||||||
|
private var subtitle: ComponentView<Empty>?
|
||||||
private let icon: VoiceChatActionButtonIconNode
|
private let icon: VoiceChatActionButtonIconNode
|
||||||
|
|
||||||
private var glowView: GlowView?
|
private var glowView: GlowView?
|
||||||
@ -322,6 +323,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
|
||||||
|
|
||||||
let titleText: String
|
let titleText: String
|
||||||
|
var subtitleText: String?
|
||||||
var isEnabled = true
|
var isEnabled = true
|
||||||
switch component.content {
|
switch component.content {
|
||||||
case .connecting:
|
case .connecting:
|
||||||
@ -331,8 +333,14 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
titleText = "Unmute"
|
titleText = "Unmute"
|
||||||
case let .unmuted(isPushToTalk):
|
case let .unmuted(isPushToTalk):
|
||||||
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
||||||
case .raiseHand:
|
case let .raiseHand(isRaised):
|
||||||
titleText = "Raise Hand"
|
if isRaised {
|
||||||
|
titleText = "You asked to speak"
|
||||||
|
subtitleText = "We let the speakers know"
|
||||||
|
} else {
|
||||||
|
titleText = "Muted by Admin"
|
||||||
|
subtitleText = "Tap if you want to speak"
|
||||||
|
}
|
||||||
case let .scheduled(state):
|
case let .scheduled(state):
|
||||||
switch state {
|
switch state {
|
||||||
case .start:
|
case .start:
|
||||||
@ -353,7 +361,7 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
|
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
containerSize: CGSize(width: 180.0, height: 100.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
let size = CGSize(width: availableSize.width, height: availableSize.height)
|
let size = CGSize(width: availableSize.width, height: availableSize.height)
|
||||||
@ -470,7 +478,10 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
|
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
|
var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
|
||||||
|
if subtitleText != nil {
|
||||||
|
titleFrame.origin.y -= 5.0
|
||||||
|
}
|
||||||
if let titleView = self.title.view {
|
if let titleView = self.title.view {
|
||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
titleView.isUserInteractionEnabled = false
|
titleView.isUserInteractionEnabled = false
|
||||||
@ -481,6 +492,47 @@ final class VideoChatMicButtonComponent: Component {
|
|||||||
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
alphaTransition.setAlpha(view: titleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let subtitleText {
|
||||||
|
let subtitle: ComponentView<Empty>
|
||||||
|
var subtitleTransition = transition
|
||||||
|
if let current = self.subtitle {
|
||||||
|
subtitle = current
|
||||||
|
} else {
|
||||||
|
subtitleTransition = subtitleTransition.withAnimation(.none)
|
||||||
|
subtitle = ComponentView()
|
||||||
|
self.subtitle = subtitle
|
||||||
|
}
|
||||||
|
let subtitleSize = subtitle.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: .white))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 180.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + 1.0), size: subtitleSize)
|
||||||
|
if let subtitleView = subtitle.view {
|
||||||
|
if subtitleView.superview == nil {
|
||||||
|
subtitleView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(subtitleView)
|
||||||
|
|
||||||
|
subtitleView.alpha = 0.0
|
||||||
|
transition.animateScale(view: subtitleView, from: 0.001, to: 1.0)
|
||||||
|
}
|
||||||
|
subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.center)
|
||||||
|
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
|
||||||
|
alphaTransition.setAlpha(view: subtitleView, alpha: component.isCollapsed ? 0.0 : 1.0)
|
||||||
|
}
|
||||||
|
} else if let subtitle = self.subtitle {
|
||||||
|
self.subtitle = nil
|
||||||
|
if let subtitleView = subtitle.view {
|
||||||
|
transition.setScale(view: subtitleView, scale: 0.001)
|
||||||
|
alphaTransition.setAlpha(view: subtitleView, alpha: 0.0, completion: { [weak subtitleView] _ in
|
||||||
|
subtitleView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.icon.view.superview == nil {
|
if self.icon.view.superview == nil {
|
||||||
self.icon.view.isUserInteractionEnabled = false
|
self.icon.view.isUserInteractionEnabled = false
|
||||||
self.addSubview(self.icon.view)
|
self.addSubview(self.icon.view)
|
||||||
|
|||||||
@ -283,7 +283,7 @@ final class VideoChatParticipantAvatarComponent: Component {
|
|||||||
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
avatarNode.updateSize(size: avatarSize)
|
avatarNode.updateSize(size: avatarSize)
|
||||||
|
|
||||||
let blobScale: CGFloat = 1.5
|
let blobScale: CGFloat = 2.0
|
||||||
|
|
||||||
if self.audioLevelDisposable == nil {
|
if self.audioLevelDisposable == nil {
|
||||||
struct Level {
|
struct Level {
|
||||||
|
|||||||
@ -117,6 +117,13 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class EventCycleState {
|
||||||
|
var ignoreScrolling: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let call: PresentationGroupCall
|
let call: PresentationGroupCall
|
||||||
let participants: Participants?
|
let participants: Participants?
|
||||||
let speakingParticipants: Set<EnginePeer.Id>
|
let speakingParticipants: Set<EnginePeer.Id>
|
||||||
@ -132,6 +139,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||||
let openInviteMembers: () -> Void
|
let openInviteMembers: () -> Void
|
||||||
|
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
call: PresentationGroupCall,
|
call: PresentationGroupCall,
|
||||||
@ -148,7 +156,8 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
||||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
||||||
openInviteMembers: @escaping () -> Void
|
openInviteMembers: @escaping () -> Void,
|
||||||
|
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
||||||
) {
|
) {
|
||||||
self.call = call
|
self.call = call
|
||||||
self.participants = participants
|
self.participants = participants
|
||||||
@ -165,6 +174,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
||||||
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
||||||
self.openInviteMembers = openInviteMembers
|
self.openInviteMembers = openInviteMembers
|
||||||
|
self.visibleParticipantsUpdated = visibleParticipantsUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
|
||||||
@ -477,7 +487,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
|
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
|
||||||
} else {
|
} else {
|
||||||
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
|
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height))
|
||||||
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom))
|
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX + layout.mainColumn.insets.left, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth - layout.mainColumn.insets.left - layout.mainColumn.insets.right, height: containerSize.height - layout.mainColumn.insets.top))
|
||||||
|
|
||||||
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height))
|
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height))
|
||||||
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
|
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top))
|
||||||
@ -599,6 +609,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
final class View: UIView, UIScrollViewDelegate {
|
final class View: UIView, UIScrollViewDelegate {
|
||||||
private let scrollViewClippingContainer: SolidRoundedCornersContainer
|
private let scrollViewClippingContainer: SolidRoundedCornersContainer
|
||||||
private let scrollView: ScrollView
|
private let scrollView: ScrollView
|
||||||
|
private let scrollViewBottomShadowView: UIImageView
|
||||||
|
|
||||||
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
|
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
|
||||||
private let separateVideoScrollView: ScrollView
|
private let separateVideoScrollView: ScrollView
|
||||||
@ -622,6 +633,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
private let expandedGridItemContainer: UIView
|
private let expandedGridItemContainer: UIView
|
||||||
private var expandedControlsView: ComponentView<Empty>?
|
private var expandedControlsView: ComponentView<Empty>?
|
||||||
private var expandedThumbnailsView: ComponentView<Empty>?
|
private var expandedThumbnailsView: ComponentView<Empty>?
|
||||||
|
private var expandedSpeakingToast: ComponentView<Empty>?
|
||||||
|
|
||||||
private var listItemViews: [EnginePeer.Id: ListItem] = [:]
|
private var listItemViews: [EnginePeer.Id: ListItem] = [:]
|
||||||
private let listItemViewContainer: UIView
|
private let listItemViewContainer: UIView
|
||||||
@ -635,9 +647,13 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
private var currentLoadMoreToken: String?
|
private var currentLoadMoreToken: String?
|
||||||
|
|
||||||
|
private var mainScrollViewEventCycleState: EventCycleState?
|
||||||
|
private var separateVideoScrollViewEventCycleState: EventCycleState?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
|
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||||
self.scrollView = ScrollView()
|
self.scrollView = ScrollView()
|
||||||
|
self.scrollViewBottomShadowView = UIImageView()
|
||||||
|
|
||||||
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
|
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||||
self.separateVideoScrollView = ScrollView()
|
self.separateVideoScrollView = ScrollView()
|
||||||
@ -687,6 +703,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.scrollViewClippingContainer.addSubview(self.scrollView)
|
self.scrollViewClippingContainer.addSubview(self.scrollView)
|
||||||
self.addSubview(self.scrollViewClippingContainer)
|
self.addSubview(self.scrollViewClippingContainer)
|
||||||
self.addSubview(self.scrollViewClippingContainer.cornersView)
|
self.addSubview(self.scrollViewClippingContainer.cornersView)
|
||||||
|
self.addSubview(self.scrollViewBottomShadowView)
|
||||||
|
|
||||||
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
|
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
|
||||||
self.addSubview(self.separateVideoScrollViewClippingContainer)
|
self.addSubview(self.separateVideoScrollViewClippingContainer)
|
||||||
@ -765,10 +782,46 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
if !self.ignoreScrolling {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
if let eventCycleState = self.mainScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.updateScrolling(transition: .immediate)
|
self.updateScrolling(transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
if let eventCycleState = self.mainScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
targetContentOffset.pointee.y = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
if let eventCycleState = self.separateVideoScrollViewEventCycleState {
|
||||||
|
if eventCycleState.ignoreScrolling {
|
||||||
|
targetContentOffset.pointee.y = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateScrolling(transition: ComponentTransition) {
|
private func updateScrolling(transition: ComponentTransition) {
|
||||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
return
|
return
|
||||||
@ -832,11 +885,18 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
var validGridItemIds: [VideoParticipantKey] = []
|
var validGridItemIds: [VideoParticipantKey] = []
|
||||||
var validGridItemIndices: [Int] = []
|
var validGridItemIndices: [Int] = []
|
||||||
|
|
||||||
|
var clippedScrollViewBounds = self.scrollView.bounds
|
||||||
|
clippedScrollViewBounds.origin.y += component.layout.mainColumn.insets.top
|
||||||
|
clippedScrollViewBounds.size.height -= component.layout.mainColumn.insets.top + component.layout.mainColumn.insets.bottom
|
||||||
|
|
||||||
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
||||||
|
let clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
||||||
if itemLayout.layout.videoColumn == nil {
|
if itemLayout.layout.videoColumn == nil {
|
||||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||||
|
clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds)
|
||||||
} else {
|
} else {
|
||||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
|
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
|
||||||
|
clippedVisibleGridItemRange = visibleGridItemRange
|
||||||
}
|
}
|
||||||
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
||||||
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||||
@ -852,6 +912,8 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
validGridItemIndices.append(index)
|
validGridItemIndices.append(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var visibleParticipants: [EnginePeer.Id] = []
|
||||||
|
|
||||||
for index in validGridItemIndices {
|
for index in validGridItemIndices {
|
||||||
let videoParticipant = self.gridParticipants[index]
|
let videoParticipant = self.gridParticipants[index]
|
||||||
@ -879,6 +941,10 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) {
|
||||||
|
visibleParticipants.append(videoParticipant.key.id)
|
||||||
|
}
|
||||||
|
|
||||||
var suppressItemExpansionCollapseAnimation = false
|
var suppressItemExpansionCollapseAnimation = false
|
||||||
if isItemExpanded {
|
if isItemExpanded {
|
||||||
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
|
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
|
||||||
@ -1066,11 +1132,16 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
|
|
||||||
var validListItemIds: [EnginePeer.Id] = []
|
var validListItemIds: [EnginePeer.Id] = []
|
||||||
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
|
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
|
||||||
|
let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds)
|
||||||
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
|
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
|
||||||
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
|
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
|
||||||
let participant = self.listParticipants[i]
|
let participant = self.listParticipants[i]
|
||||||
validListItemIds.append(participant.peer.id)
|
validListItemIds.append(participant.peer.id)
|
||||||
|
|
||||||
|
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
|
||||||
|
visibleParticipants.append(participant.peer.id)
|
||||||
|
}
|
||||||
|
|
||||||
var itemTransition = transition
|
var itemTransition = transition
|
||||||
let itemView: ListItem
|
let itemView: ListItem
|
||||||
if let current = self.listItemViews[participant.peer.id] {
|
if let current = self.listItemViews[participant.peer.id] {
|
||||||
@ -1087,9 +1158,15 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == component.call.accountContext.account.peerId {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
|
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
|
||||||
} else if component.speakingParticipants.contains(participant.peer.id) {
|
} else if component.speakingParticipants.contains(participant.peer.id) {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
|
if let volume = participant.volume, volume != 10000 {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive)
|
||||||
|
} else {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
|
||||||
|
}
|
||||||
|
} else if let about = participant.about, !about.isEmpty {
|
||||||
|
subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral)
|
||||||
} else {
|
} else {
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral)
|
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
||||||
@ -1412,12 +1489,86 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { $0 != expandedVideoState.mainParticipant.id }), let speakingPeer = participants.participants.first(where: { $0.peer.id == firstOther })?.peer {
|
||||||
|
let expandedSpeakingToast: ComponentView<Empty>
|
||||||
|
var expandedSpeakingToastTransition = transition
|
||||||
|
if let current = self.expandedSpeakingToast {
|
||||||
|
expandedSpeakingToast = current
|
||||||
|
} else {
|
||||||
|
expandedSpeakingToastTransition = expandedSpeakingToastTransition.withAnimation(.none)
|
||||||
|
expandedSpeakingToast = ComponentView()
|
||||||
|
self.expandedSpeakingToast = expandedSpeakingToast
|
||||||
|
}
|
||||||
|
let expandedSpeakingToastSize = expandedSpeakingToast.update(
|
||||||
|
transition: expandedSpeakingToastTransition,
|
||||||
|
component: AnyComponent(VideoChatExpandedSpeakingToastComponent(
|
||||||
|
context: component.call.accountContext,
|
||||||
|
peer: EnginePeer(speakingPeer),
|
||||||
|
strings: component.strings,
|
||||||
|
theme: component.theme,
|
||||||
|
action: { [weak self] peer in
|
||||||
|
guard let self, let component = self.component, let participants = component.participants else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let participant = participants.participants.first(where: { $0.peer.id == peer.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var key: VideoParticipantKey?
|
||||||
|
if participant.presentationDescription != nil {
|
||||||
|
key = VideoParticipantKey(id: peer.id, isPresentation: true)
|
||||||
|
} else if participant.videoDescription != nil {
|
||||||
|
key = VideoParticipantKey(id: peer.id, isPresentation: false)
|
||||||
|
}
|
||||||
|
if let key {
|
||||||
|
component.updateMainParticipant(key, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
|
||||||
|
)
|
||||||
|
let expandedSpeakingToastFrame = CGRect(origin: CGPoint(x: floor((itemLayout.expandedGrid.itemContainerFrame().size.width - expandedSpeakingToastSize.width) * 0.5), y: 44.0), size: expandedSpeakingToastSize)
|
||||||
|
if let expandedSpeakingToastView = expandedSpeakingToast.view {
|
||||||
|
var animateIn = false
|
||||||
|
if expandedSpeakingToastView.superview == nil {
|
||||||
|
animateIn = true
|
||||||
|
self.expandedGridItemContainer.addSubview(expandedSpeakingToastView)
|
||||||
|
}
|
||||||
|
expandedSpeakingToastTransition.setFrame(view: expandedSpeakingToastView, frame: expandedSpeakingToastFrame)
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
alphaTransition.animateAlpha(view: expandedSpeakingToastView, from: 0.0, to: 1.0)
|
||||||
|
transition.animateScale(view: expandedSpeakingToastView, from: 0.6, to: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let expandedSpeakingToast = self.expandedSpeakingToast {
|
||||||
|
self.expandedSpeakingToast = nil
|
||||||
|
if let expandedSpeakingToastView = expandedSpeakingToast.view {
|
||||||
|
alphaTransition.setAlpha(view: expandedSpeakingToastView, alpha: 0.0, completion: { [weak expandedSpeakingToastView] _ in
|
||||||
|
expandedSpeakingToastView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.setScale(view: expandedSpeakingToastView, scale: 0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 {
|
if let participants = component.participants, let loadMoreToken = participants.loadMoreToken, visibleListItemRange.maxIndex >= self.listParticipants.count - 5 {
|
||||||
if self.currentLoadMoreToken != loadMoreToken {
|
if self.currentLoadMoreToken != loadMoreToken {
|
||||||
self.currentLoadMoreToken = loadMoreToken
|
self.currentLoadMoreToken = loadMoreToken
|
||||||
component.call.loadMoreMembers(token: loadMoreToken)
|
component.call.loadMoreMembers(token: loadMoreToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component.visibleParticipantsUpdated(Set(visibleParticipants))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) {
|
||||||
|
if scrollView == self.scrollView {
|
||||||
|
self.mainScrollViewEventCycleState = eventCycleState
|
||||||
|
} else if scrollView == self.separateVideoScrollView {
|
||||||
|
self.separateVideoScrollViewEventCycleState = eventCycleState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatParticipantsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
@ -1482,11 +1633,16 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
if let participants = component.participants {
|
if let participants = component.participants {
|
||||||
for participant in participants.participants {
|
for participant in participants.participants {
|
||||||
|
var isFullyMuted = false
|
||||||
|
if let muteState = participant.muteState, !muteState.canUnmute {
|
||||||
|
isFullyMuted = true
|
||||||
|
}
|
||||||
|
|
||||||
var hasVideo = false
|
var hasVideo = false
|
||||||
if participant.videoDescription != nil {
|
if participant.videoDescription != nil {
|
||||||
hasVideo = true
|
hasVideo = true
|
||||||
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
|
let videoParticipant = VideoParticipant(participant: participant, isPresentation: false)
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId || participant.peer.id == participants.myPeerId {
|
if participant.peer.id == participants.myPeerId {
|
||||||
gridParticipants.insert(videoParticipant, at: 0)
|
gridParticipants.insert(videoParticipant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
gridParticipants.append(videoParticipant)
|
gridParticipants.append(videoParticipant)
|
||||||
@ -1495,14 +1651,14 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if participant.presentationDescription != nil {
|
if participant.presentationDescription != nil {
|
||||||
hasVideo = true
|
hasVideo = true
|
||||||
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
|
let videoParticipant = VideoParticipant(participant: participant, isPresentation: true)
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == participants.myPeerId {
|
||||||
gridParticipants.insert(videoParticipant, at: 0)
|
gridParticipants.insert(videoParticipant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
gridParticipants.append(videoParticipant)
|
gridParticipants.append(videoParticipant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasVideo || component.layout.videoColumn != nil {
|
if !hasVideo || component.layout.videoColumn != nil {
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == participants.myPeerId && !isFullyMuted {
|
||||||
listParticipants.insert(participant, at: 0)
|
listParticipants.insert(participant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
listParticipants.append(participant)
|
listParticipants.append(participant)
|
||||||
@ -1594,6 +1750,37 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
smoothCorners: false
|
smoothCorners: false
|
||||||
), transition: transition)
|
), transition: transition)
|
||||||
|
|
||||||
|
if self.scrollViewBottomShadowView.image == nil {
|
||||||
|
let height: CGFloat = 80.0
|
||||||
|
let baseGradientAlpha: CGFloat = 1.0
|
||||||
|
let numSteps = 8
|
||||||
|
let firstStep = 0
|
||||||
|
let firstLocation = 0.0
|
||||||
|
let colors = (0 ..< numSteps).map { i -> UIColor in
|
||||||
|
if i < firstStep {
|
||||||
|
return UIColor(white: 1.0, alpha: 1.0)
|
||||||
|
} else {
|
||||||
|
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||||
|
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
|
||||||
|
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let locations = (0 ..< numSteps).map { i -> CGFloat in
|
||||||
|
if i < firstStep {
|
||||||
|
return 0.0
|
||||||
|
} else {
|
||||||
|
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||||
|
return (firstLocation + (1.0 - firstLocation) * step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrollViewBottomShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0))
|
||||||
|
self.scrollViewBottomShadowView.tintColor = .black
|
||||||
|
}
|
||||||
|
let scrollViewBottomShadowOverflow: CGFloat = 30.0
|
||||||
|
let scrollViewBottomShadowFrame = CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX, y: itemLayout.scrollClippingFrame.maxY - component.layout.mainColumn.insets.bottom - scrollViewBottomShadowOverflow), size: CGSize(width: itemLayout.scrollClippingFrame.width, height: component.layout.mainColumn.insets.bottom + scrollViewBottomShadowOverflow))
|
||||||
|
transition.setFrame(view: self.scrollViewBottomShadowView, frame: scrollViewBottomShadowFrame)
|
||||||
|
|
||||||
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
|
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
|
||||||
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
|
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
|
||||||
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
|
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
|
||||||
|
|||||||
@ -41,15 +41,22 @@ final class VideoChatScreenComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PanGestureState {
|
private final class PanState {
|
||||||
var offsetFraction: CGFloat
|
var fraction: CGFloat
|
||||||
|
weak var scrollView: UIScrollView?
|
||||||
|
var startContentOffsetY: CGFloat = 0.0
|
||||||
|
var accumulatedOffset: CGFloat = 0.0
|
||||||
|
var dismissedTooltips: Bool = false
|
||||||
|
var didLockScrolling: Bool = false
|
||||||
|
var contentOffset: CGFloat?
|
||||||
|
|
||||||
init(offsetFraction: CGFloat) {
|
init(fraction: CGFloat, scrollView: UIScrollView?) {
|
||||||
self.offsetFraction = offsetFraction
|
self.fraction = fraction
|
||||||
|
self.scrollView = scrollView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView, UIGestureRecognizerDelegate {
|
||||||
let containerView: UIView
|
let containerView: UIView
|
||||||
|
|
||||||
var component: VideoChatScreenComponent?
|
var component: VideoChatScreenComponent?
|
||||||
@ -57,7 +64,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
weak var state: EmptyComponentState?
|
weak var state: EmptyComponentState?
|
||||||
var isUpdating: Bool = false
|
var isUpdating: Bool = false
|
||||||
|
|
||||||
private var panGestureState: PanGestureState?
|
private var verticalPanState: PanState?
|
||||||
var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
|
var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
|
||||||
var completionOnPanGestureApply: (() -> Void)?
|
var completionOnPanGestureApply: (() -> Void)?
|
||||||
|
|
||||||
@ -95,6 +102,9 @@ final class VideoChatScreenComponent: Component {
|
|||||||
var members: PresentationGroupCallMembers?
|
var members: PresentationGroupCallMembers?
|
||||||
var membersDisposable: Disposable?
|
var membersDisposable: Disposable?
|
||||||
|
|
||||||
|
var speakingParticipantPeers: [EnginePeer] = []
|
||||||
|
var visibleParticipants: Set<EnginePeer.Id> = Set()
|
||||||
|
|
||||||
let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
|
let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
var applicationStateDisposable: Disposable?
|
var applicationStateDisposable: Disposable?
|
||||||
|
|
||||||
@ -117,9 +127,11 @@ final class VideoChatScreenComponent: Component {
|
|||||||
|
|
||||||
self.addSubview(self.containerView)
|
self.addSubview(self.containerView)
|
||||||
|
|
||||||
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||||
|
panGestureRecognizer.delegate = self
|
||||||
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
|
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -139,37 +151,159 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func animateIn() {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
|
|
||||||
self.panGestureState = nil
|
self.verticalPanState = nil
|
||||||
self.state?.updated(transition: .spring(duration: 0.5))
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(completion: @escaping () -> Void) {
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: 1.0)
|
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
|
||||||
self.completionOnPanGestureApply = completion
|
self.completionOnPanGestureApply = completion
|
||||||
self.state?.updated(transition: .spring(duration: 0.5))
|
self.state?.updated(transition: .spring(duration: 0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer is UITapGestureRecognizer {
|
||||||
|
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer is UIPanGestureRecognizer {
|
||||||
|
if let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||||
|
if otherGestureRecognizer.view is UIScrollView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
if otherGestureRecognizer.view === participantsView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .began, .changed:
|
case .began, .changed:
|
||||||
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
|
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
|
||||||
let translation = recognizer.translation(in: self)
|
let translation = recognizer.translation(in: self)
|
||||||
self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
|
let fraction = max(0.0, translation.y / self.bounds.height)
|
||||||
self.state?.updated(transition: .immediate)
|
if let verticalPanState = self.verticalPanState {
|
||||||
|
verticalPanState.fraction = fraction
|
||||||
|
} else {
|
||||||
|
var targetScrollView: UIScrollView?
|
||||||
|
if case .began = recognizer.state, let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
if let hitResult = participantsView.hitTest(self.convert(recognizer.location(in: self), to: participantsView), with: nil) {
|
||||||
|
func findTargetScrollView(target: UIView, minParent: UIView) -> UIScrollView? {
|
||||||
|
if target === participantsView {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if let target = target as? UIScrollView {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
if let parent = target.superview {
|
||||||
|
return findTargetScrollView(target: parent, minParent: minParent)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetScrollView = findTargetScrollView(target: hitResult, minParent: participantsView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.verticalPanState = PanState(fraction: fraction, scrollView: targetScrollView)
|
||||||
|
if let targetScrollView {
|
||||||
|
self.verticalPanState?.contentOffset = targetScrollView.contentOffset.y
|
||||||
|
self.verticalPanState?.startContentOffsetY = recognizer.translation(in: self).y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let verticalPanState = self.verticalPanState {
|
||||||
|
/*if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips {
|
||||||
|
verticalPanState.dismissedTooltips = true
|
||||||
|
self.dismissAllTooltips()
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if let scrollView = verticalPanState.scrollView {
|
||||||
|
let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY
|
||||||
|
let overflowY = scrollView.contentOffset.y - relativeTranslationY
|
||||||
|
|
||||||
|
if !verticalPanState.didLockScrolling {
|
||||||
|
if scrollView.contentOffset.y == 0.0 {
|
||||||
|
verticalPanState.didLockScrolling = true
|
||||||
|
}
|
||||||
|
if let previousContentOffset = verticalPanState.contentOffset, (previousContentOffset < 0.0) != (scrollView.contentOffset.y < 0.0) {
|
||||||
|
verticalPanState.didLockScrolling = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resetContentOffset = false
|
||||||
|
if verticalPanState.didLockScrolling {
|
||||||
|
verticalPanState.accumulatedOffset += -overflowY
|
||||||
|
|
||||||
|
if verticalPanState.accumulatedOffset < 0.0 {
|
||||||
|
verticalPanState.accumulatedOffset = 0.0
|
||||||
|
}
|
||||||
|
if scrollView.contentOffset.y < 0.0 {
|
||||||
|
resetContentOffset = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
verticalPanState.accumulatedOffset += -overflowY
|
||||||
|
verticalPanState.accumulatedOffset = max(0.0, verticalPanState.accumulatedOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verticalPanState.accumulatedOffset > 0.0 || resetContentOffset {
|
||||||
|
scrollView.contentOffset = CGPoint()
|
||||||
|
|
||||||
|
if let participantsView = self.participants.view as? VideoChatParticipantsComponent.View {
|
||||||
|
let eventCycleState = VideoChatParticipantsComponent.EventCycleState()
|
||||||
|
eventCycleState.ignoreScrolling = true
|
||||||
|
participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: eventCycleState)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak scrollView, weak participantsView] in
|
||||||
|
guard let participantsView, let scrollView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
participantsView.setEventCycleState(scrollView: scrollView, eventCycleState: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verticalPanState.contentOffset = scrollView.contentOffset.y
|
||||||
|
verticalPanState.startContentOffsetY = recognizer.translation(in: self).y
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .cancelled, .ended:
|
case .cancelled, .ended:
|
||||||
if !self.bounds.height.isZero {
|
if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState {
|
||||||
let translation = recognizer.translation(in: self)
|
let translation = recognizer.translation(in: self)
|
||||||
let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
|
verticalPanState.fraction = max(0.0, translation.y / self.bounds.height)
|
||||||
|
|
||||||
|
let effectiveFraction: CGFloat
|
||||||
|
if verticalPanState.scrollView != nil {
|
||||||
|
effectiveFraction = verticalPanState.accumulatedOffset / self.bounds.height
|
||||||
|
} else {
|
||||||
|
effectiveFraction = verticalPanState.fraction
|
||||||
|
}
|
||||||
|
|
||||||
let velocity = recognizer.velocity(in: self)
|
let velocity = recognizer.velocity(in: self)
|
||||||
|
|
||||||
self.panGestureState = nil
|
self.verticalPanState = nil
|
||||||
if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 {
|
if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) {
|
||||||
self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0)
|
self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil)
|
||||||
self.notifyDismissedInteractivelyOnPanGestureApply = true
|
self.notifyDismissedInteractivelyOnPanGestureApply = true
|
||||||
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
|
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
|
||||||
controller.notifyDismissed()
|
controller.notifyDismissed()
|
||||||
@ -347,6 +481,15 @@ final class VideoChatScreenComponent: Component {
|
|||||||
guard let component = self.component, let environment = self.environment else {
|
guard let component = self.component, let environment = self.environment else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if case .connecting = callState.networkState {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let muteState = callState.muteState, !muteState.canUnmute {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
HapticFeedback().impact(.light)
|
HapticFeedback().impact(.light)
|
||||||
if component.call.hasVideo {
|
if component.call.hasVideo {
|
||||||
@ -556,6 +699,39 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func onVisibleParticipantsUpdated(ids: Set<EnginePeer.Id>) {
|
||||||
|
if self.visibleParticipants == ids {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.visibleParticipants = ids
|
||||||
|
self.updateTitleSpeakingStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTitleSpeakingStatus() {
|
||||||
|
guard let titleView = self.title.view as? VideoChatTitleComponent.View else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.speakingParticipantPeers.isEmpty {
|
||||||
|
titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2))
|
||||||
|
} else {
|
||||||
|
var titleSpeakingStatusValue = ""
|
||||||
|
for participant in self.speakingParticipantPeers {
|
||||||
|
if !self.visibleParticipants.contains(participant.id) {
|
||||||
|
if !titleSpeakingStatusValue.isEmpty {
|
||||||
|
titleSpeakingStatusValue.append(", ")
|
||||||
|
}
|
||||||
|
titleSpeakingStatusValue.append(participant.compactDisplayTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if titleSpeakingStatusValue.isEmpty {
|
||||||
|
titleView.updateActivityStatus(value: nil, transition: .easeInOut(duration: 0.2))
|
||||||
|
} else {
|
||||||
|
titleView.updateActivityStatus(value: titleSpeakingStatusValue, transition: .easeInOut(duration: 0.2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -585,7 +761,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
if self.members != members {
|
if self.members != members {
|
||||||
var members = members
|
var members = members
|
||||||
|
|
||||||
#if DEBUG && false
|
#if DEBUG && true
|
||||||
if let membersValue = members {
|
if let membersValue = members {
|
||||||
var participants = membersValue.participants
|
var participants = membersValue.participants
|
||||||
for i in 1 ... 20 {
|
for i in 1 ... 20 {
|
||||||
@ -640,25 +816,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if let membersValue = members {
|
if let membersValue = members {
|
||||||
var participants = membersValue.participants
|
let participants = membersValue.participants
|
||||||
participants = participants.sorted(by: { lhs, rhs in
|
|
||||||
guard let lhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == lhs.peer.id }) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let rhsIndex = membersValue.participants.firstIndex(where: { $0.peer.id == rhs.peer.id }) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if let lhsActivityRank = lhs.activityRank, let rhsActivityRank = rhs.activityRank {
|
|
||||||
if lhsActivityRank != rhsActivityRank {
|
|
||||||
return lhsActivityRank < rhsActivityRank
|
|
||||||
}
|
|
||||||
} else if (lhs.activityRank == nil) != (rhs.activityRank == nil) {
|
|
||||||
return lhs.activityRank != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return lhsIndex < rhsIndex
|
|
||||||
})
|
|
||||||
members = PresentationGroupCallMembers(
|
members = PresentationGroupCallMembers(
|
||||||
participants: participants,
|
participants: participants,
|
||||||
speakingParticipants: membersValue.speakingParticipants,
|
speakingParticipants: membersValue.speakingParticipants,
|
||||||
@ -746,6 +904,19 @@ final class VideoChatScreenComponent: Component {
|
|||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var speakingParticipantPeers: [EnginePeer] = []
|
||||||
|
if let members, !members.speakingParticipants.isEmpty {
|
||||||
|
for participant in members.participants {
|
||||||
|
if members.speakingParticipants.contains(participant.peer.id) {
|
||||||
|
speakingParticipantPeers.append(EnginePeer(participant.peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.speakingParticipantPeers != speakingParticipantPeers {
|
||||||
|
self.speakingParticipantPeers = speakingParticipantPeers
|
||||||
|
self.updateTitleSpeakingStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -898,8 +1069,12 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var containerOffset: CGFloat = 0.0
|
var containerOffset: CGFloat = 0.0
|
||||||
if let panGestureState = self.panGestureState {
|
if let verticalPanState = self.verticalPanState {
|
||||||
containerOffset = panGestureState.offsetFraction * availableSize.height
|
if verticalPanState.scrollView != nil {
|
||||||
|
containerOffset = verticalPanState.accumulatedOffset
|
||||||
|
} else {
|
||||||
|
containerOffset = verticalPanState.fraction * availableSize.height
|
||||||
|
}
|
||||||
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
|
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -907,7 +1082,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
guard let self, completed else {
|
guard let self, completed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.panGestureState == nil {
|
if self.verticalPanState == nil {
|
||||||
self.containerView.layer.cornerRadius = 0.0
|
self.containerView.layer.cornerRadius = 0.0
|
||||||
}
|
}
|
||||||
if self.notifyDismissedInteractivelyOnPanGestureApply {
|
if self.notifyDismissedInteractivelyOnPanGestureApply {
|
||||||
@ -1141,11 +1316,19 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonsSideInset: CGFloat = 42.0
|
let buttonsSideInset: CGFloat = 26.0
|
||||||
|
|
||||||
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
|
let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter
|
||||||
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
|
let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth
|
||||||
let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
|
|
||||||
|
let effectiveMaxActionMicrophoneButtonSpacing: CGFloat
|
||||||
|
if areButtonsCollapsed {
|
||||||
|
effectiveMaxActionMicrophoneButtonSpacing = 80.0
|
||||||
|
} else {
|
||||||
|
effectiveMaxActionMicrophoneButtonSpacing = maxActionMicrophoneButtonSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionMicrophoneButtonSpacing = min(effectiveMaxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5))
|
||||||
|
|
||||||
var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
|
var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter))
|
||||||
var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
|
var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter))
|
||||||
@ -1330,6 +1513,12 @@ final class VideoChatScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openInviteMembers()
|
self.openInviteMembers()
|
||||||
|
},
|
||||||
|
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.onVisibleParticipantsUpdated(ids: visibleParticipants)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -1403,8 +1592,8 @@ final class VideoChatScreenComponent: Component {
|
|||||||
micButtonContent = .connecting
|
micButtonContent = .connecting
|
||||||
actionButtonMicrophoneState = .connecting
|
actionButtonMicrophoneState = .connecting
|
||||||
case .connected:
|
case .connected:
|
||||||
if let callState = callState.muteState {
|
if let muteState = callState.muteState {
|
||||||
if callState.canUnmute {
|
if muteState.canUnmute {
|
||||||
if self.isPushToTalkActive {
|
if self.isPushToTalkActive {
|
||||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||||
actionButtonMicrophoneState = .unmuted
|
actionButtonMicrophoneState = .unmuted
|
||||||
@ -1413,7 +1602,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
actionButtonMicrophoneState = .muted
|
actionButtonMicrophoneState = .muted
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
micButtonContent = .raiseHand
|
micButtonContent = .raiseHand(isRaised: callState.raisedHand)
|
||||||
actionButtonMicrophoneState = .raiseHand
|
actionButtonMicrophoneState = .raiseHand
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1741,9 +1930,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
|||||||
}
|
}
|
||||||
self.isAnimatingDismiss = false
|
self.isAnimatingDismiss = false
|
||||||
self.superDismiss()
|
self.superDismiss()
|
||||||
|
completion?()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
self.superDismiss()
|
self.superDismiss()
|
||||||
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -248,8 +248,8 @@ extension VideoChatScreenComponent.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let context = component.call.accountContext
|
let context = component.call.accountContext
|
||||||
environment.controller()?.dismiss(completion: { [weak navigationController] in
|
controller.dismiss(completion: { [weak navigationController] in
|
||||||
Queue.mainQueue().after(0.3) {
|
Queue.mainQueue().after(0.1) {
|
||||||
guard let navigationController else {
|
guard let navigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import ComponentFlow
|
|||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import HierarchyTrackingLayer
|
import HierarchyTrackingLayer
|
||||||
|
import ChatTitleActivityNode
|
||||||
|
|
||||||
final class VideoChatTitleComponent: Component {
|
final class VideoChatTitleComponent: Component {
|
||||||
let title: String
|
let title: String
|
||||||
@ -43,12 +44,17 @@ final class VideoChatTitleComponent: Component {
|
|||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||||
private let title = ComponentView<Empty>()
|
private let title = ComponentView<Empty>()
|
||||||
private var status: ComponentView<Empty>?
|
private let status = ComponentView<Empty>()
|
||||||
private var recordingImageView: UIImageView?
|
private var recordingImageView: UIImageView?
|
||||||
|
|
||||||
|
private var activityStatusNode: ChatTitleActivityNode?
|
||||||
|
|
||||||
private var component: VideoChatTitleComponent?
|
private var component: VideoChatTitleComponent?
|
||||||
private var isUpdating: Bool = false
|
private var isUpdating: Bool = false
|
||||||
|
|
||||||
|
private var currentActivityStatus: String?
|
||||||
|
private var currentSize: CGSize?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||||
|
|
||||||
@ -81,6 +87,64 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateActivityStatus(value: String?, transition: ComponentTransition) {
|
||||||
|
if self.currentActivityStatus == value {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentActivityStatus = value
|
||||||
|
|
||||||
|
guard let currentSize = self.currentSize, let statusView = self.status.view else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let alphaTransition: ComponentTransition
|
||||||
|
if transition.animation.isImmediate {
|
||||||
|
alphaTransition = .immediate
|
||||||
|
} else {
|
||||||
|
alphaTransition = .easeInOut(duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let value {
|
||||||
|
let activityStatusNode: ChatTitleActivityNode
|
||||||
|
if let current = self.activityStatusNode {
|
||||||
|
activityStatusNode = current
|
||||||
|
} else {
|
||||||
|
activityStatusNode = ChatTitleActivityNode()
|
||||||
|
self.activityStatusNode = activityStatusNode
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none)
|
||||||
|
let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center)
|
||||||
|
let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize)
|
||||||
|
|
||||||
|
let activityStatusNodeView = activityStatusNode.view
|
||||||
|
activityStatusNodeView.center = activityStatusFrame.center
|
||||||
|
activityStatusNodeView.bounds = CGRect(origin: CGPoint(), size: activityStatusFrame.size)
|
||||||
|
if activityStatusNodeView.superview == nil {
|
||||||
|
self.addSubview(activityStatusNode.view)
|
||||||
|
ComponentTransition.immediate.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0))
|
||||||
|
activityStatusNodeView.alpha = 0.0
|
||||||
|
}
|
||||||
|
transition.setTransform(view: activityStatusNodeView, transform: CATransform3DIdentity)
|
||||||
|
alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 1.0)
|
||||||
|
|
||||||
|
transition.setTransform(view: statusView, transform: CATransform3DMakeTranslation(0.0, 10.0, 0.0))
|
||||||
|
alphaTransition.setAlpha(view: statusView, alpha: 0.0)
|
||||||
|
} else {
|
||||||
|
if let activityStatusNode = self.activityStatusNode {
|
||||||
|
self.activityStatusNode = nil
|
||||||
|
let activityStatusNodeView = activityStatusNode.view
|
||||||
|
transition.setTransform(view: activityStatusNodeView, transform: CATransform3DMakeTranslation(0.0, -10.0, 0.0))
|
||||||
|
alphaTransition.setAlpha(view: activityStatusNodeView, alpha: 0.0, completion: { [weak activityStatusNodeView] _ in
|
||||||
|
activityStatusNodeView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setTransform(view: statusView, transform: CATransform3DIdentity)
|
||||||
|
alphaTransition.setAlpha(view: statusView, alpha: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: VideoChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -100,19 +164,12 @@ final class VideoChatTitleComponent: Component {
|
|||||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
let status: ComponentView<Empty>
|
|
||||||
if let current = self.status {
|
|
||||||
status = current
|
|
||||||
} else {
|
|
||||||
status = ComponentView()
|
|
||||||
self.status = status
|
|
||||||
}
|
|
||||||
let statusComponent: AnyComponent<Empty>
|
let statusComponent: AnyComponent<Empty>
|
||||||
statusComponent = AnyComponent(MultilineTextComponent(
|
statusComponent = AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5)))
|
text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5)))
|
||||||
))
|
))
|
||||||
|
|
||||||
let statusSize = status.update(
|
let statusSize = self.status.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: statusComponent,
|
component: statusComponent,
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -131,7 +188,7 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
|
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
|
||||||
if let statusView = status.view {
|
if let statusView = self.status.view {
|
||||||
if statusView.superview == nil {
|
if statusView.superview == nil {
|
||||||
self.addSubview(statusView)
|
self.addSubview(statusView)
|
||||||
}
|
}
|
||||||
@ -165,6 +222,8 @@ final class VideoChatTitleComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.currentSize = size
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7098,15 +7098,14 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
|
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
|
||||||
/*var useV2 = true
|
var useV2 = true
|
||||||
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
|
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
|
||||||
useV2 = false
|
useV2 = false
|
||||||
}
|
}
|
||||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] {
|
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] {
|
||||||
useV2 = false
|
useV2 = false
|
||||||
}
|
}
|
||||||
return useV2*/
|
return useV2
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
|
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
import Foundation
|
||||||
|
import Postbox
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramApi
|
||||||
|
import MtProtoKit
|
||||||
|
|
||||||
|
public enum ReportContentResult {
|
||||||
|
public struct Option: Equatable {
|
||||||
|
public let text: String
|
||||||
|
public let option: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
case options(title: String, options: [Option])
|
||||||
|
case addComment(optional: Bool, option: Data)
|
||||||
|
case reported
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ReportContentError {
|
||||||
|
case generic
|
||||||
|
case messageIdRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ReportContentSubject: Equatable {
|
||||||
|
case peer(EnginePeer.Id)
|
||||||
|
case messages([EngineMessage.Id])
|
||||||
|
case stories(EnginePeer.Id, [Int32])
|
||||||
|
|
||||||
|
var peerId: EnginePeer.Id {
|
||||||
|
switch self {
|
||||||
|
case let .peer(peerId):
|
||||||
|
return peerId
|
||||||
|
case let .messages(messageIds):
|
||||||
|
return messageIds.first!.peerId
|
||||||
|
case let .stories(peerId, _):
|
||||||
|
return peerId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal<ReportContentResult, ReportContentError> {
|
||||||
|
return account.postbox.transaction { transaction -> Signal<ReportContentResult, ReportContentError> in
|
||||||
|
guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else {
|
||||||
|
return .fail(.generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
let request: Signal<Api.ReportResult, MTRpcError>
|
||||||
|
if case let .stories(_, ids) = subject {
|
||||||
|
request = account.network.request(Api.functions.stories.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
|
||||||
|
} else {
|
||||||
|
var ids: [Int32] = []
|
||||||
|
if case let .messages(messageIds) = subject {
|
||||||
|
ids = messageIds.map { $0.id }
|
||||||
|
}
|
||||||
|
request = account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids, option: Buffer(data: option), message: message ?? ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
|> mapError { error -> ReportContentError in
|
||||||
|
if error.errorDescription == "MESSAGE_ID_REQUIRED" {
|
||||||
|
return .messageIdRequired
|
||||||
|
}
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> map { result -> ReportContentResult in
|
||||||
|
switch result {
|
||||||
|
case let .reportResultChooseOption(title, options):
|
||||||
|
return .options(title: title, options: options.map {
|
||||||
|
switch $0 {
|
||||||
|
case let .messageReportOption(text, option):
|
||||||
|
return ReportContentResult.Option(text: text, option: option.makeData())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case let .reportResultAddComment(flags, option):
|
||||||
|
return .addComment(optional: (flags & (1 << 0)) != 0, option: option.makeData())
|
||||||
|
case .reportResultReported:
|
||||||
|
return .reported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> castError(ReportContentError.self)
|
||||||
|
|> switchToLatest
|
||||||
|
}
|
||||||
@ -1463,6 +1463,10 @@ public extension TelegramEngine {
|
|||||||
return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option)
|
return _internal_reportAdMessage(account: self.account, peerId: peerId, opaqueId: opaqueId, option: option)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func reportContent(subject: ReportContentSubject, option: Data?, message: String?) -> Signal<ReportContentResult, ReportContentError> {
|
||||||
|
return _internal_reportContent(account: self.account, subject: subject, option: option, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
|
public func updateExtendedMedia(messageIds: [EngineMessage.Id]) -> Signal<Never, NoError> {
|
||||||
return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds)
|
return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -468,7 +468,7 @@ private final class StarsContextImpl {
|
|||||||
}
|
}
|
||||||
var transactions = state.transactions
|
var transactions = state.transactions
|
||||||
if addTransaction {
|
if addTransaction {
|
||||||
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil), at: 0)
|
transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil), at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading))
|
self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading))
|
||||||
@ -490,7 +490,7 @@ private final class StarsContextImpl {
|
|||||||
private extension StarsContext.State.Transaction {
|
private extension StarsContext.State.Transaction {
|
||||||
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
|
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
|
||||||
switch apiTransaction {
|
switch apiTransaction {
|
||||||
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId):
|
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift):
|
||||||
let parsedPeer: StarsContext.State.Transaction.Peer
|
let parsedPeer: StarsContext.State.Transaction.Peer
|
||||||
var paidMessageId: MessageId?
|
var paidMessageId: MessageId?
|
||||||
var giveawayMessageId: MessageId?
|
var giveawayMessageId: MessageId?
|
||||||
@ -544,7 +544,7 @@ private extension StarsContext.State.Transaction {
|
|||||||
|
|
||||||
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
|
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
|
||||||
let _ = subscriptionPeriod
|
let _ = subscriptionPeriod
|
||||||
self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod)
|
self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -613,6 +613,7 @@ public final class StarsContext {
|
|||||||
public let giveawayMessageId: MessageId?
|
public let giveawayMessageId: MessageId?
|
||||||
public let media: [Media]
|
public let media: [Media]
|
||||||
public let subscriptionPeriod: Int32?
|
public let subscriptionPeriod: Int32?
|
||||||
|
public let starGift: StarGift?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
@ -628,7 +629,8 @@ public final class StarsContext {
|
|||||||
paidMessageId: MessageId?,
|
paidMessageId: MessageId?,
|
||||||
giveawayMessageId: MessageId?,
|
giveawayMessageId: MessageId?,
|
||||||
media: [Media],
|
media: [Media],
|
||||||
subscriptionPeriod: Int32?
|
subscriptionPeriod: Int32?,
|
||||||
|
starGift: StarGift?
|
||||||
) {
|
) {
|
||||||
self.flags = flags
|
self.flags = flags
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -644,6 +646,7 @@ public final class StarsContext {
|
|||||||
self.giveawayMessageId = giveawayMessageId
|
self.giveawayMessageId = giveawayMessageId
|
||||||
self.media = media
|
self.media = media
|
||||||
self.subscriptionPeriod = subscriptionPeriod
|
self.subscriptionPeriod = subscriptionPeriod
|
||||||
|
self.starGift = starGift
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
|
public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
|
||||||
@ -689,6 +692,9 @@ public final class StarsContext {
|
|||||||
if lhs.subscriptionPeriod != rhs.subscriptionPeriod {
|
if lhs.subscriptionPeriod != rhs.subscriptionPeriod {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.starGift != rhs.starGift {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,42 +148,44 @@ func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal<Void, NoError> {
|
func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal<Void, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
return .complete()
|
||||||
let groupedIds = messagesIdsGroupedByPeerId(messageIds)
|
// return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
let signals = groupedIds.values.compactMap { ids -> Signal<Void, NoError>? in
|
// let groupedIds = messagesIdsGroupedByPeerId(messageIds)
|
||||||
guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
|
// let signals = groupedIds.values.compactMap { ids -> Signal<Void, NoError>? in
|
||||||
return nil
|
// guard let peerId = ids.first?.peerId, let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
|
||||||
}
|
// return nil
|
||||||
return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message))
|
// }
|
||||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
// return account.network.request(Api.functions.messages.report(peer: inputPeer, id: ids.map { $0.id }, reason: reason.apiReason, message: message))
|
||||||
return .single(.boolFalse)
|
// |> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||||
}
|
// return .single(.boolFalse)
|
||||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
// }
|
||||||
return .complete()
|
// |> mapToSignal { _ -> Signal<Void, NoError> in
|
||||||
}
|
// return .complete()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
return combineLatest(signals)
|
//
|
||||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
// return combineLatest(signals)
|
||||||
return .complete()
|
// |> mapToSignal { _ -> Signal<Void, NoError> in
|
||||||
}
|
// return .complete()
|
||||||
} |> switchToLatest
|
// }
|
||||||
|
// } |> switchToLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal<Void, NoError> {
|
func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal<Void, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
return .complete()
|
||||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
// return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message))
|
// if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
// return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message))
|
||||||
return .single(.boolFalse)
|
// |> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||||
}
|
// return .single(.boolFalse)
|
||||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
// }
|
||||||
return .complete()
|
// |> mapToSignal { _ -> Signal<Void, NoError> in
|
||||||
}
|
// return .complete()
|
||||||
} else {
|
// }
|
||||||
return .complete()
|
// } else {
|
||||||
}
|
// return .complete()
|
||||||
} |> switchToLatest
|
// }
|
||||||
|
// } |> switchToLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal<Never, NoError> {
|
func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal<Never, NoError> {
|
||||||
|
|||||||
@ -461,6 +461,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||||
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
|
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
|
||||||
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
||||||
|
"//submodules/TelegramUI/Components/ContentReportScreen",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||||
"//build-system:ios_sim_arm64": [],
|
"//build-system:ios_sim_arm64": [],
|
||||||
|
|||||||
@ -396,6 +396,7 @@ private final class SheetContent: CombinedComponent {
|
|||||||
let navigation = navigation.update(
|
let navigation = navigation.update(
|
||||||
component: NavigationStackComponent(
|
component: NavigationStackComponent(
|
||||||
items: items,
|
items: items,
|
||||||
|
clipContent: false,
|
||||||
requestPop: { [weak state] in
|
requestPop: { [weak state] in
|
||||||
state?.pushedOptions.removeLast()
|
state?.pushedOptions.removeLast()
|
||||||
update(.spring(duration: 0.45))
|
update(.spring(duration: 0.45))
|
||||||
|
|||||||
@ -324,13 +324,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
|||||||
self.closeAction?()
|
self.closeAction?()
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
if !"".isEmpty {
|
||||||
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
if #available(iOS 16.0, *) {
|
||||||
pipVideoCallViewController.view.addSubview(self.pipView)
|
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
||||||
self.pipView.frame = pipVideoCallViewController.view.bounds
|
pipVideoCallViewController.view.addSubview(self.pipView)
|
||||||
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
self.pipView.frame = pipVideoCallViewController.view.bounds
|
||||||
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
self.pipVideoCallViewController = pipVideoCallViewController
|
self.pipView.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
self.pipVideoCallViewController = pipVideoCallViewController
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blurFilter = makeBlurFilter() {
|
if let blurFilter = makeBlurFilter() {
|
||||||
|
|||||||
@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
let loopVideo = updatedVideoFile.isAnimated
|
let loopVideo = updatedVideoFile.isAnimated
|
||||||
|
|
||||||
let videoContent: UniversalVideoContent
|
let videoContent: UniversalVideoContent
|
||||||
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile), context.sharedContext.immediateExperimentalUISettings.dynamicStreaming {
|
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile) {
|
||||||
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
|
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
|
||||||
} else {
|
} else {
|
||||||
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
|
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
|
||||||
|
|||||||
41
submodules/TelegramUI/Components/ContentReportScreen/BUILD
Normal file
41
submodules/TelegramUI/Components/ContentReportScreen/BUILD
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "ContentReportScreen",
|
||||||
|
module_name = "ContentReportScreen",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/Postbox",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/Components/ViewControllerComponent",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/Components/MultilineTextComponent",
|
||||||
|
"//submodules/Components/BalancedTextComponent",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/ItemListUI",
|
||||||
|
"//submodules/TelegramStringFormatting",
|
||||||
|
"//submodules/PresentationDataUtils",
|
||||||
|
"//submodules/Components/SheetComponent",
|
||||||
|
"//submodules/UndoUI",
|
||||||
|
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||||
|
"//submodules/TelegramUI/Components/NavigationStackComponent",
|
||||||
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -0,0 +1,726 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import Markdown
|
||||||
|
import TextFormat
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ViewControllerComponent
|
||||||
|
import SheetComponent
|
||||||
|
import BalancedTextComponent
|
||||||
|
import MultilineTextComponent
|
||||||
|
import ListSectionComponent
|
||||||
|
import ListActionItemComponent
|
||||||
|
import NavigationStackComponent
|
||||||
|
import ItemListUI
|
||||||
|
import UndoUI
|
||||||
|
import AccountContext
|
||||||
|
import LottieComponent
|
||||||
|
import TextFieldComponent
|
||||||
|
import ListMultilineTextFieldItemComponent
|
||||||
|
import ButtonComponent
|
||||||
|
|
||||||
|
private enum ReportResult {
|
||||||
|
case reported
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SheetPageContent: CombinedComponent {
|
||||||
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
enum Content: Equatable {
|
||||||
|
struct Item: Equatable {
|
||||||
|
let title: String
|
||||||
|
let option: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
case options(items: [Item])
|
||||||
|
case comment(isOptional: Bool, option: Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let isFirst: Bool
|
||||||
|
let title: String?
|
||||||
|
let subtitle: String
|
||||||
|
let content: Content
|
||||||
|
let action: (Content.Item, String?) -> Void
|
||||||
|
let pop: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
isFirst: Bool,
|
||||||
|
title: String?,
|
||||||
|
subtitle: String,
|
||||||
|
content: Content,
|
||||||
|
action: @escaping (Content.Item, String?) -> Void,
|
||||||
|
pop: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.isFirst = isFirst
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.content = content
|
||||||
|
self.action = action
|
||||||
|
self.pop = pop
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: SheetPageContent, rhs: SheetPageContent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subtitle != rhs.subtitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.content != rhs.content {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
var backArrowImage: (UIImage, PresentationTheme)?
|
||||||
|
|
||||||
|
let playOnce = ActionSlot<Void>()
|
||||||
|
private var didPlayAnimation = false
|
||||||
|
|
||||||
|
let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||||
|
|
||||||
|
func playAnimationIfNeeded() {
|
||||||
|
guard !self.didPlayAnimation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.didPlayAnimation = true
|
||||||
|
self.playOnce.invoke(Void())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State()
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let background = Child(RoundedRectangle.self)
|
||||||
|
let back = Child(Button.self)
|
||||||
|
let title = Child(Text.self)
|
||||||
|
let animation = Child(LottieComponent.self)
|
||||||
|
let section = Child(ListSectionComponent.self)
|
||||||
|
let button = Child(ButtonComponent.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let environment = context.environment[EnvironmentType.self]
|
||||||
|
let component = context.component
|
||||||
|
let state = context.state
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let theme = environment.theme
|
||||||
|
let strings = environment.strings
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||||
|
|
||||||
|
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||||
|
|
||||||
|
let background = background.update(
|
||||||
|
component: RoundedRectangle(color: theme.list.modalBlocksBackgroundColor, cornerRadius: 8.0),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(background
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
let backArrowImage: UIImage
|
||||||
|
if let (cached, cachedTheme) = state.backArrowImage, cachedTheme === theme {
|
||||||
|
backArrowImage = cached
|
||||||
|
} else {
|
||||||
|
backArrowImage = NavigationBarTheme.generateBackArrowImage(color: theme.list.itemAccentColor)!
|
||||||
|
state.backArrowImage = (backArrowImage, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
let backContents: AnyComponent<Empty>
|
||||||
|
if component.isFirst {
|
||||||
|
backContents = AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.list.itemAccentColor))
|
||||||
|
} else {
|
||||||
|
backContents = AnyComponent(
|
||||||
|
HStack([
|
||||||
|
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(Image(image: backArrowImage, contentMode: .center))),
|
||||||
|
AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: strings.Common_Back, font: Font.regular(17.0), color: theme.list.itemAccentColor)))
|
||||||
|
], spacing: 6.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let back = back.update(
|
||||||
|
component: Button(
|
||||||
|
content: backContents,
|
||||||
|
action: {
|
||||||
|
component.pop()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(back
|
||||||
|
.position(CGPoint(x: sideInset + back.size.width / 2.0 - (component.title != nil ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0
|
||||||
|
|
||||||
|
let titleString: String
|
||||||
|
if let title = component.title {
|
||||||
|
titleString = title
|
||||||
|
} else {
|
||||||
|
titleString = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = title.update(
|
||||||
|
component: Text(text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor),
|
||||||
|
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(title
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||||
|
)
|
||||||
|
contentSize.height += title.size.height
|
||||||
|
contentSize.height += 24.0
|
||||||
|
|
||||||
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||||
|
var footer: AnyComponent<Empty>?
|
||||||
|
|
||||||
|
switch component.content {
|
||||||
|
case let .options(options):
|
||||||
|
for item in options {
|
||||||
|
items.append(AnyComponentWithIdentity(id: item.title, component: AnyComponent(ListActionItemComponent(
|
||||||
|
theme: theme,
|
||||||
|
title: AnyComponent(VStack([
|
||||||
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(
|
||||||
|
string: item.title,
|
||||||
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
|
)),
|
||||||
|
maximumNumberOfLines: 1
|
||||||
|
))),
|
||||||
|
], alignment: .left, spacing: 2.0)),
|
||||||
|
accessory: .arrow,
|
||||||
|
action: { _ in
|
||||||
|
component.action(item, nil)
|
||||||
|
}
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
case let .comment(isOptional, _):
|
||||||
|
contentSize.height -= 11.0
|
||||||
|
|
||||||
|
let animationHeight: CGFloat = 120.0
|
||||||
|
let animation = animation.update(
|
||||||
|
component: LottieComponent(
|
||||||
|
content: LottieComponent.AppBundleContent(name: "Cop"),
|
||||||
|
startingPosition: .begin,
|
||||||
|
playOnce: state.playOnce
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
availableSize: CGSize(width: animationHeight, height: animationHeight),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(animation
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0))
|
||||||
|
)
|
||||||
|
contentSize.height += animation.size.height
|
||||||
|
contentSize.height += 18.0
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(id: items.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||||
|
externalState: state.textInputState,
|
||||||
|
context: component.context,
|
||||||
|
theme: theme,
|
||||||
|
strings: strings,
|
||||||
|
initialText: "",
|
||||||
|
resetText: nil,
|
||||||
|
placeholder: isOptional ? "Add Comment (Optional)" : "Add Comment",
|
||||||
|
autocapitalizationType: .none,
|
||||||
|
autocorrectionType: .no,
|
||||||
|
returnKeyType: .done,
|
||||||
|
characterLimit: 140,
|
||||||
|
displayCharacterLimit: true,
|
||||||
|
emptyLineHandling: .notAllowed,
|
||||||
|
updated: { [weak state] _ in
|
||||||
|
state?.updated()
|
||||||
|
},
|
||||||
|
returnKeyAction: {
|
||||||
|
// guard let self else {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
||||||
|
// titleView.endEditing(true)
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
textUpdateTransition: .spring(duration: 0.4),
|
||||||
|
tag: nil
|
||||||
|
)))
|
||||||
|
)
|
||||||
|
|
||||||
|
footer = AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(
|
||||||
|
NSAttributedString(string: "Please help us by telling what is wrong with the message you have selected.", font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor)
|
||||||
|
),
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
let section = section.update(
|
||||||
|
component: ListSectionComponent(
|
||||||
|
theme: theme,
|
||||||
|
header: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(
|
||||||
|
string: component.subtitle.uppercased(),
|
||||||
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
|
textColor: theme.list.freeTextColor
|
||||||
|
)),
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
)),
|
||||||
|
footer: footer,
|
||||||
|
items: items
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(section
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0))
|
||||||
|
)
|
||||||
|
contentSize.height += section.size.height
|
||||||
|
contentSize.height += 54.0
|
||||||
|
|
||||||
|
if case let .comment(isOptional, option) = component.content {
|
||||||
|
contentSize.height -= 16.0
|
||||||
|
|
||||||
|
let action = component.action
|
||||||
|
let button = button.update(
|
||||||
|
component: ButtonComponent(
|
||||||
|
background: ButtonComponent.Background(
|
||||||
|
color: theme.list.itemCheckColors.fillColor,
|
||||||
|
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||||
|
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||||
|
),
|
||||||
|
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: "Send Report", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor))),
|
||||||
|
isEnabled: isOptional || state.textInputState.hasText,
|
||||||
|
allowActionWhenDisabled: false,
|
||||||
|
displaysProgress: false,
|
||||||
|
action: {
|
||||||
|
action(SheetPageContent.Content.Item(title: "", option: option), state.textInputState.text.string)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(button
|
||||||
|
.clipsToBounds(true)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
|
||||||
|
)
|
||||||
|
contentSize.height += button.size.height
|
||||||
|
contentSize.height += 16.0
|
||||||
|
|
||||||
|
if environment.inputHeight.isZero && environment.safeInsets.bottom > 0.0 {
|
||||||
|
contentSize.height += environment.safeInsets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentSize.height += environment.inputHeight
|
||||||
|
|
||||||
|
state.playAnimationIfNeeded()
|
||||||
|
|
||||||
|
return contentSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SheetContent: CombinedComponent {
|
||||||
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let subject: ReportContentSubject
|
||||||
|
let title: String
|
||||||
|
let options: [ReportContentResult.Option]
|
||||||
|
let pts: Int
|
||||||
|
let openMore: () -> Void
|
||||||
|
let complete: (ReportResult) -> Void
|
||||||
|
let dismiss: () -> Void
|
||||||
|
let update: (ComponentTransition) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
subject: ReportContentSubject,
|
||||||
|
title: String,
|
||||||
|
options: [ReportContentResult.Option],
|
||||||
|
pts: Int,
|
||||||
|
openMore: @escaping () -> Void,
|
||||||
|
complete: @escaping (ReportResult) -> Void,
|
||||||
|
dismiss: @escaping () -> Void,
|
||||||
|
update: @escaping (ComponentTransition) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.subject = subject
|
||||||
|
self.title = title
|
||||||
|
self.options = options
|
||||||
|
self.pts = pts
|
||||||
|
self.openMore = openMore
|
||||||
|
self.complete = complete
|
||||||
|
self.dismiss = dismiss
|
||||||
|
self.update = update
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subject != rhs.subject {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.options != rhs.options {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.pts != rhs.pts {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
var pushedOptions: [(title: String, subtitle: String, content: SheetPageContent.Content)] = []
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State()
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let navigation = Child(NavigationStackComponent<EnvironmentType>.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let environment = context.environment[EnvironmentType.self]
|
||||||
|
let component = context.component
|
||||||
|
let state = context.state
|
||||||
|
let update = component.update
|
||||||
|
|
||||||
|
let accountContext = component.context
|
||||||
|
let subject = component.subject
|
||||||
|
let complete = component.complete
|
||||||
|
let action: (SheetPageContent.Content.Item, String?) -> Void = { [weak state] item, message in
|
||||||
|
guard let state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.disposable.set(
|
||||||
|
(accountContext.engine.messages.reportContent(subject: subject, option: item.option, message: message)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak state] result in
|
||||||
|
switch result {
|
||||||
|
case let .options(title, options):
|
||||||
|
state?.pushedOptions.append((item.title, title, .options(items: options.map { SheetPageContent.Content.Item(title: $0.text, option: $0.option) })))
|
||||||
|
state?.updated(transition: .spring(duration: 0.45))
|
||||||
|
case let .addComment(isOptional, option):
|
||||||
|
state?.pushedOptions.append((item.title, "", .comment(isOptional: isOptional, option: option)))
|
||||||
|
state?.updated(transition: .spring(duration: 0.45))
|
||||||
|
case .reported:
|
||||||
|
complete(.reported)
|
||||||
|
}
|
||||||
|
}, error: { error in
|
||||||
|
// if case .premiumRequired = error {
|
||||||
|
// complete(.premiumRequired)
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainTitle: String
|
||||||
|
switch component.subject {
|
||||||
|
case .peer:
|
||||||
|
mainTitle = "Report Peer"
|
||||||
|
case .messages:
|
||||||
|
mainTitle = "Report Message"
|
||||||
|
case .stories:
|
||||||
|
mainTitle = "Report Story"
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [AnyComponentWithIdentity<EnvironmentType>] = []
|
||||||
|
items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent(
|
||||||
|
SheetPageContent(
|
||||||
|
context: component.context,
|
||||||
|
isFirst: true,
|
||||||
|
title: mainTitle,
|
||||||
|
subtitle: component.title,
|
||||||
|
content: .options(items: component.options.map {
|
||||||
|
SheetPageContent.Content.Item(title: $0.text, option: $0.option)
|
||||||
|
}),
|
||||||
|
action: { item, message in
|
||||||
|
action(item, message)
|
||||||
|
},
|
||||||
|
pop: {
|
||||||
|
component.dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)))
|
||||||
|
for pushedOption in state.pushedOptions {
|
||||||
|
items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent(
|
||||||
|
SheetPageContent(
|
||||||
|
context: component.context,
|
||||||
|
isFirst: false,
|
||||||
|
title: pushedOption.title,
|
||||||
|
subtitle: pushedOption.subtitle,
|
||||||
|
content: pushedOption.content,
|
||||||
|
action: { item, message in
|
||||||
|
action(item, message)
|
||||||
|
},
|
||||||
|
pop: { [weak state] in
|
||||||
|
state?.pushedOptions.removeLast()
|
||||||
|
update(.spring(duration: 0.45))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentSize = CGSize(width: context.availableSize.width, height: 0.0)
|
||||||
|
let navigation = navigation.update(
|
||||||
|
component: NavigationStackComponent(
|
||||||
|
items: items,
|
||||||
|
clipContent: false,
|
||||||
|
requestPop: { [weak state] in
|
||||||
|
state?.pushedOptions.removeLast()
|
||||||
|
update(.spring(duration: 0.45))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
environment: { environment },
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(navigation
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: navigation.size.height / 2.0))
|
||||||
|
.clipsToBounds(true)
|
||||||
|
.cornerRadius(8.0)
|
||||||
|
)
|
||||||
|
contentSize.height += navigation.size.height
|
||||||
|
|
||||||
|
return contentSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SheetContainerComponent: CombinedComponent {
|
||||||
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let subject: ReportContentSubject
|
||||||
|
let title: String
|
||||||
|
let options: [ReportContentResult.Option]
|
||||||
|
let openMore: () -> Void
|
||||||
|
let complete: (ReportResult) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
subject: ReportContentSubject,
|
||||||
|
title: String,
|
||||||
|
options: [ReportContentResult.Option],
|
||||||
|
openMore: @escaping () -> Void,
|
||||||
|
complete: @escaping (ReportResult) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.subject = subject
|
||||||
|
self.title = title
|
||||||
|
self.options = options
|
||||||
|
self.openMore = openMore
|
||||||
|
self.complete = complete
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subject != rhs.subject {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.options != rhs.options {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
var pts: Int = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State()
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let sheet = Child(SheetComponent<EnvironmentType>.self)
|
||||||
|
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||||
|
|
||||||
|
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let environment = context.environment[EnvironmentType.self]
|
||||||
|
let state = context.state
|
||||||
|
let controller = environment.controller
|
||||||
|
|
||||||
|
let sheet = sheet.update(
|
||||||
|
component: SheetComponent<EnvironmentType>(
|
||||||
|
content: AnyComponent<EnvironmentType>(SheetContent(
|
||||||
|
context: context.component.context,
|
||||||
|
subject: context.component.subject,
|
||||||
|
title: context.component.title,
|
||||||
|
options: context.component.options,
|
||||||
|
pts: state.pts,
|
||||||
|
openMore: context.component.openMore,
|
||||||
|
complete: context.component.complete,
|
||||||
|
dismiss: {
|
||||||
|
animateOut.invoke(Action { _ in
|
||||||
|
if let controller = controller() {
|
||||||
|
controller.dismiss(completion: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
update: { [weak state] transition in
|
||||||
|
state?.pts += 1
|
||||||
|
state?.updated(transition: transition)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
|
||||||
|
followContentSizeChanges: true,
|
||||||
|
externalState: sheetExternalState,
|
||||||
|
animateOut: animateOut
|
||||||
|
),
|
||||||
|
environment: {
|
||||||
|
environment
|
||||||
|
SheetComponentEnvironment(
|
||||||
|
isDisplaying: environment.value.isVisible,
|
||||||
|
isCentered: environment.metrics.widthClass == .regular,
|
||||||
|
hasInputHeight: !environment.inputHeight.isZero,
|
||||||
|
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||||
|
dismiss: { animated in
|
||||||
|
if animated {
|
||||||
|
animateOut.invoke(Action { _ in
|
||||||
|
if let controller = controller() {
|
||||||
|
controller.dismiss(completion: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if let controller = controller() {
|
||||||
|
controller.dismiss(completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(sheet
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
|
||||||
|
let layout = ContainerViewLayout(
|
||||||
|
size: context.availableSize,
|
||||||
|
metrics: environment.metrics,
|
||||||
|
deviceMetrics: environment.deviceMetrics,
|
||||||
|
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
|
||||||
|
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
|
||||||
|
additionalInsets: .zero,
|
||||||
|
statusBarHeight: environment.statusBarHeight,
|
||||||
|
inputHeight: nil,
|
||||||
|
inputHeightIsInteractivellyChanging: false,
|
||||||
|
inVoiceOver: false
|
||||||
|
)
|
||||||
|
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public final class ContentReportScreen: ViewControllerComponentContainer {
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
|
public init(
|
||||||
|
context: AccountContext,
|
||||||
|
subject: ReportContentSubject,
|
||||||
|
title: String,
|
||||||
|
options: [ReportContentResult.Option],
|
||||||
|
forceDark: Bool = false,
|
||||||
|
completed: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
var completeImpl: ((ReportResult) -> Void)?
|
||||||
|
super.init(
|
||||||
|
context: context,
|
||||||
|
component: SheetContainerComponent(
|
||||||
|
context: context,
|
||||||
|
subject: subject,
|
||||||
|
title: title,
|
||||||
|
options: options,
|
||||||
|
openMore: {},
|
||||||
|
complete: { hidden in
|
||||||
|
completeImpl?(hidden)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
navigationBarAppearance: .none,
|
||||||
|
statusBarStyle: .ignore,
|
||||||
|
theme: forceDark ? .dark : .default
|
||||||
|
)
|
||||||
|
|
||||||
|
self.navigationPresentation = .flatModal
|
||||||
|
|
||||||
|
completeImpl = { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let navigationController = self.navigationController
|
||||||
|
self.dismissAnimated()
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .reported:
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
Queue.mainQueue().after(0.4, {
|
||||||
|
(navigationController?.viewControllers.last as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return true }), in: .current)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.view.disablesInteractiveModalDismiss = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func dismissAnimated() {
|
||||||
|
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||||
|
view.dismissAnimated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "GiftAnimationComponent",
|
||||||
|
module_name = "GiftAnimationComponent",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/Postbox",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/TelegramStringFormatting",
|
||||||
|
"//submodules/PresentationDataUtils",
|
||||||
|
"//submodules/TextFormat",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AppBundle
|
||||||
|
import AccountContext
|
||||||
|
import EmojiTextAttachmentView
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
|
public final class GiftAnimationComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let file: TelegramMediaFile?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
file: TelegramMediaFile?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.file = file
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.file != rhs.file {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class View: UIView {
|
||||||
|
private var component: GiftAnimationComponent?
|
||||||
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
|
private var animationLayer: InlineStickerItemLayer?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
self.componentState = state
|
||||||
|
|
||||||
|
let emoji = ChatTextInputTextCustomEmojiAttribute(
|
||||||
|
interactivelySelectedFromPackId: nil,
|
||||||
|
fileId: component.file?.fileId.id ?? 0,
|
||||||
|
file: component.file
|
||||||
|
)
|
||||||
|
|
||||||
|
let iconSize = availableSize
|
||||||
|
if self.animationLayer == nil {
|
||||||
|
let animationLayer = InlineStickerItemLayer(
|
||||||
|
context: .account(component.context),
|
||||||
|
userLocation: .other,
|
||||||
|
attemptSynchronousLoad: false,
|
||||||
|
emoji: emoji,
|
||||||
|
file: component.file,
|
||||||
|
cache: component.context.animationCache,
|
||||||
|
renderer: component.context.animationRenderer,
|
||||||
|
unique: true,
|
||||||
|
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
||||||
|
pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2),
|
||||||
|
loopCount: 1
|
||||||
|
)
|
||||||
|
animationLayer.isVisibleForAnimations = true
|
||||||
|
self.animationLayer = animationLayer
|
||||||
|
self.layer.addSublayer(animationLayer)
|
||||||
|
}
|
||||||
|
if let animationLayer = self.animationLayer {
|
||||||
|
transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ swift_library(
|
|||||||
"//submodules/Components/SolidRoundedButtonComponent",
|
"//submodules/Components/SolidRoundedButtonComponent",
|
||||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||||
|
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
|
||||||
"//submodules/UndoUI",
|
"//submodules/UndoUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import TelegramStringFormatting
|
|||||||
import StarsAvatarComponent
|
import StarsAvatarComponent
|
||||||
import EmojiTextAttachmentView
|
import EmojiTextAttachmentView
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
import GiftAnimationComponent
|
||||||
|
|
||||||
private final class GiftViewSheetContent: CombinedComponent {
|
private final class GiftViewSheetContent: CombinedComponent {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -695,14 +696,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
|||||||
case message(EngineMessage)
|
case message(EngineMessage)
|
||||||
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
|
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
|
||||||
|
|
||||||
var arguments: (peerId: EnginePeer.Id, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
|
var arguments: (peerId: EnginePeer.Id, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .message(message):
|
case let .message(message):
|
||||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
|
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
|
||||||
return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
|
return (message.id.peerId, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
|
||||||
}
|
}
|
||||||
case let .profileGift(peerId, gift):
|
case let .profileGift(peerId, gift):
|
||||||
return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
|
return (peerId, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -789,9 +790,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
|||||||
if let lastController = navigationController.viewControllers.last as? ViewController {
|
if let lastController = navigationController.viewControllers.last as? ViewController {
|
||||||
let resultController = UndoOverlayController(
|
let resultController = UndoOverlayController(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in your profile." : "The gift is no longer displayed in your profile.", undoText: nil, customAction: nil),
|
content: .sticker(context: context, file: arguments.gift.file, loop: false, title: added ? "Gift Saved to Profile" : "Gift Removed from Profile", text: added ? "The gift is now displayed in [your profile]()." : "The gift is no longer displayed in [your profile]().", undoText: nil, customAction: nil),
|
||||||
elevatedLayout: lastController is ChatController,
|
elevatedLayout: lastController is ChatController,
|
||||||
action: { _ in return true}
|
action: { action in
|
||||||
|
if case .info = action {
|
||||||
|
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
lastController.present(resultController, in: .window(.root))
|
lastController.present(resultController, in: .window(.root))
|
||||||
}
|
}
|
||||||
@ -800,13 +806,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
convertToStarsImpl = { [weak self] in
|
convertToStarsImpl = { [weak self] in
|
||||||
guard let self, case let .message(message) = subject, let arguments = subject.arguments, let messageId = arguments.messageId, let navigationController = self.navigationController as? NavigationController else {
|
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId, let fromPeerName = arguments.fromPeerName, let navigationController = self.navigationController as? NavigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let controller = textAlertController(
|
let controller = textAlertController(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
title: "Convert Gift to Stars",
|
title: "Convert Gift to Stars",
|
||||||
text: "Do you want to convert this gift from **\(message.author?.compactDisplayTitle ?? "")** to **\(arguments.convertStars) Stars**?\n\nThis action cannot be undone.",
|
text: "Do you want to convert this gift from **\(fromPeerName)** to **\(arguments.convertStars) Stars**?\n\nThis action cannot be undone.",
|
||||||
actions: [
|
actions: [
|
||||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
|
||||||
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
|
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
|
||||||
@ -1253,91 +1259,3 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
|
|||||||
context.strokePath()
|
context.strokePath()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class GiftAnimationComponent: Component {
|
|
||||||
let context: AccountContext
|
|
||||||
let theme: PresentationTheme
|
|
||||||
let file: TelegramMediaFile?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
context: AccountContext,
|
|
||||||
theme: PresentationTheme,
|
|
||||||
file: TelegramMediaFile?
|
|
||||||
) {
|
|
||||||
self.context = context
|
|
||||||
self.theme = theme
|
|
||||||
self.file = file
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool {
|
|
||||||
if lhs.context !== rhs.context {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.theme !== rhs.theme {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.file != rhs.file {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class View: UIView {
|
|
||||||
private var component: GiftAnimationComponent?
|
|
||||||
private weak var componentState: EmptyComponentState?
|
|
||||||
|
|
||||||
private var animationLayer: InlineStickerItemLayer?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
||||||
self.component = component
|
|
||||||
self.componentState = state
|
|
||||||
|
|
||||||
let emoji = ChatTextInputTextCustomEmojiAttribute(
|
|
||||||
interactivelySelectedFromPackId: nil,
|
|
||||||
fileId: component.file?.fileId.id ?? 0,
|
|
||||||
file: component.file
|
|
||||||
)
|
|
||||||
|
|
||||||
let iconSize = availableSize
|
|
||||||
if self.animationLayer == nil {
|
|
||||||
let animationLayer = InlineStickerItemLayer(
|
|
||||||
context: .account(component.context),
|
|
||||||
userLocation: .other,
|
|
||||||
attemptSynchronousLoad: false,
|
|
||||||
emoji: emoji,
|
|
||||||
file: component.file,
|
|
||||||
cache: component.context.animationCache,
|
|
||||||
renderer: component.context.animationRenderer,
|
|
||||||
unique: true,
|
|
||||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
|
||||||
pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2),
|
|
||||||
loopCount: 1
|
|
||||||
)
|
|
||||||
animationLayer.isVisibleForAnimations = true
|
|
||||||
self.animationLayer = animationLayer
|
|
||||||
self.layer.addSublayer(animationLayer)
|
|
||||||
}
|
|
||||||
if let animationLayer = self.animationLayer {
|
|
||||||
transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
return iconSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func makeView() -> View {
|
|
||||||
return View(frame: CGRect())
|
|
||||||
}
|
|
||||||
|
|
||||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -85,13 +85,16 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
|
|||||||
}
|
}
|
||||||
|
|
||||||
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||||
|
public let clipContent: Bool
|
||||||
public let requestPop: () -> Void
|
public let requestPop: () -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
items: [AnyComponentWithIdentity<ChildEnvironment>],
|
items: [AnyComponentWithIdentity<ChildEnvironment>],
|
||||||
|
clipContent: Bool = true,
|
||||||
requestPop: @escaping () -> Void
|
requestPop: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.clipContent = clipContent
|
||||||
self.requestPop = requestPop
|
self.requestPop = requestPop
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +102,9 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
|
|||||||
if lhs.items != rhs.items {
|
if lhs.items != rhs.items {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.clipContent != rhs.clipContent {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +204,7 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
|
|||||||
} else {
|
} else {
|
||||||
itemTransition = itemTransition.withAnimation(.none)
|
itemTransition = itemTransition.withAnimation(.none)
|
||||||
itemView = ItemView()
|
itemView = ItemView()
|
||||||
itemView.clipsToBounds = true
|
itemView.clipsToBounds = component.clipContent
|
||||||
self.itemViews[itemId] = itemView
|
self.itemViews[itemId] = itemView
|
||||||
itemView.contents.parentState = state
|
itemView.contents.parentState = state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -999,7 +999,8 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
|||||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: {
|
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: {
|
||||||
interaction.openSettings(.businessSetup)
|
interaction.openSettings(.businessSetup)
|
||||||
}))
|
}))
|
||||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {
|
//TODO:localize
|
||||||
|
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: "Send a Gift", icon: PresentationResourcesSettings.premiumGift, action: {
|
||||||
interaction.openSettings(.premiumGift)
|
interaction.openSettings(.premiumGift)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -6098,8 +6099,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport), let cachedData = data.cachedData as? CachedUserData, !cachedData.premiumGiftOptions.isEmpty {
|
if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_GiftPremium, icon: { theme in
|
//TODO:localize
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "Send a Gift", icon: { theme in
|
||||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor)
|
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.dismissWithoutContent)
|
f(.dismissWithoutContent)
|
||||||
@ -11642,11 +11644,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.view.endEditing(true)
|
strongSelf.view.endEditing(true)
|
||||||
|
|
||||||
strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in
|
strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in
|
||||||
self?.controller?.present(c, in: .window(.root), with: a)
|
self?.controller?.present(c, in: .window(.root), with: a)
|
||||||
}, push: { c in
|
}, push: { c in
|
||||||
self?.controller?.push(c)
|
self?.controller?.push(c)
|
||||||
}, completion: { _, _ in }), in: .window(.root))
|
}, completion: { _, _ in }), in: .window(.root))
|
||||||
|
|
||||||
|
|
||||||
}, displayCopyProtectionTip: { [weak self] node, save in
|
}, displayCopyProtectionTip: { [weak self] node, save in
|
||||||
if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty {
|
if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty {
|
||||||
let _ = (strongSelf.context.engine.data.get(EngineDataMap(
|
let _ = (strongSelf.context.engine.data.get(EngineDataMap(
|
||||||
|
|||||||
@ -36,6 +36,7 @@ swift_library(
|
|||||||
"//submodules/GalleryUI",
|
"//submodules/GalleryUI",
|
||||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||||
|
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import GalleryUI
|
|||||||
import StarsAvatarComponent
|
import StarsAvatarComponent
|
||||||
import MiniAppListScreen
|
import MiniAppListScreen
|
||||||
import PremiumStarComponent
|
import PremiumStarComponent
|
||||||
|
import GiftAnimationComponent
|
||||||
|
|
||||||
private final class StarsTransactionSheetContent: CombinedComponent {
|
private final class StarsTransactionSheetContent: CombinedComponent {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -145,6 +146,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
let title = Child(MultilineTextComponent.self)
|
let title = Child(MultilineTextComponent.self)
|
||||||
let star = Child(StarsImageComponent.self)
|
let star = Child(StarsImageComponent.self)
|
||||||
let activeStar = Child(PremiumStarComponent.self)
|
let activeStar = Child(PremiumStarComponent.self)
|
||||||
|
let gift = Child(GiftAnimationComponent.self)
|
||||||
let amountBackground = Child(RoundedRectangle.self)
|
let amountBackground = Child(RoundedRectangle.self)
|
||||||
let amount = Child(BalancedTextComponent.self)
|
let amount = Child(BalancedTextComponent.self)
|
||||||
let amountStar = Child(BundleIconComponent.self)
|
let amountStar = Child(BundleIconComponent.self)
|
||||||
@ -225,6 +227,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
var isReaction = false
|
var isReaction = false
|
||||||
var giveawayMessageId: MessageId?
|
var giveawayMessageId: MessageId?
|
||||||
var isBoost = false
|
var isBoost = false
|
||||||
|
var giftAnimation: TelegramMediaFile?
|
||||||
|
|
||||||
var delayedCloseOnOpenPeer = true
|
var delayedCloseOnOpenPeer = true
|
||||||
switch subject {
|
switch subject {
|
||||||
@ -322,7 +325,18 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .transaction(transaction, parentPeer):
|
case let .transaction(transaction, parentPeer):
|
||||||
if let giveawayMessageIdValue = transaction.giveawayMessageId {
|
if let starGift = transaction.starGift {
|
||||||
|
titleText = "Gift"
|
||||||
|
descriptionText = ""
|
||||||
|
count = transaction.count
|
||||||
|
transactionId = transaction.id
|
||||||
|
date = transaction.date
|
||||||
|
if case let .peer(peer) = transaction.peer {
|
||||||
|
toPeer = peer
|
||||||
|
}
|
||||||
|
transactionPeer = transaction.peer
|
||||||
|
giftAnimation = starGift.file
|
||||||
|
} else if let giveawayMessageIdValue = transaction.giveawayMessageId {
|
||||||
titleText = strings.Stars_Transaction_Giveaway_Title
|
titleText = strings.Stars_Transaction_Giveaway_Title
|
||||||
descriptionText = ""
|
descriptionText = ""
|
||||||
count = transaction.count
|
count = transaction.count
|
||||||
@ -572,7 +586,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
imageIcon = nil
|
imageIcon = nil
|
||||||
}
|
}
|
||||||
var starChild: _UpdatedChildComponent
|
var starChild: _UpdatedChildComponent
|
||||||
if isBoost {
|
if let giftAnimation {
|
||||||
|
starChild = gift.update(
|
||||||
|
component: GiftAnimationComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: theme,
|
||||||
|
file: giftAnimation
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 128.0, height: 128.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
} else if isBoost {
|
||||||
starChild = activeStar.update(
|
starChild = activeStar.update(
|
||||||
component: PremiumStarComponent(
|
component: PremiumStarComponent(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
@ -877,7 +901,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
context.add(starChild
|
context.add(starChild
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: starChild.size.height / 2.0 - 19.0))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: 200.0 / 2.0 - 19.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
context.add(title
|
context.add(title
|
||||||
@ -885,7 +909,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
var originY: CGFloat = 0.0
|
var originY: CGFloat = 0.0
|
||||||
originY += starChild.size.height - 23.0
|
originY += 200.0 - 23.0
|
||||||
|
|
||||||
var descriptionSize: CGSize = .zero
|
var descriptionSize: CGSize = .zero
|
||||||
if !descriptionText.isEmpty {
|
if !descriptionText.isEmpty {
|
||||||
|
|||||||
@ -209,7 +209,10 @@ final class StarsTransactionsListPanelComponent: Component {
|
|||||||
var itemPeer = item.peer
|
var itemPeer = item.peer
|
||||||
switch item.peer {
|
switch item.peer {
|
||||||
case let .peer(peer):
|
case let .peer(peer):
|
||||||
if let _ = item.giveawayMessageId {
|
if let _ = item.starGift {
|
||||||
|
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
||||||
|
itemSubtitle = item.count > 0 ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift
|
||||||
|
} else if let _ = item.giveawayMessageId {
|
||||||
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
||||||
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize
|
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize
|
||||||
} else if !item.media.isEmpty {
|
} else if !item.media.isEmpty {
|
||||||
|
|||||||
@ -6961,48 +6961,69 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if !component.slice.effectivePeer.isService {
|
if !component.slice.effectivePeer.isService {
|
||||||
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Report, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] c, a in
|
}, action: { [weak self] _, f in
|
||||||
guard let self, let component = self.component, let controller = component.controller() else {
|
guard let self, let component = self.component, let controller = component.controller() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
|
f(.default)
|
||||||
presentPeerReportOptions(
|
|
||||||
|
self.isReporting = true
|
||||||
|
self.updateIsProgressPaused()
|
||||||
|
|
||||||
|
component.context.sharedContext.makeContentReportScreen(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
parent: controller,
|
subject: .stories(component.slice.effectivePeer.id, [component.slice.item.storyItem.id]),
|
||||||
contextController: c,
|
forceDark: true,
|
||||||
backAction: { _ in },
|
present: { c in
|
||||||
subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id),
|
controller.push(c)
|
||||||
options: options,
|
},
|
||||||
passthrough: true,
|
completion: { [weak self] in
|
||||||
forceTheme: defaultDarkPresentationTheme,
|
|
||||||
isDetailedReportingVisible: { [weak self] isReporting in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.isReporting = isReporting
|
self.isReporting = false
|
||||||
self.updateIsProgressPaused()
|
self.updateIsProgressPaused()
|
||||||
},
|
|
||||||
completion: { [weak self] reason, _ in
|
|
||||||
guard let self, let component = self.component, let controller = component.controller(), let reason else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone()
|
|
||||||
controller.present(
|
|
||||||
UndoOverlayController(
|
|
||||||
presentationData: presentationData,
|
|
||||||
content: .emoji(
|
|
||||||
name: "PoliceCar",
|
|
||||||
text: presentationData.strings.Report_Succeed
|
|
||||||
),
|
|
||||||
elevatedLayout: false,
|
|
||||||
blurred: true,
|
|
||||||
action: { _ in return false }
|
|
||||||
)
|
|
||||||
, in: .current
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
|
||||||
|
// presentPeerReportOptions(
|
||||||
|
// context: component.context,
|
||||||
|
// parent: controller,
|
||||||
|
// contextController: c,
|
||||||
|
// backAction: { _ in },
|
||||||
|
// subject: .story(component.slice.effectivePeer.id, component.slice.item.storyItem.id),
|
||||||
|
// options: options,
|
||||||
|
// passthrough: true,
|
||||||
|
// forceTheme: defaultDarkPresentationTheme,
|
||||||
|
// isDetailedReportingVisible: { [weak self] isReporting in
|
||||||
|
// guard let self else {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// self.isReporting = isReporting
|
||||||
|
// self.updateIsProgressPaused()
|
||||||
|
// },
|
||||||
|
// completion: { [weak self] reason, _ in
|
||||||
|
// guard let self, let component = self.component, let controller = component.controller(), let reason else {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.effectivePeer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").startStandalone()
|
||||||
|
// controller.present(
|
||||||
|
// UndoOverlayController(
|
||||||
|
// presentationData: presentationData,
|
||||||
|
// content: .emoji(
|
||||||
|
// name: "PoliceCar",
|
||||||
|
// text: presentationData.strings.Report_Succeed
|
||||||
|
// ),
|
||||||
|
// elevatedLayout: false,
|
||||||
|
// blurred: true,
|
||||||
|
// action: { _ in return false }
|
||||||
|
// )
|
||||||
|
// , in: .current
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1957,22 +1957,30 @@ extension ChatControllerImpl {
|
|||||||
])
|
])
|
||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
} else {
|
} else {
|
||||||
strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in
|
strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in
|
||||||
self?.present(c, in: .window(.root), with: a)
|
self?.push(controller)
|
||||||
}, push: { c in
|
}, completion: { [weak self] in
|
||||||
self?.push(c)
|
self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||||
}, completion: { _, done in
|
})
|
||||||
if done {
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
|
||||||
}
|
|
||||||
}), in: .window(.root))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, reportMessages: { [weak self] messages, contextController in
|
}, reportMessages: { [weak self] messages, contextController in
|
||||||
if let strongSelf = self, !messages.isEmpty {
|
guard let self, !messages.isEmpty else {
|
||||||
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
|
return
|
||||||
presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in })
|
|
||||||
}
|
}
|
||||||
|
contextController?.dismiss()
|
||||||
|
self.context.sharedContext.makeContentReportScreen(
|
||||||
|
context: self.context,
|
||||||
|
subject: .messages(messages.map({ $0.id }).sorted()),
|
||||||
|
forceDark: false,
|
||||||
|
present: { [weak self] controller in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.push(controller)
|
||||||
|
},
|
||||||
|
completion: {}
|
||||||
|
)
|
||||||
}, blockMessageAuthor: { [weak self] message, contextController in
|
}, blockMessageAuthor: { [weak self] message, contextController in
|
||||||
contextController?.dismiss(completion: {
|
contextController?.dismiss(completion: {
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
|||||||
@ -73,6 +73,7 @@ import MiniAppListScreen
|
|||||||
import GiftOptionsScreen
|
import GiftOptionsScreen
|
||||||
import GiftViewScreen
|
import GiftViewScreen
|
||||||
import StarsIntroScreen
|
import StarsIntroScreen
|
||||||
|
import ContentReportScreen
|
||||||
|
|
||||||
private final class AccountUserInterfaceInUseContext {
|
private final class AccountUserInterfaceInUseContext {
|
||||||
let subscribers = Bag<(Bool) -> Void>()
|
let subscribers = Bag<(Bool) -> Void>()
|
||||||
@ -2819,6 +2820,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
return GiftViewScreen(context: context, subject: .message(message))
|
return GiftViewScreen(context: context, subject: .message(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
|
||||||
|
let _ = (context.engine.messages.reportContent(subject: subject, option: nil, message: nil)
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||||
|
if case let .options(title, options) = result {
|
||||||
|
present(ContentReportScreen(context: context, subject: subject, title: title, options: options, forceDark: forceDark, completed: completion))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
|
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
|
||||||
return MiniAppListScreen.initialData(context: context)
|
return MiniAppListScreen.initialData(context: context)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,9 @@ public enum DeviceModel: CaseIterable, Equatable {
|
|||||||
.iPhone15,
|
.iPhone15,
|
||||||
.iPhone15Plus,
|
.iPhone15Plus,
|
||||||
.iPhone15Pro,
|
.iPhone15Pro,
|
||||||
.iPhone15ProMax
|
.iPhone15ProMax,
|
||||||
|
.iPhone16Pro,
|
||||||
|
.iPhone16ProMax
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +118,9 @@ public enum DeviceModel: CaseIterable, Equatable {
|
|||||||
case iPhone15Pro
|
case iPhone15Pro
|
||||||
case iPhone15ProMax
|
case iPhone15ProMax
|
||||||
|
|
||||||
|
case iPhone16Pro
|
||||||
|
case iPhone16ProMax
|
||||||
|
|
||||||
case unknown(String)
|
case unknown(String)
|
||||||
|
|
||||||
public var modelId: [String] {
|
public var modelId: [String] {
|
||||||
@ -218,6 +223,10 @@ public enum DeviceModel: CaseIterable, Equatable {
|
|||||||
return ["iPhone16,1"]
|
return ["iPhone16,1"]
|
||||||
case .iPhone15ProMax:
|
case .iPhone15ProMax:
|
||||||
return ["iPhone16,2"]
|
return ["iPhone16,2"]
|
||||||
|
case .iPhone16Pro:
|
||||||
|
return ["iPhone17,1"]
|
||||||
|
case .iPhone16ProMax:
|
||||||
|
return ["iPhone17,2"]
|
||||||
case let .unknown(modelId):
|
case let .unknown(modelId):
|
||||||
return [modelId]
|
return [modelId]
|
||||||
}
|
}
|
||||||
@ -323,6 +332,10 @@ public enum DeviceModel: CaseIterable, Equatable {
|
|||||||
return "iPhone 15 Pro"
|
return "iPhone 15 Pro"
|
||||||
case .iPhone15ProMax:
|
case .iPhone15ProMax:
|
||||||
return "iPhone 15 Pro Max"
|
return "iPhone 15 Pro Max"
|
||||||
|
case .iPhone16Pro:
|
||||||
|
return "iPhone 16 Pro"
|
||||||
|
case .iPhone16ProMax:
|
||||||
|
return "iPhone 16 Pro Max"
|
||||||
case let .unknown(modelId):
|
case let .unknown(modelId):
|
||||||
if modelId.hasPrefix("iPhone") {
|
if modelId.hasPrefix("iPhone") {
|
||||||
return "Unknown iPhone"
|
return "Unknown iPhone"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user