Merge branch 'master' into post-suggestion

This commit is contained in:
Isaac 2025-04-25 10:55:31 +01:00
commit d6334b9748
19 changed files with 390 additions and 286 deletions

43
.vscode/launch.json vendored
View File

@ -5,15 +5,40 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "sweetpad-lldb", "type": "swift",
"request": "attach", "request": "launch",
"name": "Attach to running app (SweetPad)", "args": [],
"preLaunchTask": "sweetpad: launch", "cwd": "${workspaceFolder:telegram-ios}",
"codelldbAttributes": { "name": "Debug Telegram",
"initCommands": [ "program": "${workspaceFolder:telegram-ios}/.build/debug/Telegram",
"command source ~/.lldbinit-Xcode" "preLaunchTask": "swift: Build Debug Telegram"
] },
} {
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:telegram-ios}",
"name": "Release Telegram",
"program": "${workspaceFolder:telegram-ios}/.build/release/Telegram",
"preLaunchTask": "swift: Build Release Telegram"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:telegram-ios}",
"name": "Debug telegram-ios",
"program": "${workspaceFolder:telegram-ios}/.build/debug/telegram-ios",
"preLaunchTask": "swift: Build Debug telegram-ios"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:telegram-ios}",
"name": "Release telegram-ios",
"program": "${workspaceFolder:telegram-ios}/.build/release/telegram-ios",
"preLaunchTask": "swift: Build Release telegram-ios"
} }
] ]
} }

84
.vscode/settings.json vendored
View File

@ -2,78 +2,14 @@
"sweetpad.build.xcodeWorkspacePath": "Telegram/Telegram.xcodeproj/project.xcworkspace", "sweetpad.build.xcodeWorkspacePath": "Telegram/Telegram.xcodeproj/project.xcworkspace",
"lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
"lldb.launch.expressions": "native", "lldb.launch.expressions": "native",
"files.associations": { "search.followSymlinks": false,
"__bit_reference": "cpp", "files.exclude": {
"__hash_table": "cpp", ".git/**": true
"__locale": "cpp", },
"__node_handle": "cpp", "files.watcherExclude": {
"__split_buffer": "cpp", ".git/**": true
"__threading_support": "cpp", },
"__tree": "cpp", "search.exclude": {
"__verbose_abort": "cpp", ".git/**": true
"any": "cpp",
"array": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"cfenv": "cpp",
"charconv": "cpp",
"cinttypes": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"complex": "cpp",
"condition_variable": "cpp",
"csignal": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"execution": "cpp",
"memory": "cpp",
"forward_list": "cpp",
"fstream": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"list": "cpp",
"locale": "cpp",
"map": "cpp",
"mutex": "cpp",
"new": "cpp",
"optional": "cpp",
"ostream": "cpp",
"print": "cpp",
"queue": "cpp",
"ratio": "cpp",
"regex": "cpp",
"scoped_allocator": "cpp",
"set": "cpp",
"span": "cpp",
"sstream": "cpp",
"stack": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string": "cpp",
"string_view": "cpp",
"tuple": "cpp",
"typeindex": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"valarray": "cpp",
"variant": "cpp",
"vector": "cpp",
"algorithm": "cpp"
} }
} }

13
.vscode/tasks.json vendored
View File

@ -1,19 +1,6 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"type": "sweetpad",
"action": "launch",
"problemMatcher": [
"$sweetpad-watch", // ! Required for debugging
"$sweetpad-xcodebuild-default",
"$sweetpad-xcbeautify-errors",
"$sweetpad-xcbeautify-warnings"
],
"label": "sweetpad: launch",
"detail": "Build and Launch the app",
"isBackground": true // ! Required for debugging
}
] ]
} }

View File

@ -995,6 +995,11 @@ public enum SendInviteLinkScreenSubject {
case groupCall(link: String) case groupCall(link: String)
} }
public enum StarsWithdrawalScreenSubject {
case withdraw
case enterAmount(current: StarsAmount)
}
public protocol SharedAccountContext: AnyObject { public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get } var sharedContainerPath: String { get }
var basePath: String { get } var basePath: String { get }
@ -1179,7 +1184,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, subject: StarsWithdrawalScreenSubject, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController

View File

@ -4294,7 +4294,7 @@ enum GCDAsyncSocketConfig
return; return;
} }
BOOL hasBytesAvailable; BOOL hasBytesAvailable = false;
unsigned long estimatedBytesAvailable; unsigned long estimatedBytesAvailable;
if ([self usingCFStreamForTLS]) if ([self usingCFStreamForTLS])

View File

@ -38,9 +38,10 @@ private final class ChannelPermissionsControllerArguments {
let updateSlowmode: (Int32) -> Void let updateSlowmode: (Int32) -> Void
let updateUnrestrictBoosters: (Int32) -> Void let updateUnrestrictBoosters: (Int32) -> Void
let updateStarsAmount: (StarsAmount?, Bool) -> Void let updateStarsAmount: (StarsAmount?, Bool) -> Void
let openSetCustomStarsAmount: () -> Void
let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void
init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (EnginePeer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, presentConversionToBroadcastGroup: @escaping () -> Void, openChannelExample: @escaping () -> Void, updateSlowmode: @escaping (Int32) -> Void, updateUnrestrictBoosters: @escaping (Int32) -> Void, updateStarsAmount: @escaping (StarsAmount?, Bool) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void) { init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (EnginePeer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, presentConversionToBroadcastGroup: @escaping () -> Void, openChannelExample: @escaping () -> Void, updateSlowmode: @escaping (Int32) -> Void, updateUnrestrictBoosters: @escaping (Int32) -> Void, updateStarsAmount: @escaping (StarsAmount?, Bool) -> Void, openSetCustomStarsAmount: @escaping () -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void) {
self.context = context self.context = context
self.updatePermission = updatePermission self.updatePermission = updatePermission
self.addPeer = addPeer self.addPeer = addPeer
@ -55,6 +56,7 @@ private final class ChannelPermissionsControllerArguments {
self.updateSlowmode = updateSlowmode self.updateSlowmode = updateSlowmode
self.updateUnrestrictBoosters = updateUnrestrictBoosters self.updateUnrestrictBoosters = updateUnrestrictBoosters
self.updateStarsAmount = updateStarsAmount self.updateStarsAmount = updateStarsAmount
self.openSetCustomStarsAmount = openSetCustomStarsAmount
self.toggleIsOptionExpanded = toggleIsOptionExpanded self.toggleIsOptionExpanded = toggleIsOptionExpanded
} }
} }
@ -427,7 +429,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
case let .messagePrice(_, value, maxValue, price): case let .messagePrice(_, value, maxValue, price):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, apply in return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, apply in
arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0), apply) arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0), apply)
}) }, openSetCustom: nil)
case let .messagePriceInfo(_, value): case let .messagePriceInfo(_, value):
return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section)
case let .unrestrictBoostersSwitch(_, title, value): case let .unrestrictBoostersSwitch(_, title, value):
@ -1267,6 +1269,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
|> deliverOnMainQueue).start()) |> deliverOnMainQueue).start())
}) })
} }
}, openSetCustomStarsAmount: {
}, toggleIsOptionExpanded: { flags in }, toggleIsOptionExpanded: { flags in
updateState { state in updateState { state in
var state = state var state = state

View File

@ -20,6 +20,7 @@ private final class IncomingMessagePrivacyScreenArguments {
let infoLinkAction: () -> Void let infoLinkAction: () -> Void
let openExceptions: () -> Void let openExceptions: () -> Void
let openPremiumInfo: () -> Void let openPremiumInfo: () -> Void
let openSetCustomStarsAmount: () -> Void
init( init(
context: AccountContext, context: AccountContext,
@ -27,7 +28,8 @@ private final class IncomingMessagePrivacyScreenArguments {
disabledValuePressed: @escaping () -> Void, disabledValuePressed: @escaping () -> Void,
infoLinkAction: @escaping () -> Void, infoLinkAction: @escaping () -> Void,
openExceptions: @escaping () -> Void, openExceptions: @escaping () -> Void,
openPremiumInfo: @escaping () -> Void openPremiumInfo: @escaping () -> Void,
openSetCustomStarsAmount: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.updateValue = updateValue self.updateValue = updateValue
@ -35,6 +37,7 @@ private final class IncomingMessagePrivacyScreenArguments {
self.infoLinkAction = infoLinkAction self.infoLinkAction = infoLinkAction
self.openExceptions = openExceptions self.openExceptions = openExceptions
self.openPremiumInfo = openPremiumInfo self.openPremiumInfo = openPremiumInfo
self.openSetCustomStarsAmount = openSetCustomStarsAmount
} }
} }
@ -151,6 +154,8 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case let .price(value, maxValue, price, isEnabled): case let .price(value, maxValue, price, isEnabled):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, _ in return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value, _ in
arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0))) arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0)))
}, openSetCustom: {
arguments.openSetCustomStarsAmount()
}, openPremiumInfo: { }, openPremiumInfo: {
arguments.openPremiumInfo() arguments.openPremiumInfo()
}) })
@ -365,6 +370,20 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
controller?.replace(with: c) controller?.replace(with: c)
} }
pushControllerImpl?(controller) pushControllerImpl?(controller)
},
openSetCustomStarsAmount: {
var currentAmount: StarsAmount = StarsAmount(value: 1, nanos: 0)
if case let .paidMessages(value) = stateValue.with({ $0 }).updatedValue {
currentAmount = value
}
let starsScreen = context.sharedContext.makeStarsWithdrawalScreen(context: context, subject: .enterAmount(current: currentAmount), completion: { amount in
updateState { state in
var state = state
state.updatedValue = .paidMessages(StarsAmount(value: amount, nanos: 0))
return state
}
})
pushControllerImpl?(starsScreen)
} }
) )

View File

@ -121,6 +121,7 @@ swift_library(
"//submodules/FastBlur", "//submodules/FastBlur",
"//submodules/InviteLinksUI", "//submodules/InviteLinksUI",
"//third-party/td:TdBinding", "//third-party/td:TdBinding",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -335,6 +335,15 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS
func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant] { func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant] {
return self.call.participants().map { ConferenceCallE2EContext.BlockchainParticipant(userId: $0.userId, internalId: $0.internalId) } return self.call.participants().map { ConferenceCallE2EContext.BlockchainParticipant(userId: $0.userId, internalId: $0.internalId) }
} }
func getParticipantLatencies() -> [Int64: Double] {
let dict = self.call.participantLatencies()
var result: [Int64: Double] = [:]
for (k, v) in dict {
result[k.int64Value] = v.doubleValue
}
return result
}
func getParticipantIds() -> [Int64] { func getParticipantIds() -> [Int64] {
return self.call.participants().map { $0.userId } return self.call.participants().map { $0.userId }

View File

@ -26,6 +26,7 @@ import TooltipUI
import BlurredBackgroundComponent import BlurredBackgroundComponent
import CallsEmoji import CallsEmoji
import InviteLinksUI import InviteLinksUI
import AnimatedTextComponent
extension VideoChatCall { extension VideoChatCall {
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> {
@ -262,6 +263,9 @@ final class VideoChatScreenComponent: Component {
var invitedPeers: [InvitedPeer] = [] var invitedPeers: [InvitedPeer] = []
var invitedPeersDisposable: Disposable? var invitedPeersDisposable: Disposable?
var lastTitleEvent: String?
var lastTitleEventTimer: Foundation.Timer?
var speakingParticipantPeers: [EnginePeer] = [] var speakingParticipantPeers: [EnginePeer] = []
var visibleParticipants: Set<EnginePeer.Id> = Set() var visibleParticipants: Set<EnginePeer.Id> = Set()
@ -320,6 +324,7 @@ final class VideoChatScreenComponent: Component {
self.inviteDisposable.dispose() self.inviteDisposable.dispose()
self.conferenceCallStateDisposable?.dispose() self.conferenceCallStateDisposable?.dispose()
self.encryptionKeyEmojiDisposable?.dispose() self.encryptionKeyEmojiDisposable?.dispose()
self.lastTitleEventTimer?.invalidate()
} }
func animateIn() { func animateIn() {
@ -1600,12 +1605,13 @@ final class VideoChatScreenComponent: Component {
}) })
self.memberEventsDisposable?.dispose() self.memberEventsDisposable?.dispose()
if groupCall.peerId != nil { self.memberEventsDisposable = (groupCall.memberEvents
self.memberEventsDisposable = (groupCall.memberEvents |> deliverOnMainQueue).start(next: { [weak self] event in
|> deliverOnMainQueue).start(next: { [weak self] event in guard let self, let members = self.members, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
guard let self, let members = self.members, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return
return }
}
if groupCall.peerId != nil {
if event.joined { if event.joined {
var displayEvent = false var displayEvent = false
if case let .channel(channel) = self.peer, case .broadcast = channel.info { if case let .channel(channel) = self.peer, case .broadcast = channel.info {
@ -1624,8 +1630,30 @@ final class VideoChatScreenComponent: Component {
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
} }
} }
}) } else {
} if event.joined {
self.lastTitleEvent = "\(event.peer.compactDisplayTitle) joined"
} else {
self.lastTitleEvent = "\(event.peer.compactDisplayTitle) left"
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
self.lastTitleEventTimer?.invalidate()
self.lastTitleEventTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.5, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.lastTitleEventTimer = nil
self.lastTitleEvent = nil
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
})
}
})
case let .conferenceSource(conferenceSource): case let .conferenceSource(conferenceSource):
self.membersDisposable?.dispose() self.membersDisposable?.dispose()
self.membersDisposable = (View.groupCallMembersForConferenceSource(conferenceSource: conferenceSource) self.membersDisposable = (View.groupCallMembersForConferenceSource(conferenceSource: conferenceSource)
@ -1887,14 +1915,17 @@ final class VideoChatScreenComponent: Component {
let maxSingleColumnWidth: CGFloat = 620.0 let maxSingleColumnWidth: CGFloat = 620.0
let isTwoColumnLayout: Bool let isTwoColumnLayout: Bool
let isLandscape: Bool
if availableSize.width > maxSingleColumnWidth { if availableSize.width > maxSingleColumnWidth {
if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
isTwoColumnLayout = true isTwoColumnLayout = true
} else { } else {
isTwoColumnLayout = false isTwoColumnLayout = false
} }
isLandscape = true
} else { } else {
isTwoColumnLayout = false isTwoColumnLayout = false
isLandscape = false
} }
var containerOffset: CGFloat = 0.0 var containerOffset: CGFloat = 0.0
@ -1935,24 +1966,32 @@ final class VideoChatScreenComponent: Component {
} }
}) })
let landscapeControlsWidth: CGFloat = 88.0 let landscapeControlsWidth: CGFloat = 104.0
let landscapeControlsOffsetX: CGFloat = 18.0 var landscapeControlsOffsetX: CGFloat = 0.0
let landscapeControlsSpacing: CGFloat = 30.0 let landscapeControlsSpacing: CGFloat = 30.0
let leftInset: CGFloat = max(environment.safeInsets.left, 14.0) var leftInset: CGFloat = max(environment.safeInsets.left, 14.0)
var rightInset: CGFloat = max(environment.safeInsets.right, 14.0) var rightInset: CGFloat = max(environment.safeInsets.right, 14.0)
var buttonsOnTheSide = false var buttonsOnTheSide = false
if availableSize.width > maxSingleColumnWidth && !environment.metrics.isTablet { if availableSize.width > maxSingleColumnWidth && !environment.metrics.isTablet {
leftInset += 2.0
rightInset += 2.0
buttonsOnTheSide = true buttonsOnTheSide = true
rightInset += landscapeControlsWidth if case .landscapeLeft = environment.orientation {
rightInset = max(rightInset, environment.safeInsets.left + landscapeControlsWidth)
landscapeControlsOffsetX = -environment.safeInsets.left
} else {
rightInset = max(rightInset, landscapeControlsWidth)
}
} }
let topInset: CGFloat = environment.statusBarHeight + 2.0 let topInset: CGFloat = environment.statusBarHeight + 2.0
let navigationBarHeight: CGFloat = 61.0 let navigationBarHeight: CGFloat = 61.0
var navigationHeight = topInset + navigationBarHeight var navigationHeight = topInset + navigationBarHeight
let navigationButtonAreaWidth: CGFloat = 40.0 let navigationButtonAreaWidth: CGFloat = 34.0
let navigationButtonDiameter: CGFloat = 28.0 let navigationButtonDiameter: CGFloat = 28.0
let navigationLeftButtonSize = self.navigationLeftButton.update( let navigationLeftButtonSize = self.navigationLeftButton.update(
@ -2013,7 +2052,10 @@ final class VideoChatScreenComponent: Component {
alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0)
} }
let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) var navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
if buttonsOnTheSide {
navigationRightButtonFrame.origin.x += 42.0
}
if let navigationRightButtonView = self.navigationRightButton.view { if let navigationRightButtonView = self.navigationRightButton.view {
if navigationRightButtonView.superview == nil { if navigationRightButtonView.superview == nil {
self.containerView.addSubview(navigationRightButtonView) self.containerView.addSubview(navigationRightButtonView)
@ -2022,6 +2064,7 @@ final class VideoChatScreenComponent: Component {
alphaTransition.setAlpha(view: navigationRightButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) alphaTransition.setAlpha(view: navigationRightButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0)
} }
var navigationSidebarButtonFrame: CGRect?
if isTwoColumnLayout { if isTwoColumnLayout {
var navigationSidebarButtonTransition = transition var navigationSidebarButtonTransition = transition
let navigationSidebarButton: ComponentView<Empty> let navigationSidebarButton: ComponentView<Empty>
@ -2057,7 +2100,8 @@ final class VideoChatScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter) containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
) )
let navigationSidebarButtonFrame = CGRect(origin: CGPoint(x: navigationRightButtonFrame.minX - 32.0 - navigationSidebarButtonSize.width, y: topInset + floor((navigationBarHeight - navigationSidebarButtonSize.height) * 0.5)), size: navigationSidebarButtonSize) let navigationSidebarButtonFrameValue = CGRect(origin: CGPoint(x: navigationRightButtonFrame.minX - 21.0 - navigationSidebarButtonSize.width, y: topInset + floor((navigationBarHeight - navigationSidebarButtonSize.height) * 0.5)), size: navigationSidebarButtonSize)
navigationSidebarButtonFrame = navigationSidebarButtonFrameValue
if let navigationSidebarButtonView = navigationSidebarButton.view { if let navigationSidebarButtonView = navigationSidebarButton.view {
var animateIn = false var animateIn = false
if navigationSidebarButtonView.superview == nil { if navigationSidebarButtonView.superview == nil {
@ -2066,7 +2110,7 @@ final class VideoChatScreenComponent: Component {
self.containerView.insertSubview(navigationSidebarButtonView, aboveSubview: navigationRightButtonView) self.containerView.insertSubview(navigationSidebarButtonView, aboveSubview: navigationRightButtonView)
} }
} }
navigationSidebarButtonTransition.setFrame(view: navigationSidebarButtonView, frame: navigationSidebarButtonFrame) navigationSidebarButtonTransition.setFrame(view: navigationSidebarButtonView, frame: navigationSidebarButtonFrameValue)
if animateIn { if animateIn {
transition.animateScale(view: navigationSidebarButtonView, from: 0.001, to: 1.0) transition.animateScale(view: navigationSidebarButtonView, from: 0.001, to: 1.0)
transition.animateAlpha(view: navigationSidebarButtonView, from: 0.0, to: 1.0) transition.animateAlpha(view: navigationSidebarButtonView, from: 0.0, to: 1.0)
@ -2082,17 +2126,27 @@ final class VideoChatScreenComponent: Component {
} }
} }
let idleTitleStatusText: String var idleTitleStatusText: [AnimatedTextComponent.Item] = []
if let callState = self.callState { if let callState = self.callState {
if callState.networkState == .connected, let members = self.members { if callState.networkState == .connected, let members = self.members {
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) //TODO:localize
let totalCount = max(1, members.totalCount)
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .number(totalCount, minDigits: 0)))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: false, content: .text(totalCount == 1 ? " participant" : " participants")))
if let lastTitleEvent = self.lastTitleEvent {
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(6), isUnbreakable: false, content: .text(", \(lastTitleEvent)")))
} else if !self.invitedPeers.isEmpty {
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(3), isUnbreakable: true, content: .text(", ")))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(4), isUnbreakable: false, content: .number(self.invitedPeers.count, minDigits: 0)))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(5), isUnbreakable: false, content: .text(" invited")))
}
} else if callState.scheduleTimestamp != nil { } else if callState.scheduleTimestamp != nil {
idleTitleStatusText = environment.strings.VoiceChat_Scheduled idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .text(environment.strings.VoiceChat_Scheduled)))
} else { } else {
idleTitleStatusText = environment.strings.VoiceChat_Connecting idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .text(environment.strings.VoiceChat_Connecting)))
} }
} else { } else {
idleTitleStatusText = " " idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .text(" ")))
} }
let canManageCall = self.callState?.canManageCall ?? false let canManageCall = self.callState?.canManageCall ?? false
@ -2108,6 +2162,7 @@ final class VideoChatScreenComponent: Component {
title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? environment.strings.VideoChat_GroupCallTitle, title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? environment.strings.VideoChat_GroupCallTitle,
status: idleTitleStatusText, status: idleTitleStatusText,
isRecording: self.callState?.recordingStartTimestamp != nil, isRecording: self.callState?.recordingStartTimestamp != nil,
isLandscape: isLandscape,
strings: environment.strings, strings: environment.strings,
tapAction: self.callState?.recordingStartTimestamp != nil ? { [weak self] in tapAction: self.callState?.recordingStartTimestamp != nil ? { [weak self] in
guard let self, let environment = self.environment, let currentCall = self.currentCall else { guard let self, let environment = self.environment, let currentCall = self.currentCall else {
@ -2146,7 +2201,12 @@ final class VideoChatScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: maxTitleWidth, height: 100.0) containerSize: CGSize(width: maxTitleWidth, height: 100.0)
) )
let titleFrame = CGRect(origin: CGPoint(x: leftInset + floor((availableSize.width - leftInset - rightInset - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize) var titleFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize)
if isLandscape {
titleFrame.origin.x = navigationLeftButtonFrame.maxX + 20.0
} else {
titleFrame.origin.x = leftInset + floor((availableSize.width - leftInset - rightInset - titleSize.width) * 0.5)
}
if let titleView = self.title.view { if let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
self.containerView.addSubview(titleView) self.containerView.addSubview(titleView)
@ -2155,6 +2215,27 @@ final class VideoChatScreenComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) alphaTransition.setAlpha(view: titleView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0)
} }
let areButtonsCollapsed: Bool
let mainColumnWidth: CGFloat
let mainColumnSideInset: CGFloat
if isTwoColumnLayout {
areButtonsCollapsed = false
mainColumnWidth = min(isLandscape ? 340.0 : 320.0, availableSize.width - leftInset - rightInset - 340.0)
mainColumnSideInset = 0.0
} else {
areButtonsCollapsed = self.expandedParticipantsVideoState != nil
if availableSize.width > maxSingleColumnWidth {
mainColumnWidth = 420.0
mainColumnSideInset = 0.0
} else {
mainColumnWidth = availableSize.width
mainColumnSideInset = max(leftInset, rightInset)
}
}
var encryptionKeyFrame: CGRect? var encryptionKeyFrame: CGRect?
var isConference = false var isConference = false
if case let .group(groupCall) = self.currentCall { if case let .group(groupCall) = self.currentCall {
@ -2163,7 +2244,9 @@ final class VideoChatScreenComponent: Component {
isConference = true isConference = true
} }
if isConference { if isConference {
navigationHeight -= 2.0 if !isLandscape {
navigationHeight -= 2.0
}
let encryptionKey: ComponentView<Empty> let encryptionKey: ComponentView<Empty>
var encryptionKeyTransition = transition var encryptionKeyTransition = transition
if let current = self.encryptionKey { if let current = self.encryptionKey {
@ -2194,11 +2277,34 @@ final class VideoChatScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: min(400.0, availableSize.width - leftInset - rightInset - 16.0 * 2.0), height: 10000.0) containerSize: CGSize(width: min(400.0, availableSize.width - leftInset - rightInset - 16.0 * 2.0), height: 10000.0)
) )
let encryptionKeyFrameValue = CGRect(origin: CGPoint(x: leftInset + floor((availableSize.width - leftInset - rightInset - encryptionKeySize.width) * 0.5), y: navigationHeight), size: encryptionKeySize) var encryptionKeyFrameValue = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: encryptionKeySize)
if isLandscape {
let maxEncryptionKeyX: CGFloat
if let navigationSidebarButtonFrame {
maxEncryptionKeyX = navigationSidebarButtonFrame.minX - 8.0 - encryptionKeySize.width
} else {
maxEncryptionKeyX = navigationRightButtonFrame.minX - 8.0 - encryptionKeySize.width
}
let idealEncryptionKeyX: CGFloat
if isTwoColumnLayout {
idealEncryptionKeyX = availableSize.width - rightInset - mainColumnWidth
} else {
idealEncryptionKeyX = maxEncryptionKeyX - 13.0
}
encryptionKeyFrameValue.origin.x = min(idealEncryptionKeyX, maxEncryptionKeyX)
encryptionKeyFrameValue.origin.y = navigationLeftButtonFrame.minY + floorToScreenPixels((navigationLeftButtonFrame.height - encryptionKeySize.height) * 0.5)
} else {
encryptionKeyFrameValue.origin.x = leftInset + floor((availableSize.width - leftInset - rightInset - encryptionKeySize.width) * 0.5)
encryptionKeyFrameValue.origin.y = navigationHeight
}
encryptionKeyFrame = encryptionKeyFrameValue encryptionKeyFrame = encryptionKeyFrameValue
navigationHeight += encryptionKeySize.height if !isLandscape {
navigationHeight += 16.0 navigationHeight += encryptionKeySize.height
navigationHeight += 16.0
}
} else if let encryptionKey = self.encryptionKey { } else if let encryptionKey = self.encryptionKey {
self.encryptionKey = nil self.encryptionKey = nil
encryptionKey.view?.removeFromSuperview() encryptionKey.view?.removeFromSuperview()
@ -2207,27 +2313,6 @@ final class VideoChatScreenComponent: Component {
self.encryptionKeyBackground = nil self.encryptionKeyBackground = nil
} }
let areButtonsCollapsed: Bool
let mainColumnWidth: CGFloat
let mainColumnSideInset: CGFloat
if isTwoColumnLayout {
areButtonsCollapsed = false
mainColumnWidth = 320.0
mainColumnSideInset = 0.0
} else {
areButtonsCollapsed = self.expandedParticipantsVideoState != nil
if availableSize.width > maxSingleColumnWidth {
mainColumnWidth = 420.0
mainColumnSideInset = 0.0
} else {
mainColumnWidth = availableSize.width
mainColumnSideInset = max(leftInset, rightInset)
}
}
let actionButtonDiameter: CGFloat = 56.0 let actionButtonDiameter: CGFloat = 56.0
let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter
var collapsedMicrophoneButtonDiameter: CGFloat = 116.0 var collapsedMicrophoneButtonDiameter: CGFloat = 116.0
@ -2285,7 +2370,7 @@ final class VideoChatScreenComponent: Component {
if buttonsOnTheSide { if buttonsOnTheSide {
collapsedMicrophoneButtonFrame.origin.y = floor((availableSize.height - actionButtonDiameter) * 0.5) collapsedMicrophoneButtonFrame.origin.y = floor((availableSize.height - actionButtonDiameter) * 0.5)
collapsedMicrophoneButtonFrame.origin.x = availableSize.width - environment.safeInsets.right - landscapeControlsWidth + landscapeControlsOffsetX collapsedMicrophoneButtonFrame.origin.x = availableSize.width - landscapeControlsWidth + landscapeControlsOffsetX + floor((landscapeControlsWidth - actionButtonDiameter) * 0.5)
if isMainColumnHidden { if isMainColumnHidden {
collapsedMicrophoneButtonFrame.origin.x += mainColumnWidth + landscapeControlsWidth collapsedMicrophoneButtonFrame.origin.x += mainColumnWidth + landscapeControlsWidth

View File

@ -6,19 +6,22 @@ import MultilineTextComponent
import TelegramPresentationData import TelegramPresentationData
import HierarchyTrackingLayer import HierarchyTrackingLayer
import ChatTitleActivityNode import ChatTitleActivityNode
import AnimatedTextComponent
final class VideoChatTitleComponent: Component { final class VideoChatTitleComponent: Component {
let title: String let title: String
let status: String let status: [AnimatedTextComponent.Item]
let isRecording: Bool let isRecording: Bool
let isLandscape: Bool
let strings: PresentationStrings let strings: PresentationStrings
let tapAction: (() -> Void)? let tapAction: (() -> Void)?
let longTapAction: (() -> Void)? let longTapAction: (() -> Void)?
init( init(
title: String, title: String,
status: String, status: [AnimatedTextComponent.Item],
isRecording: Bool, isRecording: Bool,
isLandscape: Bool,
strings: PresentationStrings, strings: PresentationStrings,
tapAction: (() -> Void)?, tapAction: (() -> Void)?,
longTapAction: (() -> Void)? longTapAction: (() -> Void)?
@ -26,6 +29,7 @@ final class VideoChatTitleComponent: Component {
self.title = title self.title = title
self.status = status self.status = status
self.isRecording = isRecording self.isRecording = isRecording
self.isLandscape = isLandscape
self.strings = strings self.strings = strings
self.tapAction = tapAction self.tapAction = tapAction
self.longTapAction = longTapAction self.longTapAction = longTapAction
@ -41,6 +45,9 @@ final class VideoChatTitleComponent: Component {
if lhs.isRecording != rhs.isRecording { if lhs.isRecording != rhs.isRecording {
return false return false
} }
if lhs.isLandscape != rhs.isLandscape {
return false
}
if lhs.strings !== rhs.strings { if lhs.strings !== rhs.strings {
return false return false
} }
@ -211,12 +218,14 @@ final class VideoChatTitleComponent: Component {
) )
let statusComponent: AnyComponent<Empty> let statusComponent: AnyComponent<Empty>
statusComponent = AnyComponent(MultilineTextComponent( statusComponent = AnyComponent(AnimatedTextComponent(
text: .plain(NSAttributedString(string: component.status, font: Font.regular(13.0), textColor: UIColor(white: 1.0, alpha: 0.5))) font: Font.regular(13.0),
color: UIColor(white: 1.0, alpha: 0.5),
items: component.status
)) ))
let statusSize = self.status.update( let statusSize = self.status.update(
transition: .immediate, transition: transition,
component: statusComponent, component: statusComponent,
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0) containerSize: CGSize(width: availableSize.width, height: 100.0)
@ -224,7 +233,10 @@ final class VideoChatTitleComponent: Component {
let size = CGSize(width: availableSize.width, height: titleSize.height + spacing + statusSize.height) let size = CGSize(width: availableSize.width, height: titleSize.height + spacing + statusSize.height)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 0.0), size: titleSize) var titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize)
if !component.isLandscape {
titleFrame.origin.x = floor((size.width - titleSize.width) * 0.5)
}
if let titleView = self.title.view { if let titleView = self.title.view {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
@ -235,13 +247,17 @@ final class VideoChatTitleComponent: Component {
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
} }
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize) var statusFrame = CGRect(origin: CGPoint(x: 0.0, y: titleFrame.maxY + spacing), size: statusSize)
if !component.isLandscape {
statusFrame.origin.x = floor((size.width - statusSize.width) * 0.5)
}
if let statusView = self.status.view { if let statusView = self.status.view {
if statusView.superview == nil { if statusView.superview == nil {
statusView.layer.anchorPoint = CGPoint()
statusView.isUserInteractionEnabled = false statusView.isUserInteractionEnabled = false
self.addSubview(statusView) self.addSubview(statusView)
} }
transition.setPosition(view: statusView, position: statusFrame.center) transition.setPosition(view: statusView, position: statusFrame.origin)
statusView.bounds = CGRect(origin: CGPoint(), size: statusFrame.size) statusView.bounds = CGRect(origin: CGPoint(), size: statusFrame.size)
} }

View File

@ -53,9 +53,6 @@ func closeButtonImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0) context.setLineWidth(2.0)
context.setLineCap(.round) context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor) context.setStrokeColor(UIColor.white.cgColor)

View File

@ -5,6 +5,7 @@ public protocol ConferenceCallE2EContextState: AnyObject {
func getEmojiState() -> Data? func getEmojiState() -> Data?
func getParticipantIds() -> [Int64] func getParticipantIds() -> [Int64]
func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant] func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant]
func getParticipantLatencies() -> [Int64: Double]
func applyBlock(block: Data) func applyBlock(block: Data)
func applyBroadcastBlock(block: Data) func applyBroadcastBlock(block: Data)
@ -166,7 +167,7 @@ public final class ConferenceCallE2EContext {
let keyPair = self.keyPair let keyPair = self.keyPair
let userId = self.userId let userId = self.userId
let initializeState = self.initializeState let initializeState = self.initializeState
let (outBlocks, outEmoji, outBlockchainParticipants) = self.state.with({ callState -> ([Data], Data, [BlockchainParticipant]) in let (outBlocks, outEmoji, outBlockchainParticipants, participantLatencies) = self.state.with({ callState -> ([Data], Data, [BlockchainParticipant], [Int64: Double]) in
if let state = callState.state { if let state = callState.state {
for block in blocks { for block in blocks {
if subChainId == 0 { if subChainId == 0 {
@ -175,26 +176,26 @@ public final class ConferenceCallE2EContext {
state.applyBroadcastBlock(block: block) state.applyBroadcastBlock(block: block)
} }
} }
return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants()) return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants(), state.getParticipantLatencies())
} else { } else {
if subChainId == 0 { if subChainId == 0 {
guard let block = blocks.last else { guard let block = blocks.last else {
return ([], Data(), []) return ([], Data(), [], [:])
} }
guard let state = initializeState(keyPair, userId, block) else { guard let state = initializeState(keyPair, userId, block) else {
return ([], Data(), []) return ([], Data(), [], [:])
} }
callState.state = state callState.state = state
for block in callState.pendingIncomingBroadcastBlocks { for block in callState.pendingIncomingBroadcastBlocks {
state.applyBroadcastBlock(block: block) state.applyBroadcastBlock(block: block)
} }
callState.pendingIncomingBroadcastBlocks.removeAll() callState.pendingIncomingBroadcastBlocks.removeAll()
return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants()) return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants(), state.getParticipantLatencies())
} else if subChainId == 1 { } else if subChainId == 1 {
callState.pendingIncomingBroadcastBlocks.append(contentsOf: blocks) callState.pendingIncomingBroadcastBlocks.append(contentsOf: blocks)
return ([], Data(), []) return ([], Data(), [], [:])
} else { } else {
return ([], Data(), []) return ([], Data(), [], [:])
} }
} }
}) })
@ -204,6 +205,10 @@ public final class ConferenceCallE2EContext {
for outBlock in outBlocks { for outBlock in outBlocks {
let _ = self.engine.calls.sendConferenceCallBroadcast(callId: self.callId, accessHash: self.accessHash, block: outBlock).startStandalone() let _ = self.engine.calls.sendConferenceCallBroadcast(callId: self.callId, accessHash: self.accessHash, block: outBlock).startStandalone()
} }
#if DEBUG
print("Latencies: \(participantLatencies)")
#endif
} }
private func e2ePoll(subChainId: Int) { private func e2ePoll(subChainId: Int) {

View File

@ -26,9 +26,10 @@ public final class MessagePriceItem: ListViewItem, ItemListItem {
let price: String let price: String
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let updated: (Int64, Bool) -> Void let updated: (Int64, Bool) -> Void
let openSetCustom: (() -> Void)?
let openPremiumInfo: (() -> Void)? let openPremiumInfo: (() -> Void)?
public init(theme: PresentationTheme, strings: PresentationStrings, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64, Bool) -> Void, openPremiumInfo: (() -> Void)? = nil) { public init(theme: PresentationTheme, strings: PresentationStrings, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64, Bool) -> Void, openSetCustom: (() -> Void)? = nil, openPremiumInfo: (() -> Void)? = nil) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.isEnabled = isEnabled self.isEnabled = isEnabled
@ -38,6 +39,7 @@ public final class MessagePriceItem: ListViewItem, ItemListItem {
self.price = price self.price = price
self.sectionId = sectionId self.sectionId = sectionId
self.updated = updated self.updated = updated
self.openSetCustom = openSetCustom
self.openPremiumInfo = openPremiumInfo self.openPremiumInfo = openPremiumInfo
} }
@ -161,6 +163,8 @@ private class MessagePriceItemNode: ListViewItemNode {
private var sliderView: TGPhotoEditorSliderView? private var sliderView: TGPhotoEditorSliderView?
private let leftTextNode: ImmediateTextNode private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode private let rightTextNode: ImmediateTextNode
private let centerTextButtonNode: HighlightableButtonNode
private let centerTextButtonBackground: UIImageView
private let centerLeftTextNode: ImmediateTextNode private let centerLeftTextNode: ImmediateTextNode
private let centerRightTextNode: ImmediateTextNode private let centerRightTextNode: ImmediateTextNode
private let lockIconNode: ASImageNode private let lockIconNode: ASImageNode
@ -186,8 +190,13 @@ private class MessagePriceItemNode: ListViewItemNode {
self.leftTextNode = ImmediateTextNode() self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode() self.rightTextNode = ImmediateTextNode()
self.centerTextButtonNode = HighlightableButtonNode()
self.centerTextButtonBackground = UIImageView()
self.centerLeftTextNode = ImmediateTextNode() self.centerLeftTextNode = ImmediateTextNode()
self.centerLeftTextNode.isUserInteractionEnabled = false
self.centerRightTextNode = ImmediateTextNode() self.centerRightTextNode = ImmediateTextNode()
self.centerRightTextNode.isUserInteractionEnabled = false
self.lockIconNode = ASImageNode() self.lockIconNode = ASImageNode()
self.lockIconNode.displaysAsynchronously = false self.lockIconNode.displaysAsynchronously = false
@ -198,9 +207,13 @@ private class MessagePriceItemNode: ListViewItemNode {
self.addSubnode(self.leftTextNode) self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode) self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerLeftTextNode) self.addSubnode(self.centerTextButtonNode)
self.addSubnode(self.centerRightTextNode) self.centerTextButtonNode.view.addSubview(self.centerTextButtonBackground)
self.centerTextButtonNode.addSubnode(self.centerLeftTextNode)
self.centerTextButtonNode.addSubnode(self.centerRightTextNode)
self.addSubnode(self.lockIconNode) self.addSubnode(self.lockIconNode)
self.centerTextButtonNode.addTarget(self, action: #selector(self.centerTextButtonPressed), forControlEvents: .touchUpInside)
} }
override func didLoad() { override func didLoad() {
@ -231,7 +244,11 @@ private class MessagePriceItemNode: ListViewItemNode {
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView self.sliderView = sliderView
} }
@objc private func centerTextButtonPressed() {
self.item?.openSetCustom?()
}
func asyncLayout() -> (_ item: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item let currentItem = self.item
@ -312,8 +329,8 @@ private class MessagePriceItemNode: ListViewItemNode {
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let centralLeftText = item.strings.Privacy_Messages_Stars(Int32(item.value)) let centralLeftText = item.strings.Privacy_Messages_Stars(Int32(item.value))
strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.openSetCustom != nil ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor)
strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.theme.list.itemSecondaryTextColor) strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.openSetCustom != nil ? item.theme.list.itemAccentColor.withMultipliedAlpha(0.5) : item.theme.list.itemSecondaryTextColor)
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
@ -328,10 +345,28 @@ private class MessagePriceItemNode: ListViewItemNode {
let totalCenterWidth = centerLeftTextSize.width + centerSpacing + centerRightTextSize.width let totalCenterWidth = centerLeftTextSize.width + centerSpacing + centerRightTextSize.width
let centerLeftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0), y: 11.0), size: centerLeftTextSize) let centerLeftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0), y: 11.0), size: centerLeftTextSize)
strongSelf.centerLeftTextNode.frame = centerLeftFrame
let centerRightFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0) + totalCenterWidth - centerRightTextSize.width, y: 14.0 - UIScreenPixel), size: centerRightTextSize) let centerRightFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0) + totalCenterWidth - centerRightTextSize.width, y: 14.0 - UIScreenPixel), size: centerRightTextSize)
strongSelf.centerRightTextNode.frame = centerRightFrame
let centerButtonFrame = CGRect(origin: CGPoint(x: centerLeftFrame.minX, y: centerLeftFrame.minY), size: CGSize(width: centerRightFrame.maxX - centerLeftFrame.minX, height: centerLeftFrame.height)).insetBy(dx: -8.0, dy: -4.0)
strongSelf.centerTextButtonNode.frame = centerButtonFrame
strongSelf.centerTextButtonBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: UIScreenPixel), size: centerButtonFrame.size)
if strongSelf.centerTextButtonBackground.image == nil {
strongSelf.centerTextButtonBackground.image = generateStretchableFilledCircleImage(diameter: 16.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
strongSelf.centerTextButtonBackground.tintColor = item.theme.list.itemAccentColor.withMultipliedAlpha(0.1)
if item.openSetCustom != nil {
strongSelf.centerTextButtonNode.isEnabled = true
strongSelf.centerTextButtonBackground.isHidden = false
} else {
strongSelf.centerTextButtonNode.isEnabled = false
strongSelf.centerTextButtonBackground.isHidden = true
}
strongSelf.centerLeftTextNode.frame = centerLeftFrame.offsetBy(dx: -centerButtonFrame.minX, dy: -centerButtonFrame.minY)
strongSelf.centerRightTextNode.frame = centerRightFrame.offsetBy(dx: -centerButtonFrame.minX, dy: -centerButtonFrame.minY)
if let sliderView = strongSelf.sliderView { if let sliderView = strongSelf.sliderView {
if themeUpdated { if themeUpdated {
@ -343,12 +378,17 @@ private class MessagePriceItemNode: ListViewItemNode {
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
sliderView.interactionEnded = { [weak self] in sliderView.interactionEnded = {
guard let self else { guard let self else {
return return
} }
self.item?.updated(Int64(self.amount.realValue), true) self.item?.updated(Int64(self.amount.realValue), true)
} }
if !sliderView.isTracking {
strongSelf.amount = Amount(realValue: Int(item.value), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true)
sliderView.value = CGFloat(strongSelf.amount.sliderValue)
}
} }
strongSelf.lockIconNode.isHidden = item.isEnabled strongSelf.lockIconNode.isHidden = item.isEnabled

View File

@ -156,6 +156,15 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0) minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0) maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
amountLabel = nil amountLabel = nil
case .paidMessages:
//TODO:localize
titleString = "Price per Message"
amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price"
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.paidMessageMaxAmount, nanos: 0)
amountLabel = nil
} }
let title = title.update( let title = title.update(
@ -280,6 +289,19 @@ private final class SheetContent: CombinedComponent {
text: .plain(amountInfoString), text: .plain(amountInfoString),
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)) ))
case .paidMessages:
let amountInfoString: NSAttributedString
if let value = state.amount?.value, value > 0 {
let fullValue: Int64 = Int64(value) * 1_000_000_000 * 80 / 100
let amountValue = StarsAmount(value: fullValue / 1_000_000_000, nanos: Int32(fullValue % 1_000_000_000))
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(amountValue) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **80%**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
}
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
default: default:
amountFooter = nil amountFooter = nil
} }
@ -340,6 +362,9 @@ private final class SheetContent: CombinedComponent {
} else { } else {
buttonString = "Sell" buttonString = "Sell"
} }
} else if case .paidMessages = component.mode {
//TODO:localize
buttonString = "OK"
} else if let amount = state.amount { } else if let amount = state.amount {
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else { } else {
@ -432,6 +457,8 @@ private final class SheetContent: CombinedComponent {
amount = nil amount = nil
case .starGiftResell: case .starGiftResell:
amount = nil amount = nil
case let .paidMessages(initialValue):
amount = StarsAmount(value: initialValue, nanos: 0)
} }
self.amount = amount self.amount = amount
@ -553,6 +580,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case paidMedia(Int64?) case paidMedia(Int64?)
case reaction(Int64?) case reaction(Int64?)
case starGiftResell(Bool) case starGiftResell(Bool)
case paidMessages(Int64)
} }
private let context: AccountContext private let context: AccountContext

View File

@ -3664,8 +3664,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion) return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion)
} }
public func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController { public func makeStarsWithdrawalScreen(context: AccountContext, subject: StarsWithdrawalScreenSubject, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion) let mode: StarsWithdrawScreen.Mode
switch subject {
case .withdraw:
mode = .accountWithdraw
case let .enterAmount(current):
mode = .paidMessages(current.value)
}
return StarsWithdrawScreen(context: context, mode: mode, completion: completion)
} }
public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController { public func makeStarGiftResellScreen(context: AccountContext, update: Bool, completion: @escaping (Int64) -> Void) -> ViewController {

View File

@ -1,111 +1,4 @@
'''ffmpeg_header_paths = [
"libavutil/hwcontext.h",
"libavutil/time.h",
"libavutil/hwcontext_cuda.h",
"libavutil/intfloat.h",
"libavutil/error.h",
"libavutil/fifo.h",
"libavutil/blowfish.h",
"libavutil/hwcontext_mediacodec.h",
"libavutil/replaygain.h",
"libavutil/version.h",
"libavutil/murmur3.h",
"libavutil/stereo3d.h",
"libavutil/samplefmt.h",
"libavutil/pixdesc.h",
"libavutil/base64.h",
"libavutil/rational.h",
"libavutil/sha.h",
"libavutil/motion_vector.h",
"libavutil/avconfig.h",
"libavutil/lfg.h",
"libavutil/avutil.h",
"libavutil/xtea.h",
"libavutil/crc.h",
"libavutil/hwcontext_vdpau.h",
"libavutil/frame.h",
"libavutil/file.h",
"libavutil/md5.h",
"libavutil/cast5.h",
"libavutil/hwcontext_vaapi.h",
"libavutil/spherical.h",
"libavutil/ffversion.h",
"libavutil/audio_fifo.h",
"libavutil/tree.h",
"libavutil/threadmessage.h",
"libavutil/attributes.h",
"libavutil/adler32.h",
"libavutil/hwcontext_d3d11va.h",
"libavutil/timecode.h",
"libavutil/sha512.h",
"libavutil/hwcontext_dxva2.h",
"libavutil/display.h",
"libavutil/buffer.h",
"libavutil/camellia.h",
"libavutil/pixelutils.h",
"libavutil/hwcontext_drm.h",
"libavutil/common.h",
"libavutil/hmac.h",
"libavutil/eval.h",
"libavutil/dict.h",
"libavutil/random_seed.h",
"libavutil/opt.h",
"libavutil/mastering_display_metadata.h",
"libavutil/log.h",
"libavutil/aes.h",
"libavutil/macros.h",
"libavutil/bswap.h",
"libavutil/rc4.h",
"libavutil/tea.h",
"libavutil/cpu.h",
"libavutil/lzo.h",
"libavutil/des.h",
"libavutil/channel_layout.h",
"libavutil/encryption_info.h",
"libavutil/twofish.h",
"libavutil/imgutils.h",
"libavutil/hwcontext_videotoolbox.h",
"libavutil/mem.h",
"libavutil/parseutils.h",
"libavutil/ripemd.h",
"libavutil/bprint.h",
"libavutil/hwcontext_qsv.h",
"libavutil/pixfmt.h",
"libavutil/aes_ctr.h",
"libavutil/timestamp.h",
"libavutil/downmix_info.h",
"libavutil/avassert.h",
"libavutil/hash.h",
"libavutil/mathematics.h",
"libavutil/intreadwrite.h",
"libavutil/avstring.h",
"libavformat/version.h",
"libavformat/avio.h",
"libavformat/avformat.h",
"libavcodec/adts_parser.h",
"libavcodec/avcodec.h",
"libavcodec/version.h",
"libavcodec/vdpau.h",
"libavcodec/qsv.h",
"libavcodec/vaapi.h",
"libavcodec/videotoolbox.h",
"libavcodec/xvmc.h",
"libavcodec/mediacodec.h",
"libavcodec/d3d11va.h",
"libavcodec/avfft.h",
"libavcodec/jni.h",
"libavcodec/dirac.h",
"libavcodec/avdct.h",
"libavcodec/ac3_parser.h",
"libavcodec/vorbis_parser.h",
"libavcodec/dxva2.h",
"libavcodec/dv_profile.h",
"libswresample/version.h",
"libswresample/swresample.h",
]'''
ffmpeg_header_paths = [ ffmpeg_header_paths = [
"libavutil/hwcontext.h", "libavutil/hwcontext.h",
"libavutil/time.h", "libavutil/time.h",

View File

@ -37,6 +37,8 @@ NS_ASSUME_NONNULL_BEGIN
- (NSData *)emojiState; - (NSData *)emojiState;
- (NSArray<TdCallParticipant *> *)participants; - (NSArray<TdCallParticipant *> *)participants;
- (NSDictionary<NSNumber *, NSNumber *> *)participantLatencies;
- (void)applyBlock:(NSData *)block; - (void)applyBlock:(NSData *)block;
- (void)applyBroadcastBlock:(NSData *)block; - (void)applyBroadcastBlock:(NSData *)block;

View File

@ -190,6 +190,52 @@ static NSString *hexStringFromData(NSData *data) {
return participants; return participants;
} }
- (NSDictionary<NSNumber *, NSNumber *> *)participantLatencies {
auto describeResult = tde2e_api::call_describe(_callId);
if (describeResult.is_ok()) {
NSString *string = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding];
NSRegularExpression *pairRe = [NSRegularExpression regularExpressionWithPattern:@"(\\d+):(\\d+\\.\\d+)s" options:0 error:NULL];
NSMutableDictionary<NSNumber*, NSNumber*> *commitTimes = [NSMutableDictionary dictionary];
NSMutableDictionary<NSNumber*, NSNumber*> *revealTimes = [NSMutableDictionary dictionary];
// split into lines and look for the two lines
[string enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) {
if ([line containsString:@"commit ="]) {
[pairRe enumerateMatchesInString:line options:0 range:NSMakeRange(0, line.length) usingBlock:^(NSTextCheckingResult * _Nullable match, NSMatchingFlags flags, BOOL * _Nonnull stop) {
NSString *userIdStr = [line substringWithRange:[match rangeAtIndex:1]];
NSString *durStr = [line substringWithRange:[match rangeAtIndex:2]];
NSNumber *uid = @([userIdStr longLongValue]);
NSNumber *dur = @([durStr doubleValue]);
commitTimes[uid] = dur;
}];
}
else if ([line containsString:@"reveal ="]) {
[pairRe enumerateMatchesInString:line options:0 range:NSMakeRange(0, line.length) usingBlock:^(NSTextCheckingResult * _Nullable match, NSMatchingFlags flags, BOOL * _Nonnull stop) {
NSString *userIdStr = [line substringWithRange:[match rangeAtIndex:1]];
NSString *durStr = [line substringWithRange:[match rangeAtIndex:2]];
NSNumber *uid = @([userIdStr longLongValue]);
NSNumber *dur = @([durStr doubleValue]);
revealTimes[uid] = dur;
}];
}
}];
// build final result = commit+reveal
NSMutableDictionary<NSNumber*, NSNumber*> *result = [NSMutableDictionary dictionary];
for (NSNumber *uid in commitTimes) {
double commit = commitTimes[uid].doubleValue;
double reveal = revealTimes[uid].doubleValue; // will be 0 if missing
result[uid] = @(commit + reveal);
}
return result;
}
return @{};
}
- (void)applyBlock:(NSData *)block { - (void)applyBlock:(NSData *)block {
std::string mappedBlock((uint8_t *)block.bytes, ((uint8_t *)block.bytes) + block.length); std::string mappedBlock((uint8_t *)block.bytes, ((uint8_t *)block.bytes) + block.length);