Merge commit '243d0be94058e9f86d72aca0b0455927003739f2' into beta

This commit is contained in:
Ilya Laktyushin 2024-09-24 17:37:15 +04:00
commit 10e8154f94
44 changed files with 2218 additions and 385 deletions

View File

@ -12285,6 +12285,8 @@ Sorry for the inconvenience.";
"Stars.Intro.Transaction.FragmentWithdrawal.Subtitle" = "via Fragment";
"Stars.Intro.Transaction.TelegramAds.Title" = "Withdrawal";
"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.Refund" = "Refund";

View File

@ -1020,6 +1020,8 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsIntroScreen(context: AccountContext) -> 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 makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController

View File

@ -102,7 +102,7 @@ final class CameraDeviceContext {
return 30.0
}
switch DeviceModel.current {
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax:
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax:
return 60.0
default:
return 30.0

View File

@ -34,6 +34,10 @@ public extension Camera {
self = .iPhone15Pro
case .iPhone15ProMax:
self = .iPhone15ProMax
case .iPhone16Pro:
self = .iPhone15Pro
case .iPhone16ProMax:
self = .iPhone15ProMax
case .unknown:
self = .unknown
default:

View File

@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
case iPhone14ProZoomed
case iPhone14ProMax
case iPhone14ProMaxZoomed
case iPhone16Pro
case iPhone16ProMax
case iPad
case iPadMini
case iPad102Inch
@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
.iPhone14ProZoomed,
.iPhone14ProMax,
.iPhone14ProMaxZoomed,
.iPhone16Pro,
.iPhone16ProMax,
.iPad,
.iPadMini,
.iPad102Inch,
@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 430.0, height: 932.0)
case .iPhone14ProMaxZoomed:
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:
return CGSize(width: 768.0, height: 1024.0)
case .iPadMini:
@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 53.0 + UIScreenPixel
case .iPhone14Pro, .iPhone14ProMax:
return 55.0
case .iPhone16Pro, .iPhone16ProMax:
return 55.0
case let .unknown(_, _, _, screenCornerRadius):
return screenCornerRadius
default:
@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
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)
default:
return UIEdgeInsets.zero
@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
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
case .iPhone14ProZoomed:
return inLandscape ? 21.0 : 28.0
@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 54.0
case .iPhone14ProMaxZoomed:
return 47.0
case .iPhone16Pro, .iPhone16ProMax:
return 54.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return 44.0
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 162.0
case .iPhone6, .iPhone6Plus:
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
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0
@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 216.0
case .iPhone6Plus:
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
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
return 302.0
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 263.0
@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
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
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0
@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self {
case .iPhone4, .iPhone5:
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
case .iPhone6Plus:
return 45.0
@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var hasDynamicIsland: Bool {
switch self {
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return true
default:
return false

View File

@ -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))
} else {
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)
} 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))

View File

@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 226.0
self.biometricsOffset = 30.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.horizontalSecond = 103.0
self.horizontalThird = 206.0
@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 294.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.buttonSize = 85.0
self.horizontalSecond = 115.0
self.horizontalThird = 230.0
@ -151,11 +151,11 @@ public struct PasscodeLayout {
self.titleOffset = 112.0
self.subtitleOffset = -6.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.subtitleOffset = 0.0
self.inputFieldOffset = 206.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.titleOffset = 180.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 226.0

View File

@ -631,6 +631,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) }
dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($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[975236280] = { return Api.MessagesFilter.parse_inputMessagesFilterChatPhotos($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[1490799288] = { return Api.ReportReason.parse_inputReportReasonSpam($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[-906990053] = { return Api.RequestPeerType.parse_requestPeerTypeChat($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[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($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[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) }
dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) }
@ -1853,6 +1857,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.MessageReplyHeader:
_1.serialize(buffer, boxed)
case let _1 as Api.MessageReportOption:
_1.serialize(buffer, boxed)
case let _1 as Api.MessageViews:
_1.serialize(buffer, boxed)
case let _1 as Api.MessagesFilter:
@ -1969,6 +1975,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.ReportReason:
_1.serialize(buffer, boxed)
case let _1 as Api.ReportResult:
_1.serialize(buffer, boxed)
case let _1 as Api.RequestPeerType:
_1.serialize(buffer, boxed)
case let _1 as Api.RequestedPeer:

View File

@ -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 {
enum MessageViews: TypeConstructorDescription {
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
}
}
}
}

View File

@ -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 {
enum NotifyPeer: TypeConstructorDescription {
case notifyBroadcasts

View File

@ -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 {
enum RequestPeerType: TypeConstructorDescription {
case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?)

View File

@ -1002,13 +1002,13 @@ public extension Api {
}
public extension Api {
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) {
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 {
buffer.appendInt32(-294313259)
buffer.appendInt32(178185410)
}
serializeInt32(flags, 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 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
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):
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)])
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), ("stargift", stargift as Any)])
}
}
@ -1077,6 +1078,10 @@ public extension Api {
if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() }
var _15: Int32?
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 _c2 = _2 != nil
let _c3 = _3 != nil
@ -1092,8 +1097,9 @@ public extension Api {
let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil
let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != 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 {
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)
let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil
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 {
return nil

View File

@ -7380,22 +7380,22 @@ 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()
buffer.appendInt32(-1991005362)
buffer.appendInt32(-59199589)
peer.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(id.count))
for item in id {
serializeInt32(item, buffer: buffer, boxed: false)
}
reason.serialize(buffer, true)
serializeBytes(option, 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)
var result: Api.Bool?
var result: Api.ReportResult?
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
})
@ -10787,22 +10787,22 @@ 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()
buffer.appendInt32(421788300)
buffer.appendInt32(433646405)
peer.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(id.count))
for item in id {
serializeInt32(item, buffer: buffer, boxed: false)
}
reason.serialize(buffer, true)
serializeBytes(option, 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)
var result: Api.Bool?
var result: Api.ReportResult?
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
})

View File

@ -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)
}
}

View File

@ -184,7 +184,7 @@ final class VideoChatMicButtonComponent: Component {
case connecting
case muted
case unmuted(pushToTalk: Bool)
case raiseHand
case raiseHand(isRaised: Bool)
case scheduled(state: ScheduledState)
}
@ -226,6 +226,7 @@ final class VideoChatMicButtonComponent: Component {
private var disappearingBackgrounds: [UIImageView] = []
private var progressIndicator: RadialStatusNode?
private let title = ComponentView<Empty>()
private var subtitle: ComponentView<Empty>?
private let icon: VoiceChatActionButtonIconNode
private var glowView: GlowView?
@ -322,6 +323,7 @@ final class VideoChatMicButtonComponent: Component {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let titleText: String
var subtitleText: String?
var isEnabled = true
switch component.content {
case .connecting:
@ -331,8 +333,14 @@ final class VideoChatMicButtonComponent: Component {
titleText = "Unmute"
case let .unmuted(isPushToTalk):
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
case .raiseHand:
titleText = "Raise Hand"
case let .raiseHand(isRaised):
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):
switch state {
case .start:
@ -353,7 +361,7 @@ final class VideoChatMicButtonComponent: Component {
text: .plain(NSAttributedString(string: titleText, font: Font.regular(15.0), textColor: .white))
)),
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)
@ -470,7 +478,10 @@ final class VideoChatMicButtonComponent: Component {
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 titleView.superview == nil {
titleView.isUserInteractionEnabled = false
@ -481,6 +492,47 @@ final class VideoChatMicButtonComponent: Component {
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 {
self.icon.view.isUserInteractionEnabled = false
self.addSubview(self.icon.view)

View File

@ -283,7 +283,7 @@ final class VideoChatParticipantAvatarComponent: Component {
transition.setBounds(view: avatarNode.view, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
avatarNode.updateSize(size: avatarSize)
let blobScale: CGFloat = 1.5
let blobScale: CGFloat = 2.0
if self.audioLevelDisposable == nil {
struct Level {

View File

@ -117,6 +117,13 @@ final class VideoChatParticipantsComponent: Component {
}
}
final class EventCycleState {
var ignoreScrolling: Bool = false
init() {
}
}
let call: PresentationGroupCall
let participants: Participants?
let speakingParticipants: Set<EnginePeer.Id>
@ -132,6 +139,7 @@ final class VideoChatParticipantsComponent: Component {
let updateIsMainParticipantPinned: (Bool) -> Void
let updateIsExpandedUIHidden: (Bool) -> Void
let openInviteMembers: () -> Void
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
init(
call: PresentationGroupCall,
@ -148,7 +156,8 @@ final class VideoChatParticipantsComponent: Component {
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
openInviteMembers: @escaping () -> Void
openInviteMembers: @escaping () -> Void,
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
) {
self.call = call
self.participants = participants
@ -165,6 +174,7 @@ final class VideoChatParticipantsComponent: Component {
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
self.openInviteMembers = openInviteMembers
self.visibleParticipantsUpdated = visibleParticipantsUpdated
}
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))
} else {
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.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 {
private let scrollViewClippingContainer: SolidRoundedCornersContainer
private let scrollView: ScrollView
private let scrollViewBottomShadowView: UIImageView
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
private let separateVideoScrollView: ScrollView
@ -622,6 +633,7 @@ final class VideoChatParticipantsComponent: Component {
private let expandedGridItemContainer: UIView
private var expandedControlsView: ComponentView<Empty>?
private var expandedThumbnailsView: ComponentView<Empty>?
private var expandedSpeakingToast: ComponentView<Empty>?
private var listItemViews: [EnginePeer.Id: ListItem] = [:]
private let listItemViewContainer: UIView
@ -635,9 +647,13 @@ final class VideoChatParticipantsComponent: Component {
private var currentLoadMoreToken: String?
private var mainScrollViewEventCycleState: EventCycleState?
private var separateVideoScrollViewEventCycleState: EventCycleState?
override init(frame: CGRect) {
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
self.scrollView = ScrollView()
self.scrollViewBottomShadowView = UIImageView()
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
self.separateVideoScrollView = ScrollView()
@ -687,6 +703,7 @@ final class VideoChatParticipantsComponent: Component {
self.scrollViewClippingContainer.addSubview(self.scrollView)
self.addSubview(self.scrollViewClippingContainer)
self.addSubview(self.scrollViewClippingContainer.cornersView)
self.addSubview(self.scrollViewBottomShadowView)
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
self.addSubview(self.separateVideoScrollViewClippingContainer)
@ -765,10 +782,46 @@ final class VideoChatParticipantsComponent: Component {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
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)
}
}
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) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
@ -832,11 +885,18 @@ final class VideoChatParticipantsComponent: Component {
var validGridItemIds: [VideoParticipantKey] = []
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 clippedVisibleGridItemRange: (minIndex: Int, maxIndex: Int)
if itemLayout.layout.videoColumn == nil {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
clippedVisibleGridItemRange = itemLayout.visibleGridItemRange(for: clippedScrollViewBounds)
} else {
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
clippedVisibleGridItemRange = visibleGridItemRange
}
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
@ -852,6 +912,8 @@ final class VideoChatParticipantsComponent: Component {
validGridItemIndices.append(index)
}
}
var visibleParticipants: [EnginePeer.Id] = []
for index in validGridItemIndices {
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
if isItemExpanded {
if let previousExpandedItemId, previousExpandedItemId != videoParticipantKey {
@ -1066,11 +1132,16 @@ final class VideoChatParticipantsComponent: Component {
var validListItemIds: [EnginePeer.Id] = []
let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds)
let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds)
if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex {
for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex {
let participant = self.listParticipants[i]
validListItemIds.append(participant.peer.id)
if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex {
visibleParticipants.append(participant.peer.id)
}
var itemTransition = transition
let itemView: ListItem
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 {
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
} 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 {
subtitle = PeerListItemComponent.Subtitle(text: participant.about ?? "listening", color: .neutral)
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral)
}
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 self.currentLoadMoreToken != loadMoreToken {
self.currentLoadMoreToken = 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 {
@ -1482,11 +1633,16 @@ final class VideoChatParticipantsComponent: Component {
var listParticipants: [GroupCallParticipantsContext.Participant] = []
if let participants = component.participants {
for participant in participants.participants {
var isFullyMuted = false
if let muteState = participant.muteState, !muteState.canUnmute {
isFullyMuted = true
}
var hasVideo = false
if participant.videoDescription != nil {
hasVideo = true
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)
} else {
gridParticipants.append(videoParticipant)
@ -1495,14 +1651,14 @@ final class VideoChatParticipantsComponent: Component {
if participant.presentationDescription != nil {
hasVideo = 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)
} else {
gridParticipants.append(videoParticipant)
}
}
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)
} else {
listParticipants.append(participant)
@ -1594,6 +1750,37 @@ final class VideoChatParticipantsComponent: Component {
smoothCorners: false
), 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.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)

View File

@ -41,15 +41,22 @@ final class VideoChatScreenComponent: Component {
return true
}
private struct PanGestureState {
var offsetFraction: CGFloat
private final class PanState {
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) {
self.offsetFraction = offsetFraction
init(fraction: CGFloat, scrollView: UIScrollView?) {
self.fraction = fraction
self.scrollView = scrollView
}
}
final class View: UIView {
final class View: UIView, UIGestureRecognizerDelegate {
let containerView: UIView
var component: VideoChatScreenComponent?
@ -57,7 +64,7 @@ final class VideoChatScreenComponent: Component {
weak var state: EmptyComponentState?
var isUpdating: Bool = false
private var panGestureState: PanGestureState?
private var verticalPanState: PanState?
var notifyDismissedInteractivelyOnPanGestureApply: Bool = false
var completionOnPanGestureApply: (() -> Void)?
@ -95,6 +102,9 @@ final class VideoChatScreenComponent: Component {
var members: PresentationGroupCallMembers?
var membersDisposable: Disposable?
var speakingParticipantPeers: [EnginePeer] = []
var visibleParticipants: Set<EnginePeer.Id> = Set()
let isPresentedValue = ValuePromise<Bool>(false, ignoreRepeated: true)
var applicationStateDisposable: Disposable?
@ -117,9 +127,11 @@ final class VideoChatScreenComponent: Component {
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) {
@ -139,37 +151,159 @@ final class VideoChatScreenComponent: Component {
}
func animateIn() {
self.panGestureState = PanGestureState(offsetFraction: 1.0)
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
self.state?.updated(transition: .immediate)
self.panGestureState = nil
self.verticalPanState = nil
self.state?.updated(transition: .spring(duration: 0.5))
}
func animateOut(completion: @escaping () -> Void) {
self.panGestureState = PanGestureState(offsetFraction: 1.0)
self.verticalPanState = PanState(fraction: 1.0, scrollView: nil)
self.completionOnPanGestureApply = completion
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) {
switch recognizer.state {
case .began, .changed:
if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply {
let translation = recognizer.translation(in: self)
self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height)
self.state?.updated(transition: .immediate)
let fraction = max(0.0, translation.y / self.bounds.height)
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:
if !self.bounds.height.isZero {
if !self.bounds.height.isZero, let verticalPanState = self.verticalPanState {
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)
self.panGestureState = nil
if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 {
self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0)
self.verticalPanState = nil
if effectiveFraction > 0.6 || (effectiveFraction > 0.0 && velocity.y >= 100.0) {
self.verticalPanState = PanState(fraction: effectiveFraction < 0.0 ? -1.0 : 1.0, scrollView: nil)
self.notifyDismissedInteractivelyOnPanGestureApply = true
if let controller = self.environment?.controller() as? VideoChatScreenV2Impl {
controller.notifyDismissed()
@ -347,6 +481,15 @@ final class VideoChatScreenComponent: Component {
guard let component = self.component, let environment = self.environment else {
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)
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 {
self.isUpdating = true
defer {
@ -585,7 +761,7 @@ final class VideoChatScreenComponent: Component {
if self.members != members {
var members = members
#if DEBUG && false
#if DEBUG && true
if let membersValue = members {
var participants = membersValue.participants
for i in 1 ... 20 {
@ -640,25 +816,7 @@ final class VideoChatScreenComponent: Component {
#endif
if let membersValue = members {
var 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
})
let participants = membersValue.participants
members = PresentationGroupCallMembers(
participants: participants,
speakingParticipants: membersValue.speakingParticipants,
@ -746,6 +904,19 @@ final class VideoChatScreenComponent: Component {
if !self.isUpdating {
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
if let panGestureState = self.panGestureState {
containerOffset = panGestureState.offsetFraction * availableSize.height
if let verticalPanState = self.verticalPanState {
if verticalPanState.scrollView != nil {
containerOffset = verticalPanState.accumulatedOffset
} else {
containerOffset = verticalPanState.fraction * availableSize.height
}
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
}
@ -907,7 +1082,7 @@ final class VideoChatScreenComponent: Component {
guard let self, completed else {
return
}
if self.panGestureState == nil {
if self.verticalPanState == nil {
self.containerView.layer.cornerRadius = 0.0
}
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 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 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
}
self.openInviteMembers()
},
visibleParticipantsUpdated: { [weak self] visibleParticipants in
guard let self else {
return
}
self.onVisibleParticipantsUpdated(ids: visibleParticipants)
}
)),
environment: {},
@ -1403,8 +1592,8 @@ final class VideoChatScreenComponent: Component {
micButtonContent = .connecting
actionButtonMicrophoneState = .connecting
case .connected:
if let callState = callState.muteState {
if callState.canUnmute {
if let muteState = callState.muteState {
if muteState.canUnmute {
if self.isPushToTalkActive {
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
actionButtonMicrophoneState = .unmuted
@ -1413,7 +1602,7 @@ final class VideoChatScreenComponent: Component {
actionButtonMicrophoneState = .muted
}
} else {
micButtonContent = .raiseHand
micButtonContent = .raiseHand(isRaised: callState.raisedHand)
actionButtonMicrophoneState = .raiseHand
}
} else {
@ -1741,9 +1930,11 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
}
self.isAnimatingDismiss = false
self.superDismiss()
completion?()
})
} else {
self.superDismiss()
completion?()
}
}
}

View File

@ -248,8 +248,8 @@ extension VideoChatScreenComponent.View {
}
let context = component.call.accountContext
environment.controller()?.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().after(0.3) {
controller.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().after(0.1) {
guard let navigationController else {
return
}

View File

@ -5,6 +5,7 @@ import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import HierarchyTrackingLayer
import ChatTitleActivityNode
final class VideoChatTitleComponent: Component {
let title: String
@ -43,12 +44,17 @@ final class VideoChatTitleComponent: Component {
final class View: UIView {
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private let title = ComponentView<Empty>()
private var status: ComponentView<Empty>?
private let status = ComponentView<Empty>()
private var recordingImageView: UIImageView?
private var activityStatusNode: ChatTitleActivityNode?
private var component: VideoChatTitleComponent?
private var isUpdating: Bool = false
private var currentActivityStatus: String?
private var currentSize: CGSize?
override init(frame: CGRect) {
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 {
self.isUpdating = true
defer {
@ -100,19 +164,12 @@ final class VideoChatTitleComponent: Component {
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>
statusComponent = AnyComponent(MultilineTextComponent(
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,
component: statusComponent,
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)
if let statusView = status.view {
if let statusView = self.status.view {
if statusView.superview == nil {
self.addSubview(statusView)
}
@ -165,6 +222,8 @@ final class VideoChatTitleComponent: Component {
}
}
self.currentSize = size
return size
}
}

View File

@ -7098,15 +7098,14 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
}
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
/*var useV2 = true
var useV2 = true
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
useV2 = false
}
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_videochatui_v2"] {
useV2 = false
}
return useV2*/
return false
return useV2
}
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {

View File

@ -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
}

View File

@ -1463,6 +1463,10 @@ public extension TelegramEngine {
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> {
return _internal_updateExtendedMedia(account: self.account, messageIds: messageIds)
}

View File

@ -468,7 +468,7 @@ private final class StarsContextImpl {
}
var transactions = state.transactions
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))
@ -490,7 +490,7 @@ private final class StarsContextImpl {
private extension StarsContext.State.Transaction {
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
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
var paidMessageId: 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 _ = 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 media: [Media]
public let subscriptionPeriod: Int32?
public let starGift: StarGift?
public init(
flags: Flags,
@ -628,7 +629,8 @@ public final class StarsContext {
paidMessageId: MessageId?,
giveawayMessageId: MessageId?,
media: [Media],
subscriptionPeriod: Int32?
subscriptionPeriod: Int32?,
starGift: StarGift?
) {
self.flags = flags
self.id = id
@ -644,6 +646,7 @@ public final class StarsContext {
self.giveawayMessageId = giveawayMessageId
self.media = media
self.subscriptionPeriod = subscriptionPeriod
self.starGift = starGift
}
public static func == (lhs: Transaction, rhs: Transaction) -> Bool {
@ -689,6 +692,9 @@ public final class StarsContext {
if lhs.subscriptionPeriod != rhs.subscriptionPeriod {
return false
}
if lhs.starGift != rhs.starGift {
return false
}
return true
}
}

View File

@ -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> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
let groupedIds = messagesIdsGroupedByPeerId(messageIds)
let signals = groupedIds.values.compactMap { ids -> Signal<Void, NoError>? in
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 .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} |> switchToLatest
return .complete()
// return account.postbox.transaction { transaction -> Signal<Void, NoError> in
// let groupedIds = messagesIdsGroupedByPeerId(messageIds)
// let signals = groupedIds.values.compactMap { ids -> Signal<Void, NoError>? in
// 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 .single(.boolFalse)
// }
// |> mapToSignal { _ -> Signal<Void, NoError> in
// return .complete()
// }
// }
//
// return combineLatest(signals)
// |> mapToSignal { _ -> Signal<Void, NoError> in
// return .complete()
// }
// } |> switchToLatest
}
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
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
} |> switchToLatest
return .complete()
// return account.postbox.transaction { transaction -> Signal<Void, NoError> in
// if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
// return account.network.request(Api.functions.stories.report(peer: inputPeer, id: [storyId], reason: reason.apiReason, message: message))
// |> `catch` { _ -> Signal<Api.Bool, NoError> in
// return .single(.boolFalse)
// }
// |> mapToSignal { _ -> Signal<Void, NoError> in
// return .complete()
// }
// } else {
// return .complete()
// }
// } |> switchToLatest
}
func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal<Never, NoError> {

View File

@ -461,6 +461,7 @@ swift_library(
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
"//submodules/TelegramUI/Components/ContentReportScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -396,6 +396,7 @@ private final class SheetContent: CombinedComponent {
let navigation = navigation.update(
component: NavigationStackComponent(
items: items,
clipContent: false,
requestPop: { [weak state] in
state?.pushedOptions.removeLast()
update(.spring(duration: 0.45))

View File

@ -324,13 +324,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
self.closeAction?()
}
if #available(iOS 16.0, *) {
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.view.addSubview(self.pipView)
self.pipView.frame = pipVideoCallViewController.view.bounds
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.pipView.translatesAutoresizingMaskIntoConstraints = true
self.pipVideoCallViewController = pipVideoCallViewController
if !"".isEmpty {
if #available(iOS 16.0, *) {
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.view.addSubview(self.pipView)
self.pipView.frame = pipVideoCallViewController.view.bounds
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.pipView.translatesAutoresizingMaskIntoConstraints = true
self.pipVideoCallViewController = pipVideoCallViewController
}
}
if let blurFilter = makeBlurFilter() {

View File

@ -1659,7 +1659,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
let loopVideo = updatedVideoFile.isAnimated
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)
} 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

View 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",
],
)

View File

@ -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()
}
}
}

View File

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

View File

@ -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)
}
}

View File

@ -37,6 +37,7 @@ swift_library(
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
"//submodules/UndoUI",
],
visibility = [

View File

@ -23,6 +23,7 @@ import TelegramStringFormatting
import StarsAvatarComponent
import EmojiTextAttachmentView
import UndoUI
import GiftAnimationComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -695,14 +696,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case message(EngineMessage)
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 {
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 {
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):
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
}
@ -789,9 +790,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
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,
action: { _ in return true}
action: { action in
if case .info = action {
}
return true
}
)
lastController.present(resultController, in: .window(.root))
}
@ -800,13 +806,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
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
}
let controller = textAlertController(
context: self.context,
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: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in
@ -1253,91 +1259,3 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
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)
}
}

View File

@ -85,13 +85,16 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
}
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
public let clipContent: Bool
public let requestPop: () -> Void
public init(
items: [AnyComponentWithIdentity<ChildEnvironment>],
clipContent: Bool = true,
requestPop: @escaping () -> Void
) {
self.items = items
self.clipContent = clipContent
self.requestPop = requestPop
}
@ -99,6 +102,9 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
if lhs.items != rhs.items {
return false
}
if lhs.clipContent != rhs.clipContent {
return false
}
return true
}
@ -198,7 +204,7 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
itemView.clipsToBounds = true
itemView.clipsToBounds = component.clipContent
self.itemViews[itemId] = itemView
itemView.contents.parentState = state
}

View File

@ -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: {
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)
}))
}
@ -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 {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_GiftPremium, icon: { theme in
if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
//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)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
@ -11642,11 +11644,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
strongSelf.view.endEditing(true)
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)
}, push: { c in
self?.controller?.push(c)
}, completion: { _, _ in }), in: .window(.root))
}, displayCopyProtectionTip: { [weak self] node, save in
if let strongSelf = self, let peer = strongSelf.data?.peer, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty {
let _ = (strongSelf.context.engine.data.get(EngineDataMap(

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/GalleryUI",
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
],
visibility = [
"//visibility:public",

View File

@ -25,6 +25,7 @@ import GalleryUI
import StarsAvatarComponent
import MiniAppListScreen
import PremiumStarComponent
import GiftAnimationComponent
private final class StarsTransactionSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -145,6 +146,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let title = Child(MultilineTextComponent.self)
let star = Child(StarsImageComponent.self)
let activeStar = Child(PremiumStarComponent.self)
let gift = Child(GiftAnimationComponent.self)
let amountBackground = Child(RoundedRectangle.self)
let amount = Child(BalancedTextComponent.self)
let amountStar = Child(BundleIconComponent.self)
@ -225,6 +227,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var isReaction = false
var giveawayMessageId: MessageId?
var isBoost = false
var giftAnimation: TelegramMediaFile?
var delayedCloseOnOpenPeer = true
switch subject {
@ -322,7 +325,18 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
}
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
descriptionText = ""
count = transaction.count
@ -572,7 +586,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
imageIcon = nil
}
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(
component: PremiumStarComponent(
theme: theme,
@ -877,7 +901,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
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
@ -885,7 +909,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
var originY: CGFloat = 0.0
originY += starChild.size.height - 23.0
originY += 200.0 - 23.0
var descriptionSize: CGSize = .zero
if !descriptionText.isEmpty {

View File

@ -209,7 +209,10 @@ final class StarsTransactionsListPanelComponent: Component {
var itemPeer = item.peer
switch item.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)
itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize
} else if !item.media.isEmpty {

View File

@ -6961,48 +6961,69 @@ public final class StoryItemSetContainerComponent: Component {
if !component.slice.effectivePeer.isService {
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)
}, action: { [weak self] c, a in
}, action: { [weak self] _, f in
guard let self, let component = self.component, let controller = component.controller() else {
return
}
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
presentPeerReportOptions(
f(.default)
self.isReporting = true
self.updateIsProgressPaused()
component.context.sharedContext.makeContentReportScreen(
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
subject: .stories(component.slice.effectivePeer.id, [component.slice.item.storyItem.id]),
forceDark: true,
present: { c in
controller.push(c)
},
completion: { [weak self] in
guard let self else {
return
}
self.isReporting = isReporting
self.isReporting = false
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
// )
// }
// )
})))
}
}

View File

@ -1957,22 +1957,30 @@ extension ChatControllerImpl {
])
strongSelf.present(controller, in: .window(.root))
} else {
strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in
self?.present(c, in: .window(.root), with: a)
}, push: { c in
self?.push(c)
}, completion: { _, done in
if done {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
}
}), in: .window(.root))
strongSelf.context.sharedContext.makeContentReportScreen(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), forceDark: false, present: { [weak self] controller in
self?.push(controller)
}, completion: { [weak self] in
self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
})
}
}
}, reportMessages: { [weak self] messages, contextController in
if let strongSelf = self, !messages.isEmpty {
let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other]
presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in })
guard let self, !messages.isEmpty else {
return
}
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
contextController?.dismiss(completion: {
guard let strongSelf = self else {

View File

@ -73,6 +73,7 @@ import MiniAppListScreen
import GiftOptionsScreen
import GiftViewScreen
import StarsIntroScreen
import ContentReportScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -2819,6 +2820,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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> {
return MiniAppListScreen.initialData(context: context)
}

View File

@ -48,7 +48,9 @@ public enum DeviceModel: CaseIterable, Equatable {
.iPhone15,
.iPhone15Plus,
.iPhone15Pro,
.iPhone15ProMax
.iPhone15ProMax,
.iPhone16Pro,
.iPhone16ProMax
]
}
@ -116,6 +118,9 @@ public enum DeviceModel: CaseIterable, Equatable {
case iPhone15Pro
case iPhone15ProMax
case iPhone16Pro
case iPhone16ProMax
case unknown(String)
public var modelId: [String] {
@ -218,6 +223,10 @@ public enum DeviceModel: CaseIterable, Equatable {
return ["iPhone16,1"]
case .iPhone15ProMax:
return ["iPhone16,2"]
case .iPhone16Pro:
return ["iPhone17,1"]
case .iPhone16ProMax:
return ["iPhone17,2"]
case let .unknown(modelId):
return [modelId]
}
@ -323,6 +332,10 @@ public enum DeviceModel: CaseIterable, Equatable {
return "iPhone 15 Pro"
case .iPhone15ProMax:
return "iPhone 15 Pro Max"
case .iPhone16Pro:
return "iPhone 16 Pro"
case .iPhone16ProMax:
return "iPhone 16 Pro Max"
case let .unknown(modelId):
if modelId.hasPrefix("iPhone") {
return "Unknown iPhone"